diff options
Diffstat (limited to 'activerecord/lib')
61 files changed, 789 insertions, 364 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 77d17fc975..5a973fa801 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -318,7 +318,7 @@ module ActiveRecord # create_other(attributes={}) | X | | X # create_other!(attributes={}) | X | | X # - # ===Collection associations (one-to-many / many-to-many) + # === Collection associations (one-to-many / many-to-many) # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- @@ -1326,7 +1326,8 @@ module ActiveRecord # Specifies type of the source association used by #has_many <tt>:through</tt> queries where the source # association is a polymorphic #belongs_to. # [:validate] - # If +false+, don't validate the associated objects when saving the parent object. true by default. + # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. # [:autosave] # If true, always save the associated objects or destroy them if marked for destruction, # when saving the parent object. If false, never save or destroy the associated objects. @@ -1456,7 +1457,8 @@ module ActiveRecord # Specifies type of the source association used by #has_one <tt>:through</tt> queries where the source # association is a polymorphic #belongs_to. # [:validate] - # If +false+, don't validate the associated object when saving the parent object. +false+ by default. + # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. # [:autosave] # If true, always save the associated object or destroy it if marked for destruction, # when saving the parent object. If false, never save or destroy the associated object. @@ -1580,7 +1582,8 @@ module ActiveRecord # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>). # [:validate] - # If +false+, don't validate the associated objects when saving the parent object. +false+ by default. + # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. # [:autosave] # If true, always save the associated object or destroy it if marked for destruction, when # saving the parent object. @@ -1766,7 +1769,8 @@ module ActiveRecord # So if a Person class makes a #has_and_belongs_to_many association to Project, # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. # [:validate] - # If +false+, don't validate the associated objects when saving the parent object. +true+ by default. + # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. # [:autosave] # If true, always save the associated objects or destroy them if marked for destruction, when # saving the parent object. diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f7edfbfb5f..62e867a353 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -217,7 +217,8 @@ module ActiveRecord unless record.is_a?(reflection.klass) fresh_class = reflection.class_name.safe_constantize unless fresh_class && record.is_a?(fresh_class) - message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" + message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\ + "got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})" raise ActiveRecord::AssociationTypeMismatch, message end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 882f1225fc..15844de0bc 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -64,11 +64,11 @@ module ActiveRecord foreign_key = join_keys.foreign_key value = transform_value(owner[foreign_key]) - scope = scope.where(table.name.to_sym => { key => value }) + scope = scope.where(table.name => { key => value }) if reflection.type polymorphic_type = transform_value(owner.class.base_class.name) - scope = scope.where(table.name.to_sym => { reflection.type => polymorphic_type }) + scope = scope.where(table.name => { reflection.type => polymorphic_type }) end scope @@ -124,8 +124,7 @@ module ActiveRecord scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) reflection = chain_head - loop do - break unless reflection + while reflection table = reflection.alias_name unless reflection == chain_tail diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 41698c5360..24997370b2 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -61,6 +61,7 @@ module ActiveRecord def update_counters_on_replace(record) if require_counter_update? && different_target?(record) + owner.instance_variable_set :@_after_replace_counter_called, true record.increment!(reflection.counter_cache_column) decrement_counters end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 346329c610..3121e70a04 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -33,6 +33,8 @@ module ActiveRecord::Associations::Builder # :nodoc: if (@_after_create_counter_called ||= false) @_after_create_counter_called = false + elsif (@_after_replace_counter_called ||= false) + @_after_replace_counter_called = false elsif attribute_changed?(foreign_key) && !new_record? if reflection.polymorphic? model = attribute(reflection.foreign_type).try(:constantize) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 2dca6b612e..00355f3e89 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -246,9 +246,12 @@ module ActiveRecord end end - # Count all records using SQL. Construct options and pass them with - # scope to the target class's +count+. + # Returns the number of records. If no arguments are given, it counts all + # columns using SQL. If one argument is given, it counts only the passed + # column using SQL. If a block is given, it counts the number of records + # yielding a true value. def count(column_name = nil) + return super if block_given? relation = scope if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index b9aed05135..9350064028 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -715,12 +715,13 @@ module ActiveRecord end alias uniq distinct - # Count all records using SQL. + # Count all records. # # class Person < ActiveRecord::Base # has_many :pets # end # + # # This will perform the count using SQL. # person.pets.count # => 3 # person.pets # # => [ @@ -728,8 +729,13 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil) - @association.count(column_name) + # + # Passing a block will select all of a person's pets in SQL and then + # perform the count using Ruby. + # + # person.pets.count { |pet| pet.name.include?('-') } # => 2 + def count(column_name = nil, &block) + @association.count(column_name, &block) end # Returns the size of the collection. If the collection hasn't been loaded, diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 3c4c8f10ec..24231dc9e1 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -170,7 +170,7 @@ module ActiveRecord super(name, nil, Type::Value.new) end - def value + def type_cast(*) nil end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index e0ceafc617..519de271c3 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -67,12 +67,14 @@ module ActiveRecord # # A default can also be provided. # + # # db/schema.rb # create_table :store_listings, force: true do |t| # t.string :my_string, default: "original default" # end # # StoreListing.new.my_string # => "original default" # + # # app/models/store_listing.rb # class StoreListing < ActiveRecord::Base # attribute :my_string, :string, default: "new default" # end @@ -89,6 +91,7 @@ module ActiveRecord # # \Attributes do not need to be backed by a database column. # + # # app/models/my_model.rb # class MyModel < ActiveRecord::Base # attribute :my_string, :string # attribute :my_int_array, :integer, array: true @@ -131,7 +134,7 @@ module ActiveRecord # # config/initializers/types.rb # ActiveRecord::Type.register(:money, MoneyType) # - # # /app/models/store_listing.rb + # # app/models/store_listing.rb # class StoreListing < ActiveRecord::Base # attribute :price_in_cents, :money # end @@ -167,8 +170,10 @@ module ActiveRecord # end # end # + # # config/initializers/types.rb # ActiveRecord::Type.register(:money, MoneyType) # + # # app/models/product.rb # class Product < ActiveRecord::Base # currency_converter = ConversionRatesFromTheInternet.new # attribute :price_in_bitcoins, :money, currency_converter: currency_converter diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 1f1b11eb68..95de6937af 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -53,9 +53,9 @@ module ActiveRecord # end # # class Firm < ActiveRecord::Base - # # Destroys the associated clients and people when the firm is destroyed - # before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } - # before_destroy { |record| Client.destroy_all "client_of = #{record.id}" } + # # Disables access to the system, for associated clients and people when the firm is destroyed + # before_destroy { |record| Person.where(firm_id: record.id).update_all(access: 'disabled') } + # before_destroy { |record| Client.where(client_of: record.id).update_all(access: 'disabled') } # end # # == Inheritable callback queues diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb index 75d3bfe625..cb185a881e 100644 --- a/activerecord/lib/active_record/coders/json.rb +++ b/activerecord/lib/active_record/coders/json.rb @@ -6,7 +6,7 @@ module ActiveRecord end def self.load(json) - ActiveSupport::JSON.decode(json) unless json.nil? + ActiveSupport::JSON.decode(json) unless json.blank? end end end diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 5dcc98424a..8d41c0d799 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -16,7 +16,7 @@ module ActiveRecord query = collection .unscope(:select) - .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp") + .select("COUNT(*) AS #{connection.quote_column_name("size")}", "MAX(#{column}) AS timestamp") .unscope(:order) result = connection.select_one(query) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index e389d818fd..f3abd01290 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -298,7 +298,7 @@ module ActiveRecord def run return unless frequency Thread.new(frequency, pool) { |t, p| - while true + loop do sleep t p.reap end @@ -618,7 +618,7 @@ module ActiveRecord timeout_time = Time.now + (@checkout_timeout * 2) @available.with_a_bias_for(Thread.current) do - while true + loop do synchronize do return if collected_conns.size == @connections.size && @now_connecting == 0 remaining_timeout = timeout_time - Time.now @@ -778,8 +778,7 @@ module ActiveRecord end # ConnectionHandler is a collection of ConnectionPool objects. It is used - # for keeping separate connection pools for Active Record models that connect - # to different databases. + # for keeping separate connection pools that connect to different databases. # # For example, suppose that you have 5 models, with the following hierarchy: # @@ -821,17 +820,16 @@ module ActiveRecord # ConnectionHandler accessible via ActiveRecord::Base.connection_handler. # All Active Record models use this handler to determine the connection pool that they # should use. + # + # The ConnectionHandler class is not coupled with the Active models, as it has no knowlodge + # about the model. The model, needs to pass a specification name to the handler, + # in order to lookup the correct connection pool. class ConnectionHandler def initialize - # These caches are keyed by klass.name, NOT klass. Keying them by klass - # alone would lead to memory leaks in development mode as all previous - # instances of the class would stay in memory. + # These caches are keyed by spec.name (ConnectionSpecification#name). @owner_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k| h[k] = Concurrent::Map.new(:initial_capacity => 2) end - @class_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k| - h[k] = Concurrent::Map.new - end end def connection_pool_list @@ -839,10 +837,8 @@ module ActiveRecord end alias :connection_pools :connection_pool_list - def establish_connection(owner, spec) - @class_to_pool.clear - raise RuntimeError, "Anonymous class is not allowed." unless owner.name - owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec) + def establish_connection(spec) + owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec) end # Returns true if there are any active connections among the connection @@ -873,18 +869,18 @@ module ActiveRecord # active or defined connection: if it is the latter, it will be # opened and set as the active connection for the class it was defined # for (not necessarily the current class). - def retrieve_connection(klass) #:nodoc: - pool = retrieve_connection_pool(klass) - raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool + def retrieve_connection(spec_name) #:nodoc: + pool = retrieve_connection_pool(spec_name) + raise ConnectionNotEstablished, "No connection pool with id #{spec_name} found." unless pool conn = pool.connection - raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn + raise ConnectionNotEstablished, "No connection for #{spec_name} in connection pool" unless conn conn end # Returns true if a connection that's accessible to this class has # already been opened. - def connected?(klass) - conn = retrieve_connection_pool(klass) + def connected?(spec_name) + conn = retrieve_connection_pool(spec_name) conn && conn.connected? end @@ -892,9 +888,8 @@ module ActiveRecord # connection and the defined connection (if they exist). The result # can be used as an argument for establish_connection, for easily # re-establishing the connection. - def remove_connection(owner) - if pool = owner_to_pool.delete(owner.name) - @class_to_pool.clear + def remove_connection(spec_name) + if pool = owner_to_pool.delete(spec_name) pool.automatic_reconnect = false pool.disconnect! pool.spec.config @@ -910,14 +905,20 @@ module ActiveRecord # #fetch is significantly slower than #[]. So in the nil case, no caching will # take place, but that's ok since the nil case is not the common one that we wish # to optimise for. - def retrieve_connection_pool(klass) - class_to_pool[klass.name] ||= begin - until pool = pool_for(klass) - klass = klass.superclass - break unless klass <= Base + def retrieve_connection_pool(spec_name) + owner_to_pool.fetch(spec_name) do + # Check if a connection was previously established in an ancestor process, + # which may have been forked. + if ancestor_pool = pool_from_any_process_for(spec_name) + # A connection was established in an ancestor process that must have + # subsequently forked. We can't reuse the connection, but we can copy + # the specification and establish a new connection with it. + establish_connection(ancestor_pool.spec).tap do |pool| + pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache + end + else + owner_to_pool[spec_name] = nil end - - class_to_pool[klass.name] = pool end end @@ -927,28 +928,9 @@ module ActiveRecord @owner_to_pool[Process.pid] end - def class_to_pool - @class_to_pool[Process.pid] - end - - def pool_for(owner) - owner_to_pool.fetch(owner.name) { - if ancestor_pool = pool_from_any_process_for(owner) - # A connection was established in an ancestor process that must have - # subsequently forked. We can't reuse the connection, but we can copy - # the specification and establish a new connection with it. - establish_connection(owner, ancestor_pool.spec).tap do |pool| - pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache - end - else - owner_to_pool[owner.name] = nil - end - } - end - - def pool_from_any_process_for(owner) - owner_to_pool = @owner_to_pool.values.find { |v| v[owner.name] } - owner_to_pool && owner_to_pool[owner.name] + def pool_from_any_process_for(spec_name) + owner_to_pool = @owner_to_pool.values.find { |v| v[spec_name] } + owner_to_pool && owner_to_pool[spec_name] end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 824040775d..507a925d32 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -66,8 +66,8 @@ module ActiveRecord # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. def select_rows(sql, name = nil, binds = []) + exec_query(sql, name, binds).rows end - undef_method :select_rows # Executes the SQL statement in the context of this connection and returns # the raw result from the connection adapter. @@ -75,13 +75,14 @@ module ActiveRecord # method may be manually memory managed. Consider using the exec_query # wrapper instead. def execute(sql, name = nil) + raise NotImplementedError end - undef_method :execute # Executes +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_query(sql, name = 'SQL', binds = [], prepare: false) + raise NotImplementedError end # Executes insert +sql+ statement in the context of this connection using @@ -220,9 +221,7 @@ module ActiveRecord # * You are creating a nested (savepoint) transaction # # The mysql2 and postgresql adapters support setting the transaction - # isolation level. However, support is disabled for MySQL versions below 5, - # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] - # which means the isolation level gets persisted outside the transaction. + # isolation level. def transaction(requires_new: nil, isolation: nil, joinable: true) if !requires_new && current_transaction.joinable? if isolation @@ -292,9 +291,6 @@ module ActiveRecord exec_rollback_to_savepoint(name) end - def exec_rollback_to_savepoint(name = nil) #:nodoc: - end - def default_sequence_name(table, column) nil end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 2eeefb13d7..860ef17dca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -146,6 +146,10 @@ module ActiveRecord end end + def quoted_time(value) # :nodoc: + quoted_date(value).sub(/\A2000-01-01 /, '') + end + def prepare_binds_for_database(binds) # :nodoc: binds.map(&:value_for_database) end @@ -166,6 +170,7 @@ module ActiveRecord # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s('F') when Numeric, ActiveSupport::Duration then value.to_s + when Type::Time::Value then "'#{quoted_time(value)}'" when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value}'" @@ -181,6 +186,7 @@ module ActiveRecord when false then unquoted_false # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s('F') + when Type::Time::Value then quoted_time(value) when Date, Time then quoted_date(value) when *types_which_need_no_typecasting value diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb index c0662f8473..3a06f75292 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -1,8 +1,8 @@ module ActiveRecord module ConnectionAdapters - module Savepoints #:nodoc: - def supports_savepoints? - true + module Savepoints + def current_savepoint_name + current_transaction.savepoint_name end def create_savepoint(name = current_savepoint_name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 0ba4d94e3c..6add697eeb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -53,8 +53,8 @@ module ActiveRecord statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) end - create_sql << "(#{statements.join(', ')}) " if statements.present? - create_sql << "#{o.options}" + create_sql << "(#{statements.join(', ')})" if statements.present? + add_table_options!(create_sql, table_options(o)) create_sql << " AS #{@conn.to_sql(o.as)}" if o.as create_sql end @@ -82,6 +82,19 @@ module ActiveRecord "DROP CONSTRAINT #{quote_column_name(name)}" end + def table_options(o) + table_options = {} + table_options[:comment] = o.comment + table_options[:options] = o.options + table_options + end + + def add_table_options!(create_sql, options) + if options_sql = options[:options] + create_sql << " #{options_sql}" + end + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -92,6 +105,7 @@ module ActiveRecord column_options[:auto_increment] = o.auto_increment column_options[:primary_key] = o.primary_key column_options[:collation] = o.collation + column_options[:comment] = o.comment column_options end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 4f97c7c065..bbb0e9249d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -3,14 +3,14 @@ module ActiveRecord # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc: + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc: end # Abstract representation of a column definition. Instances of this type # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key @@ -207,9 +207,9 @@ module ActiveRecord include ColumnMethods attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys + attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment - def initialize(name, temporary, options, as = nil) + def initialize(name, temporary = false, options = nil, as = nil, comment: nil) @columns_hash = {} @indexes = {} @foreign_keys = [] @@ -218,6 +218,7 @@ module ActiveRecord @options = options @as = as @name = name + @comment = comment end def primary_keys(name = nil) # :nodoc: @@ -330,6 +331,9 @@ module ActiveRecord end def foreign_key(table_name, options = {}) # :nodoc: + table_name_prefix = ActiveRecord::Base.table_name_prefix + table_name_suffix = ActiveRecord::Base.table_name_suffix + table_name = "#{table_name_prefix}#{table_name}#{table_name_suffix}" foreign_keys.push([table_name, options]) end @@ -373,6 +377,7 @@ module ActiveRecord column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] column.collation = options[:collation] + column.comment = options[:comment] column end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 4880d216d6..677a4c6bd0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -16,7 +16,7 @@ module ActiveRecord def column_spec_for_primary_key(column) return {} if default_primary_key?(column) spec = { id: schema_type(column).inspect } - spec.merge!(prepare_column_options(column)) + spec.merge!(prepare_column_options(column).except!(:null)) end # This can be overridden on an Adapter level basis to support other @@ -46,12 +46,14 @@ module ActiveRecord spec[:collation] = collation end + spec[:comment] = column.comment.inspect if column.comment.present? + spec end # Lists the valid migration options def migration_keys - [:name, :limit, :precision, :scale, :default, :null, :collation] + [:name, :limit, :precision, :scale, :default, :null, :collation, :comment] end private diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 020d9bbdca..99a3e99bdc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -18,6 +18,11 @@ module ActiveRecord nil end + # Returns the table comment that's stored in database metadata. + def table_comment(table_name) + nil + end + # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) table_name[0...table_alias_length].tr('.', '_') @@ -254,8 +259,8 @@ module ActiveRecord # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id # # See also TableDefinition#column for details on how to create columns. - def create_table(table_name, options = {}) - td = create_table_definition table_name, options[:temporary], options[:options], options[:as] + def create_table(table_name, comment: nil, **options) + td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment if options[:id] != false && !options[:as] pk = options.fetch(:primary_key) do @@ -283,6 +288,14 @@ module ActiveRecord end end + if supports_comments? && !supports_comments_in_create? + change_table_comment(table_name, comment) if comment + + td.columns.each do |column| + change_column_comment(table_name, column.name, column.comment) if column.comment + end + end + result end @@ -329,12 +342,13 @@ module ActiveRecord column_options = options.delete(:column_options) || {} column_options.reverse_merge!(null: false) + type = column_options.delete(:type) || :integer t1_column, t2_column = [table_1, table_2].map{ |t| t.to_s.singularize.foreign_key } create_table(join_table_name, options.merge!(id: false)) do |td| - td.integer t1_column, column_options - td.integer t2_column, column_options + td.send type, t1_column, column_options + td.send type, t2_column, column_options yield td if block_given? end end @@ -776,7 +790,8 @@ module ActiveRecord # [<tt>:type</tt>] # The reference column type. Defaults to +:integer+. # [<tt>:index</tt>] - # Add an appropriate index. Defaults to false. + # Add an appropriate index. Defaults to false. + # See #add_index for usage of this option. # [<tt>:foreign_key</tt>] # Add an appropriate foreign key constraint. Defaults to false. # [<tt>:polymorphic</tt>] @@ -796,6 +811,14 @@ module ActiveRecord # # add_reference(:products, :supplier, polymorphic: true, index: true) # + # ====== Create a supplier_id column with a unique index + # + # add_reference(:products, :supplier, index: { unique: true }) + # + # ====== Create a supplier_id column with a named index + # + # add_reference(:products, :supplier, index: { name: "my_supplier_index" }) + # # ====== Create a supplier_id column and appropriate foreign key # # add_reference(:products, :supplier, foreign_key: true) @@ -962,11 +985,23 @@ module ActiveRecord end def dump_schema_information #:nodoc: + versions = ActiveRecord::SchemaMigration.order('version').pluck(:version) + insert_versions_sql(versions) + end + + def insert_versions_sql(versions) # :nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name - sql = "INSERT INTO #{sm_table} (version) VALUES " - sql << ActiveRecord::SchemaMigration.order('version').pluck(:version).map {|v| "('#{v}')" }.join(', ') - sql << ";\n\n" + if supports_multi_insert? + sql = "INSERT INTO #{sm_table} (version) VALUES " + sql << versions.map {|v| "('#{v}')" }.join(', ') + sql << ";\n\n" + sql + else + versions.map { |version| + "INSERT INTO #{sm_table} (version) VALUES ('#{version}');" + }.join "\n\n" + end end # Should not be called normally, but this operation is non-destructive. @@ -1003,7 +1038,7 @@ module ActiveRecord if (duplicate = inserting.detect {|v| inserting.count(v) > 1}) raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." end - execute "INSERT INTO #{sm_table} (version) VALUES #{inserting.map {|v| "('#{v}')"}.join(', ') }" + execute insert_versions_sql(inserting) end end @@ -1075,15 +1110,19 @@ module ActiveRecord Table.new(table_name, base) end - def add_index_options(table_name, column_name, options = {}) #:nodoc: - column_names = Array(column_name) + def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: + if column_name.is_a?(String) && /\W/ === column_name + column_names = column_name + else + column_names = Array(column_name) + end options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) index_type = options[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) - index_name ||= index_name(table_name, column: column_names) + index_name ||= index_name(table_name, index_name_options(column_names)) max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length if options.key?(:algorithm) @@ -1106,13 +1145,23 @@ module ActiveRecord end index_columns = quoted_columns_for_index(column_names, options).join(", ") - [index_name, index_type, index_columns, index_options, algorithm, using] + [index_name, index_type, index_columns, index_options, algorithm, using, comment] end def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + # Changes the comment for a table or removes it if +nil+. + def change_table_comment(table_name, comment) + raise NotImplementedError, "#{self.class} does not support changing table comments" + end + + # Changes the comment for a column or removes it if +nil+. + def change_column_comment(table_name, column_name, comment) #:nodoc: + raise NotImplementedError, "#{self.class} does not support changing column comments" + end + protected def add_index_sort_order(option_strings, column_names, options = {}) if options.is_a?(Hash) && order = options[:order] @@ -1129,6 +1178,8 @@ module ActiveRecord # Overridden by the MySQL adapter for supporting index lengths def quoted_columns_for_index(column_names, options = {}) + return [column_names] if column_names.is_a?(String) + option_strings = Hash[column_names.map {|name| [name, '']}] # add index sort order if supported @@ -1140,6 +1191,8 @@ module ActiveRecord end def index_name_for_remove(table_name, options = {}) + return options[:name] if can_remove_index_by_name?(options) + # if the adapter doesn't support the indexes call the best we can do # is return the default index name for the options provided return index_name(table_name, options) unless respond_to?(:indexes) @@ -1147,7 +1200,7 @@ module ActiveRecord checks = [] if options.is_a?(Hash) - checks << lambda { |i| i.name == options[:name].to_s } if options.has_key?(:name) + checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name) column_names = Array(options[:column]).map(&:to_s) else column_names = Array(options).map(&:to_s) @@ -1194,14 +1247,22 @@ module ActiveRecord end private - def create_table_definition(name, temporary = false, options = nil, as = nil) - TableDefinition.new(name, temporary, options, as) + def create_table_definition(*args) + TableDefinition.new(*args) end def create_alter_table(name) AlterTable.new create_table_definition(name) end + def index_name_options(column_names) # :nodoc: + if column_names.is_a?(String) + column_names = column_names.scan(/\w+/).join('_') + end + + { column: column_names } + end + def foreign_key_name(table_name, options) # :nodoc: identifier = "#{table_name}_#{options.fetch(:column)}_fk" hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) @@ -1223,6 +1284,10 @@ module ActiveRecord default_or_changes end end + + def can_remove_index_by_name?(options) + options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 069346253a..d4b9e301bc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -67,6 +67,7 @@ module ActiveRecord include QueryCache include ActiveSupport::Callbacks include ColumnDumper + include Savepoints SIMPLE_INT = /\A\d+\z/ @@ -104,9 +105,15 @@ module ActiveRecord @config = config @pool = nil @schema_cache = SchemaCache.new self - @visitor = nil - @prepared_statements = false @quoted_column_names, @quoted_table_names = {}, {} + @visitor = arel_visitor + + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true + @visitor.extend(DetermineIfPreparableVisitor) + else + @prepared_statements = false + end end class Version @@ -142,8 +149,12 @@ module ActiveRecord end end + def arel_visitor # :nodoc: + Arel::Visitors::ToSql.new(self) + end + def valid_type?(type) - true + false end def schema_creation @@ -237,6 +248,11 @@ module ActiveRecord false end + # Does this adapter support expression indices? + def supports_expression_index? + false + end + # Does this adapter support explain? def supports_explain? false @@ -278,6 +294,21 @@ module ActiveRecord false end + # Does this adapter support metadata comments on database objects (tables, columns, indexes)? + def supports_comments? + false + end + + # Can comments for tables, columns, and indexes be specified in create/alter table statements? + def supports_comments_in_create? + false + end + + # Does this adapter support multi-value insert? + def supports_multi_insert? + true + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -379,12 +410,6 @@ module ActiveRecord @connection end - def create_savepoint(name = nil) - end - - def release_savepoint(name = nil) - end - def case_sensitive_comparison(table, attribute, column, value) if value.nil? table[attribute].eq(value) @@ -406,10 +431,6 @@ module ActiveRecord end private :can_perform_case_insensitive_comparison_for? - def current_savepoint_name - current_transaction.savepoint_name - end - # Check the connection back in to the connection pool def close pool.checkin self @@ -433,6 +454,24 @@ module ActiveRecord visitor.accept(node, collector).value end + def combine_bind_parameters( + from_clause: [], + join_clause: [], + where_clause: [], + having_clause: [], + limit: nil, + offset: nil + ) # :nodoc: + result = from_clause + join_clause + where_clause + having_clause + if limit + result << limit + end + if offset + result << offset + end + result + end + protected def initialize_type_map(m) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 1797af4e97..fdd6bffa13 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,4 +1,5 @@ require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/statement_pool' require 'active_record/connection_adapters/mysql/column' require 'active_record/connection_adapters/mysql/explain_pretty_printer' require 'active_record/connection_adapters/mysql/quoting' @@ -14,16 +15,19 @@ module ActiveRecord class AbstractMysqlAdapter < AbstractAdapter include MySQL::Quoting include MySQL::ColumnDumper - include Savepoints def update_table_definition(table_name, base) # :nodoc: MySQL::Table.new(table_name, base) end - def schema_creation + def schema_creation # :nodoc: MySQL::SchemaCreation.new(self) end + def arel_visitor # :nodoc: + Arel::Visitors::MySQL.new(self) + end + ## # :singleton-method: # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> @@ -52,17 +56,16 @@ module ActiveRecord INDEX_TYPES = [:fulltext, :spatial] INDEX_USINGS = [:btree, :hash] + class StatementPool < ConnectionAdapters::StatementPool + private def dealloc(stmt) + stmt[:stmt].close + end + end + def initialize(connection, logger, connection_options, config) super(connection, logger, config) - @visitor = Arel::Visitors::MySQL.new self - - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - @visitor.extend(DetermineIfPreparableVisitor) - else - @prepared_statements = false - end + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) if version < '5.0.0' raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0." @@ -77,10 +80,14 @@ module ActiveRecord } end - def version + def version #:nodoc: @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) end + def mariadb? # :nodoc: + full_version =~ /mariadb/i + end + # Returns true, since this connection adapter supports migrations. def supports_migrations? true @@ -94,6 +101,12 @@ module ActiveRecord true end + # Returns true, since this connection adapter supports prepared statement + # caching. + def supports_statement_cache? + true + end + # Technically MySQL allows to create indexes with the sort order syntax # but at the moment (5.5) it doesn't yet implement them def supports_index_sort_order? @@ -133,11 +146,11 @@ module ActiveRecord end def get_advisory_lock(lock_name, timeout = 0) # :nodoc: - select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1' + select_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1 end def release_advisory_lock(lock_name) # :nodoc: - select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1' + select_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1 end def native_database_types @@ -156,8 +169,8 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc: - MySQL::Column.new(field, default, sql_type_metadata, null, table_name, default_function, collation) + def new_column(*args) #:nodoc: + MySQL::Column.new(*args) end # Must return the MySQL error number from the exception, if the exception has an @@ -179,6 +192,14 @@ module ActiveRecord end end + # CONNECTION MANAGEMENT ==================================== + + # Clears the prepared statements cache. + def clear_cache! + reload_type_map + @statements.clear + end + #-- # DATABASE STATEMENTS ====================================== #++ @@ -192,11 +213,6 @@ module ActiveRecord MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) end - def clear_cache! - super - reload_type_map - end - # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) log(sql, name) { @connection.query(sql) } @@ -331,8 +347,7 @@ module ActiveRecord def data_source_exists?(table_name) return false unless table_name.present? - schema, name = table_name.to_s.split('.', 2) - schema, name = @config[:database], schema unless name # A table was provided without a schema + schema, name = extract_schema_qualified_name(table_name) sql = "SELECT table_name FROM information_schema.tables " sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" @@ -347,8 +362,7 @@ module ActiveRecord def view_exists?(view_name) # :nodoc: return false unless view_name.present? - schema, name = view_name.to_s.split('.', 2) - schema, name = @config[:database], schema unless name # A view was provided without a schema + schema, name = extract_schema_qualified_name(view_name) sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" @@ -369,7 +383,7 @@ module ActiveRecord mysql_index_type = row[:Index_type].downcase.to_sym index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using) + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using, row[:Index_comment].presence) end indexes.last.columns << row[:Column_name] @@ -390,12 +404,20 @@ module ActiveRecord else default, default_function = field[:Default], nil end - new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation]) + new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) end end - def create_table(table_name, options = {}) #:nodoc: - super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) + def table_comment(table_name) # :nodoc: + select_value(<<-SQL.strip_heredoc, 'SCHEMA') + SELECT table_comment + FROM information_schema.tables + WHERE table_name=#{quote(table_name)} + SQL + end + + def create_table(table_name, **options) #:nodoc: + super(table_name, options: 'ENGINE=InnoDB', **options) end def bulk_change_table(table_name, operations) #:nodoc: @@ -478,11 +500,21 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" + index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" + execute add_sql_comment!(sql, comment) + end + + def add_sql_comment!(sql, comment) # :nodoc: + sql << " COMMENT #{quote(comment)}" if comment + sql end def foreign_keys(table_name) + raise ArgumentError unless table_name.present? + + schema, name = extract_schema_qualified_name(table_name) + fk_info = select_all <<-SQL.strip_heredoc SELECT fk.referenced_table_name as 'to_table' ,fk.referenced_column_name as 'primary_key' @@ -490,8 +522,8 @@ module ActiveRecord ,fk.constraint_name as 'name' FROM information_schema.key_column_usage fk WHERE fk.referenced_column_name is not null - AND fk.table_schema = '#{@config[:database]}' - AND fk.table_name = '#{table_name}' + AND fk.table_schema = #{quote(schema)} + AND fk.table_name = #{quote(name)} SQL create_table_info = create_table_info(table_name) @@ -517,7 +549,12 @@ module ActiveRecord raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip # strip AUTO_INCREMENT - raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') + raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') + + # strip COMMENT + raw_table_options.sub!(/ COMMENT='.+'/, '') + + raw_table_options end # Maps logical Rails types to MySQL-specific data types. @@ -553,8 +590,7 @@ module ActiveRecord def primary_keys(table_name) # :nodoc: raise ArgumentError unless table_name.present? - schema, name = table_name.to_s.split('.', 2) - schema, name = @config[:database], schema unless name # A table was provided without a schema + schema, name = extract_schema_qualified_name(table_name) select_values(<<-SQL.strip_heredoc, 'SCHEMA') SELECT column_name @@ -697,6 +733,8 @@ module ActiveRecord RecordNotUnique.new(message) when 1452 InvalidForeignKey.new(message) + when 1406 + ValueTooLong.new(message) else super end @@ -782,10 +820,6 @@ module ActiveRecord subselect.from subsubselect.as('__active_record_temp') end - def mariadb? - full_version =~ /mariadb/i - end - def supports_rename_index? mariadb? ? false : version >= '5.7.6' end @@ -866,8 +900,14 @@ module ActiveRecord create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] end - def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - MySQL::TableDefinition.new(name, temporary, options, as) + def create_table_definition(*args) # :nodoc: + MySQL::TableDefinition.new(*args) + end + + def extract_schema_qualified_name(string) # :nodoc: + schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) + schema, name = @config[:database], schema unless name + [schema, name] end def integer_to_sql(limit) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 2e718b29fa..28f0c8686a 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,11 +1,9 @@ -require 'set' - module ActiveRecord # :stopdoc: module ConnectionAdapters # An abstract definition of a column in a table. class Column - attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation + attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true @@ -15,7 +13,7 @@ module ActiveRecord # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil) + def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil) @name = name.freeze @table_name = table_name @sql_type_metadata = sql_type_metadata @@ -23,6 +21,7 @@ module ActiveRecord @default = default @default_function = default_function @collation = collation + @comment = comment end def has_default? diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 4bc6447368..901c98b22b 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -3,10 +3,10 @@ require 'uri' module ActiveRecord module ConnectionAdapters class ConnectionSpecification #:nodoc: - attr_reader :config, :adapter_method + attr_reader :name, :config, :adapter_method - def initialize(config, adapter_method) - @config, @adapter_method = config, adapter_method + def initialize(name, config, adapter_method) + @name, @config, @adapter_method = name, config, adapter_method end def initialize_dup(original) @@ -164,7 +164,7 @@ module ActiveRecord # spec.config # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # - def spec(config) + def spec(config, name = nil) spec = resolve(config).symbolize_keys raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) @@ -179,7 +179,14 @@ module ActiveRecord end adapter_method = "#{spec[:adapter]}_connection" - ConnectionSpecification.new(spec, adapter_method) + + name ||= + if config.is_a?(Symbol) + config.to_s + else + "primary" + end + ConnectionSpecification.new(name, spec, adapter_method) end private diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb new file mode 100644 index 0000000000..13c9b6cbd9 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -0,0 +1,125 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module DatabaseStatements + # Returns an ActiveRecord::Result instance. + def select_all(arel, name = nil, binds = [], preparable: nil) + result = if ExplainRegistry.collect? && prepared_statements + unprepared_statement { super } + else + super + end + @connection.next_result while @connection.more_results? + result + end + + # Returns a record hash with the column names as keys and column values + # as values. + def select_one(arel, name = nil, binds = []) + arel, binds = binds_from_relation(arel, binds) + @connection.query_options.merge!(as: :hash) + select_result(to_sql(arel, binds), name, binds) do |result| + @connection.next_result while @connection.more_results? + result.first + end + ensure + @connection.query_options.merge!(as: :array) + end + + # Returns an array of arrays containing the field values. + # Order is the same as that returned by +columns+. + def select_rows(sql, name = nil, binds = []) + select_result(sql, name, binds) do |result| + @connection.next_result while @connection.more_results? + result.to_a + end + end + + # Executes the SQL statement in the context of this connection. + def execute(sql, name = nil) + if @connection + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + end + + super + end + + def exec_query(sql, name = 'SQL', binds = [], prepare: false) + if without_prepared_statement?(binds) + execute_and_free(sql, name) do |result| + ActiveRecord::Result.new(result.fields, result.to_a) if result + end + else + exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result| + ActiveRecord::Result.new(result.fields, result.to_a) if result + end + end + end + + def exec_delete(sql, name, binds) + if without_prepared_statement?(binds) + execute_and_free(sql, name) { @connection.affected_rows } + else + exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows } + end + end + alias :exec_update :exec_delete + + protected + + def last_inserted_id(result) + @connection.last_id + end + + private + + def select_result(sql, name = nil, binds = []) + if without_prepared_statement?(binds) + execute_and_free(sql, name) { |result| yield result } + else + exec_stmt_and_free(sql, name, binds, cache_stmt: true) { |_, result| yield result } + end + end + + def exec_stmt_and_free(sql, name, binds, cache_stmt: false) + if @connection + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + end + + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } + + log(sql, name, binds) do + if cache_stmt + cache = @statements[sql] ||= { + stmt: @connection.prepare(sql) + } + stmt = cache[:stmt] + else + stmt = @connection.prepare(sql) + end + + begin + result = stmt.execute(*type_casted_binds) + rescue Mysql2::Error => e + if cache_stmt + @statements.delete(sql) + else + stmt.close + end + raise e + end + + ret = yield stmt, result + result.free if result + stmt.close unless cache_stmt + ret + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index 1e2c859af9..fd2dc2aee8 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -2,6 +2,9 @@ module ActiveRecord module ConnectionAdapters module MySQL class SchemaCreation < AbstractAdapter::SchemaCreation + delegate :add_sql_comment!, to: :@conn + private :add_sql_comment! + private def visit_DropForeignKey(name) @@ -22,6 +25,10 @@ module ActiveRecord add_column_position!(change_column_sql, column_options(o.column)) end + def add_table_options!(create_sql, options) + add_sql_comment!(super, options[:comment]) + end + def column_options(o) column_options = super column_options[:charset] = o.charset @@ -29,13 +36,15 @@ module ActiveRecord end def add_column_options!(sql, options) - if options[:charset] - sql << " CHARACTER SET #{options[:charset]}" + if charset = options[:charset] + sql << " CHARACTER SET #{charset}" end - if options[:collation] - sql << " COLLATE #{options[:collation]}" + + if collation = options[:collation] + sql << " COLLATE #{collation}" end - super + + add_sql_comment!(super, options[:comment]) end def add_column_position!(sql, options) @@ -44,12 +53,13 @@ module ActiveRecord elsif options[:after] sql << " AFTER #{quote_column_name(options[:after])}" end + sql end def index_in_create(table_name, column_name, options) - index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options) - "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) " + index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options) + add_sql_comment!("#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})", comment) end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index be40df4101..2ba9657f24 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -7,7 +7,7 @@ module ActiveRecord spec = { id: :bigint.inspect } spec[:default] = schema_default(column) || 'nil' unless column.auto_increment? else - spec = super.except!(:null) + spec = super end spec[:unsigned] = 'true' if column.unsigned? spec diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index e7541748de..22d35f1db5 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,7 +1,9 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' +require 'active_record/connection_adapters/mysql/database_statements' gem 'mysql2', '>= 0.3.18', '< 0.5' require 'mysql2' +raise 'mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+' if Mysql2::VERSION == '0.4.3' module ActiveRecord module ConnectionHandling # :nodoc: @@ -35,9 +37,11 @@ module ActiveRecord class Mysql2Adapter < AbstractMysqlAdapter ADAPTER_NAME = 'Mysql2'.freeze + include MySQL::DatabaseStatements + def initialize(connection, logger, connection_options, config) super - @prepared_statements = false + @prepared_statements = false unless config.key?(:prepared_statements) configure_connection end @@ -45,6 +49,18 @@ module ActiveRecord !mariadb? && version >= '5.7.8' end + def supports_comments? + true + end + + def supports_comments_in_create? + true + end + + def supports_savepoints? + true + end + # HELPER METHODS =========================================== def each_hash(result) # :nodoc: @@ -95,55 +111,6 @@ module ActiveRecord end end - #-- - # DATABASE STATEMENTS ====================================== - #++ - - # Returns a record hash with the column names as keys and column values - # as values. - def select_one(arel, name = nil, binds = []) - arel, binds = binds_from_relation(arel, binds) - execute(to_sql(arel, binds), name).each(as: :hash) do |row| - @connection.next_result while @connection.more_results? - return row - end - end - - # Returns an array of arrays containing the field values. - # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil, binds = []) - result = execute(sql, name) - @connection.next_result while @connection.more_results? - result.to_a - end - - # Executes the SQL statement in the context of this connection. - def execute(sql, name = nil) - if @connection - # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been - # made since we established the connection - @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone - end - - super - end - - def exec_query(sql, name = 'SQL', binds = [], prepare: false) - result = execute(sql, name) - @connection.next_result while @connection.more_results? - ActiveRecord::Result.new(result.fields, result.to_a) if result - end - - def exec_delete(sql, name, binds) - execute to_sql(sql, binds), name - @connection.affected_rows - end - alias :exec_update :exec_delete - - def last_inserted_id(result) - @connection.last_id - end - private def connect diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index eeccb09bdf..838cb63281 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -1,3 +1,5 @@ +require 'ipaddr' + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb index 7427a25ad5..4da240edb2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb @@ -14,6 +14,8 @@ module ActiveRecord def cast(value) case value when ::String + return if value.blank? + if value[0] == '(' && value[-1] == ')' value = value[1...-1] end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb index 1047ba8cac..a1e10fd364 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module ColumnDumper def column_spec_for_primary_key(column) - spec = super.except!(:null) + spec = super if schema_type(column) == :uuid spec[:default] ||= 'nil' end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 39e8492688..6318b1c65a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/strip' + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -172,7 +174,11 @@ module ActiveRecord table = Utils.extract_schema_qualified_name(table_name.to_s) result = query(<<-SQL, 'SCHEMA') - SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, + pg_catalog.obj_description(i.oid, 'pg_class') AS comment, + (SELECT COUNT(*) FROM pg_opclass o + JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c + ON o.oid = c.oid WHERE o.opcdefault = 'f') FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid @@ -190,43 +196,61 @@ module ActiveRecord indkey = row[2].split(" ").map(&:to_i) inddef = row[3] oid = row[4] + comment = row[5] + opclass = row[6] - columns = Hash[query(<<-SQL, "SCHEMA")] - SELECT a.attnum, a.attname - FROM pg_attribute a - WHERE a.attrelid = #{oid} - AND a.attnum IN (#{indkey.join(",")}) - SQL + using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten - column_names = columns.values_at(*indkey).compact + if indkey.include?(0) || opclass > 0 + columns = expressions + else + columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact + SELECT a.attnum, a.attname + FROM pg_attribute a + WHERE a.attrelid = #{oid} + AND a.attnum IN (#{indkey.join(",")}) + SQL - unless column_names.empty? # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - desc_order_columns = inddef.scan(/(\w+) DESC/).flatten - orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - where = inddef.scan(/WHERE (.+)$/).flatten[0] - using = inddef.scan(/USING (.+?) /).flatten[0].to_sym - - IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using) + orders = Hash[ + expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] } + ] end + + IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, using.to_sym, comment.presence) end.compact end # Returns the list of all column definitions for a table. def columns(table_name) # :nodoc: table_name = table_name.to_s - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation| + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation, comment| oid = oid.to_i fmod = fmod.to_i type_metadata = fetch_type_metadata(column_name, type, oid, fmod) default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, !notnull, table_name, default_function, collation) + new_column(column_name, default_value, type_metadata, !notnull, table_name, default_function, collation, comment: comment.presence) end end - def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc: - PostgreSQLColumn.new(name, default, sql_type_metadata, null, table_name, default_function, collation) + def new_column(*args) # :nodoc: + PostgreSQLColumn.new(*args) + end + + # Returns a comment stored in database for given table + def table_comment(table_name) # :nodoc: + name = Utils.extract_schema_qualified_name(table_name.to_s) + if name.identifier + select_value(<<-SQL.strip_heredoc, 'SCHEMA') + SELECT pg_catalog.obj_description(c.oid, 'pg_class') + FROM pg_catalog.pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = #{quote(name.identifier)} + AND c.relkind IN ('r') -- (r)elation/table + AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'} + SQL + end end # Returns the current database name. @@ -445,6 +469,7 @@ module ActiveRecord def add_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! super + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) end def change_column(table_name, column_name, type, options = {}) #:nodoc: @@ -466,6 +491,7 @@ module ActiveRecord change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) end # Changes the default value of a table column. @@ -494,6 +520,18 @@ module ActiveRecord execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end + # Adds comment for given table column or drops it if +comment+ is a +nil+ + def change_column_comment(table_name, column_name, comment) # :nodoc: + clear_cache! + execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}" + end + + # Adds comment for given table or drops it if +comment+ is a +nil+ + def change_table_comment(table_name, comment) # :nodoc: + clear_cache! + execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}" + end + # Renames a column in a table. def rename_column(table_name, column_name, new_column_name) #:nodoc: clear_cache! @@ -502,8 +540,10 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}" + index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do + execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment + end end def remove_index(table_name, options = {}) #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6497b1cc31..bab80a8890 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -16,8 +16,6 @@ require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" require "active_record/connection_adapters/statement_pool" -require 'ipaddr' - module ActiveRecord module ConnectionHandling # :nodoc: # Establishes a connection to the database that's used by all Active Record objects @@ -119,12 +117,15 @@ module ActiveRecord include PostgreSQL::SchemaStatements include PostgreSQL::DatabaseStatements include PostgreSQL::ColumnDumper - include Savepoints def schema_creation # :nodoc: PostgreSQL::SchemaCreation.new self end + def arel_visitor # :nodoc: + Arel::Visitors::PostgreSQL.new(self) + end + # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -139,6 +140,10 @@ module ActiveRecord true end + def supports_expression_index? + true + end + def supports_transaction_isolation? true end @@ -159,6 +164,14 @@ module ActiveRecord postgresql_version >= 90200 end + def supports_comments? + true + end + + def supports_savepoints? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -195,14 +208,6 @@ module ActiveRecord def initialize(connection, logger, connection_parameters, config) super(connection, logger, config) - @visitor = Arel::Visitors::PostgreSQL.new self - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - @visitor.extend(DetermineIfPreparableVisitor) - else - @prepared_statements = false - end - @connection_parameters = connection_parameters # @local_tz is initialized as nil to avoid warnings when connect tries to use it @@ -212,7 +217,7 @@ module ActiveRecord connect add_pg_encoders @statements = StatementPool.new @connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) + self.class.type_cast_config_to_integer(config[:statement_limit]) if postgresql_version < 90100 raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." @@ -398,6 +403,7 @@ module ActiveRecord protected # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html + VALUE_LIMIT_VIOLATION = "22001" FOREIGN_KEY_VIOLATION = "23503" UNIQUE_VIOLATION = "23505" @@ -409,6 +415,8 @@ module ActiveRecord RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION InvalidForeignKey.new(message) + when VALUE_LIMIT_VIOLATION + ValueTooLong.new(message) else super end @@ -712,7 +720,7 @@ module ActiveRecord # Returns the list of a table's column names, data types, and default values. # # The underlying query is roughly: - # SELECT column.name, column.type, default.value + # SELECT column.name, column.type, default.value, column.comment # FROM column LEFT JOIN default # ON column.table_id = default.table_id # AND column.num = default.column_num @@ -732,7 +740,8 @@ module ActiveRecord SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, (SELECT c.collname FROM pg_collation c, pg_type t - WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) + WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation), + col_description(a.attrelid, a.attnum) AS comment FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass @@ -746,8 +755,8 @@ module ActiveRecord $1.strip if $1 end - def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - PostgreSQL::TableDefinition.new(name, temporary, options, as) + def create_table_definition(*args) # :nodoc: + PostgreSQL::TableDefinition.new(*args) end def can_perform_case_insensitive_comparison_for?(column) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index d3a91f73c8..d5a181d3e2 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -14,6 +14,10 @@ module ActiveRecord @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") end + def quoted_time(value) + quoted_date(value) + end + private def _quote(value) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb index fe1dcbd710..70c0d28830 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -3,6 +3,13 @@ module ActiveRecord module SQLite3 class SchemaCreation < AbstractAdapter::SchemaCreation private + + def column_options(o) + options = super + options[:null] = false if o.primary_key + options + end + def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 786b0ab2ed..eb2268157b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -52,7 +52,6 @@ module ActiveRecord ADAPTER_NAME = 'SQLite'.freeze include SQLite3::Quoting - include Savepoints NATIVE_DATABASE_TYPES = { primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL', @@ -80,20 +79,15 @@ module ActiveRecord SQLite3::SchemaCreation.new self end + def arel_visitor # :nodoc: + Arel::Visitors::SQLite.new(self) + end + def initialize(connection, logger, connection_options, config) super(connection, logger, config) @active = nil - @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - - @visitor = Arel::Visitors::SQLite.new self - - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - @visitor.extend(DetermineIfPreparableVisitor) - else - @prepared_statements = false - end + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) end def supports_ddl_transactions? @@ -135,6 +129,10 @@ module ActiveRecord true end + def supports_multi_insert? + sqlite_version >= '3.7.11' + end + def active? @active != false end @@ -156,6 +154,10 @@ module ActiveRecord true end + def valid_type?(type) + true + end + # Returns 62. SQLite supports index names up to 64 # characters. The rest is used by rails internally to perform # temporary rename operations @@ -230,10 +232,6 @@ module ActiveRecord log(sql, name) { @connection.execute(sql) } end - def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows - end - def begin_db_transaction #:nodoc: log('begin transaction',nil) { @connection.transaction } end @@ -543,7 +541,7 @@ module ActiveRecord result = exec_query(sql, 'SCHEMA').first if result - # Splitting with left parantheses and picking up last will return all + # Splitting with left parentheses and picking up last will return all # columns separated with comma(,). columns_string = result["sql"].split('(').last diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index 57463dd749..9b0ed3e08b 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -1,11 +1,13 @@ module ActiveRecord module ConnectionAdapters - class StatementPool + class StatementPool # :nodoc: include Enumerable - def initialize(max = 1000) + DEFAULT_STATEMENT_LIMIT = 1000 + + def initialize(statement_limit = nil) @cache = Hash.new { |h,pid| h[pid] = {} } - @max = max + @statement_limit = statement_limit || DEFAULT_STATEMENT_LIMIT end def each(&block) @@ -25,7 +27,7 @@ module ActiveRecord end def []=(sql, stmt) - while @max <= cache.size + while @statement_limit <= cache.size dealloc(cache.shift.last) end cache[sql] = stmt diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index a8b3d03ba5..f932deb18d 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -45,16 +45,20 @@ module ActiveRecord # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. def establish_connection(spec = nil) + raise "Anonymous class is not allowed." unless name + spec ||= DEFAULT_ENV.call.to_sym resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations - spec = resolver.spec(spec) + # TODO: uses name on establish_connection, for backwards compatibility + spec = resolver.spec(spec, self == Base ? "primary" : name) unless respond_to?(spec.adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" end - remove_connection - connection_handler.establish_connection self, spec + remove_connection(spec.name) + self.connection_specification_name = spec.name + connection_handler.establish_connection spec end class MergeAndResolveDefaultUrlConfig # :nodoc: @@ -87,6 +91,16 @@ module ActiveRecord retrieve_connection end + attr_writer :connection_specification_name + + # Return the specification name from the current class or its parent. + def connection_specification_name + if !defined?(@connection_specification_name) || @connection_specification_name.nil? + return self == Base ? "primary" : superclass.connection_specification_name + end + @connection_specification_name + end + def connection_id ActiveRecord::RuntimeRegistry.connection_id ||= Thread.current.object_id end @@ -106,20 +120,28 @@ module ActiveRecord end def connection_pool - connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished + connection_handler.retrieve_connection_pool(connection_specification_name) or raise ConnectionNotEstablished end def retrieve_connection - connection_handler.retrieve_connection(self) + connection_handler.retrieve_connection(connection_specification_name) end # Returns +true+ if Active Record is connected. def connected? - connection_handler.connected?(self) + connection_handler.connected?(connection_specification_name) end - def remove_connection(klass = self) - connection_handler.remove_connection(klass) + def remove_connection(name = nil) + name ||= @connection_specification_name if defined?(@connection_specification_name) + # if removing a connection that have a pool, we reset the + # connection_specification_name so it will use the parent + # pool. + if connection_handler.retrieve_connection_pool(name) + self.connection_specification_name = nil + end + + connection_handler.remove_connection(name) end def clear_cache! # :nodoc: diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 5d74631e32..f936e865e4 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -257,7 +257,7 @@ module ActiveRecord # Returns the Arel engine. def arel_engine # :nodoc: @arel_engine ||= - if Base == self || connection_handler.retrieve_connection_pool(self) + if Base == self || connection_handler.retrieve_connection_pool(connection_specification_name) self else superclass.arel_engine diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 2ec9bf3d67..b8b8684cff 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -125,6 +125,10 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end + # Raised when a record cannot be inserted or updated because a value too long for a column type. + class ValueTooLong < StatementInvalid + end + # Raised when number of bind variables in statement given to +:condition+ key # (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method) # does not match number of expected values supplied. diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 73be4cb271..f33456a744 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -6,9 +6,9 @@ module ActiveRecord module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta3" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 2336d23a1c..67b8efac66 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -150,7 +150,7 @@ module ActiveRecord # The version column used for optimistic locking. Defaults to +lock_version+. def locking_column - reset_locking_column unless defined?(@locking_column) + @locking_column = DEFAULT_LOCKING_COLUMN unless defined?(@locking_column) @locking_column end @@ -184,9 +184,16 @@ module ActiveRecord end end + + # In de/serialize we change `nil` to 0, so that we can allow passing + # `nil` values to `lock_version`, and not result in `ActiveRecord::StaleObjectError` + # during update record. class LockingType < DelegateClass(Type::Value) # :nodoc: def deserialize(value) - # `nil` *should* be changed to 0 + super.to_i + end + + def serialize(value) super.to_i end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 99a79024ad..81fe053fe1 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -166,13 +166,13 @@ module ActiveRecord class EnvironmentMismatchError < ActiveRecordError def initialize(current: nil, stored: nil) msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n" - msg << "You are running in `#{ current }` environment." + msg << "You are running in `#{ current }` environment. " msg << "If you are sure you want to continue, first set the environment using:\n\n" msg << "\tbin/rails db:environment:set" if defined?(Rails.env) - super("#{msg} RAILS_ENV=#{::Rails.env}") + super("#{msg} RAILS_ENV=#{::Rails.env}\n\n") else - super(msg) + super("#{msg}\n\n") end end end @@ -277,7 +277,7 @@ module ActiveRecord # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. # * <tt>change_column_default(table_name, column_name, default)</tt>: Sets a - # default value for +column_name+ definded by +default+ on +table_name+. + # default value for +column_name+ defined by +default+ on +table_name+. # * <tt>change_column_null(table_name, column_name, null, default = nil)</tt>: # Sets or removes a +NOT NULL+ constraint on +column_name+. The +null+ flag # indicates whether the value can be +NULL+. See @@ -524,17 +524,11 @@ module ActiveRecord end def self.[](version) - version = version.to_s - name = "V#{version.tr('.', '_')}" - unless Compatibility.const_defined?(name) - versions = Compatibility.constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete('V').tr('_', '.').inspect } - raise ArgumentError, "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}" - end - Compatibility.const_get(name) + Compatibility.find(version) end def self.current_version - Rails.version.to_f + ActiveRecord::VERSION::STRING.to_f end MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc: diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index a20d7e0820..74833f938f 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -1,7 +1,17 @@ module ActiveRecord class Migration module Compatibility # :nodoc: all - V5_0 = Current + def self.find(version) + version = version.to_s + name = "V#{version.tr('.', '_')}" + unless const_defined?(name) + versions = constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete('V').tr('_', '.').inspect } + raise ArgumentError, "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}" + end + const_get(name) + end + + V5_1 = Current module FourTwoShared module TableDefinition @@ -92,6 +102,9 @@ module ActiveRecord end end + class V5_0 < V5_1 + end + class V4_2 < V5_0 # 4.2 is defined as a module because it needs to be shared with # Legacy. When the time comes, V5_0 should be defined straight diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 52eab952e1..f691a8319d 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -238,7 +238,7 @@ module ActiveRecord end # Returns the next value that will be used as the primary key on - # an insert statment. + # an insert statement. def next_sequence_value connection.next_sequence_value(sequence_name) end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index bbbc824b25..ca12a603da 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -44,8 +44,9 @@ module ActiveRecord executor.register_hook(self) executor.to_complete do - # FIXME: This should be skipped when env['rack.test'] - ActiveRecord::Base.clear_active_connections! + unless ActiveRecord::Base.connection.transaction_open? + ActiveRecord::Base.clear_active_connections! + end end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 4e32d73001..53ddd95bb0 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,6 +1,6 @@ module ActiveRecord module Querying - delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :empty?, :none?, :one?, to: :all + delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb index 13393dc605..333b3a63cf 100644 --- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -42,7 +42,7 @@ module ActiveRecord # Delegates #delete_all, #update_all, #destroy_all methods to each batch. # # People.in_batches.delete_all - # People.in_batches.destroy_all('age < 10') + # People.where('age < 10').in_batches.destroy_all # People.in_batches.update_all('age = age + 1') [:delete_all, :update_all, :destroy_all].each do |method| define_method(method) do |*args, &block| diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 54c9af4898..120f34109e 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -37,7 +37,11 @@ module ActiveRecord # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) - calculate(:count, column_name) + if block_given? + to_a.count { |*block_args| yield(*block_args) } + else + calculate(:count, column_name) + end end # Calculates the average value on a given column. Returns +nil+ if there's diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index f2578f5f96..2484cb3264 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,4 +1,3 @@ -require 'set' require 'active_support/concern' module ActiveRecord @@ -36,7 +35,7 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, + delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, :shuffle, :split, to: :records diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 550416238f..ecce949370 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -84,7 +84,6 @@ module ActiveRecord return ["1=0"] if attributes.empty? attributes.flat_map do |key, value| - key = key.to_s if value.is_a?(Hash) associated_predicate_builder(key).expand_from_hash(value) else @@ -137,11 +136,11 @@ module ActiveRecord end def convert_dot_notation_to_hash(attributes) - dot_notation = attributes.keys.select do |s| - s.respond_to?(:include?) && s.include?(".".freeze) + dot_notation = attributes.select do |k, v| + k.include?(".".freeze) && !v.is_a?(Hash) end - dot_notation.each do |key| + dot_notation.each_key do |key| table_name, column_name = key.split(".".freeze) value = attributes.delete(key) attributes[table_name] ||= {} diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 4533f3263f..6477629560 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -99,22 +99,28 @@ module ActiveRecord end def bound_attributes - result = from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds if limit_value && !string_containing_comma?(limit_value) - result << Attribute.with_cast_value( + limit_bind = Attribute.with_cast_value( "LIMIT".freeze, connection.sanitize_limit(limit_value), Type::Value.new, ) end if offset_value - result << Attribute.with_cast_value( + offset_bind = Attribute.with_cast_value( "OFFSET".freeze, offset_value.to_i, Type::Value.new, ) end - result + connection.combine_bind_parameters( + from_clause: from_clause.binds, + join_clause: arel.bind_values, + where_clause: where_clause.binds, + having_clause: having_clause.binds, + limit: limit_bind, + offset: offset_bind, + ) end FROZEN_EMPTY_HASH = {}.freeze diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 2c2d6cfa47..89396b518c 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -158,8 +158,9 @@ module ActiveRecord end end + ARRAY_WITH_EMPTY_STRING = [''] def non_empty_predicates - predicates - [''] + predicates - ARRAY_WITH_EMPTY_STRING end def wrap_sql_literal(node) diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index c0ccb00b6f..dbf172a577 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -15,6 +15,7 @@ module ActiveRecord when Hash attributes = predicate_builder.resolve_column_aliases(opts) attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) + attributes.stringify_keys! attributes, binds = predicate_builder.create_binds(attributes) diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index affcd9aed1..301718b874 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -138,6 +138,10 @@ HEADER table_options = @connection.table_options(table) tbl.print ", options: #{table_options.inspect}" unless table_options.blank? + if comment = @connection.table_comment(table).presence + tbl.print ", comment: #{comment.inspect}" + end + tbl.puts " do |t|" # then dump all non-primary key columns @@ -175,11 +179,11 @@ HEADER tbl.puts end + indexes_in_create(table, tbl) + tbl.puts " end" tbl.puts - indexes(table, tbl) - tbl.rewind stream.print tbl.read rescue => e @@ -191,26 +195,12 @@ HEADER stream end + # Keep it for indexing materialized views def indexes(table, stream) if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| - statement_parts = [ - "add_index #{remove_prefix_and_suffix(index.table).inspect}", - index.columns.inspect, - "name: #{index.name.inspect}", - ] - statement_parts << 'unique: true' if index.unique - - index_lengths = (index.lengths || []).compact - statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? - - index_orders = index.orders || {} - statement_parts << "order: #{index.orders.inspect}" if index_orders.any? - statement_parts << "where: #{index.where.inspect}" if index.where - statement_parts << "using: #{index.using.inspect}" if index.using - statement_parts << "type: #{index.type.inspect}" if index.type - - " #{statement_parts.join(', ')}" + table_name = remove_prefix_and_suffix(index.table).inspect + " add_index #{([table_name]+index_parts(index)).join(', ')}" end stream.puts add_index_statements.sort.join("\n") @@ -218,6 +208,34 @@ HEADER end end + def indexes_in_create(table, stream) + if (indexes = @connection.indexes(table)).any? + index_statements = indexes.map do |index| + " t.index #{index_parts(index).join(', ')}" + end + stream.puts index_statements.sort.join("\n") + end + end + + def index_parts(index) + index_parts = [ + index.columns.inspect, + "name: #{index.name.inspect}", + ] + index_parts << 'unique: true' if index.unique + + index_lengths = (index.lengths || []).compact + index_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? + + index_orders = index.orders || {} + index_parts << "order: #{index.orders.inspect}" if index_orders.any? + index_parts << "where: #{index.where.inspect}" if index.where + index_parts << "using: #{index.using.inspect}" if index.using + index_parts << "type: #{index.type.inspect}" if index.type + index_parts << "comment: #{index.comment.inspect}" if index.comment + index_parts + end + def foreign_keys(table, stream) if (foreign_keys = @connection.foreign_keys(table)).any? add_foreign_key_statements = foreign_keys.map do |foreign_key| diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb index 8ec4b48d31..d9acb1a1dc 100644 --- a/activerecord/lib/active_record/suppressor.rb +++ b/activerecord/lib/active_record/suppressor.rb @@ -30,10 +30,11 @@ module ActiveRecord module ClassMethods def suppress(&block) + previous_state = SuppressorRegistry.suppressed[name] SuppressorRegistry.suppressed[name] = true yield ensure - SuppressorRegistry.suppressed[name] = false + SuppressorRegistry.suppressed[name] = previous_state end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 8881986f1b..0df46d54df 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -107,8 +107,9 @@ module ActiveRecord def create(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).create + $stdout.puts "Created database '#{configuration['database']}'" rescue DatabaseAlreadyExists - $stderr.puts "#{configuration['database']} already exists" + $stderr.puts "Database '#{configuration['database']}' already exists" rescue Exception => error $stderr.puts error $stderr.puts "Couldn't create database for #{configuration.inspect}" @@ -116,10 +117,10 @@ module ActiveRecord end def create_all - old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base) + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) each_local_configuration { |configuration| create configuration } if old_pool - ActiveRecord::Base.connection_handler.establish_connection(ActiveRecord::Base, old_pool.spec) + ActiveRecord::Base.connection_handler.establish_connection(old_pool.spec) end end @@ -133,11 +134,12 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop + $stdout.puts "Dropped database '#{configuration['database']}'" rescue ActiveRecord::NoDatabaseError $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error $stderr.puts error - $stderr.puts "Couldn't drop #{configuration['database']}" + $stderr.puts "Couldn't drop database '#{configuration['database']}'" raise end diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb index 097d1bd363..513c938088 100644 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -17,11 +17,7 @@ module ActiveRecord end def serialize(value) - if value.is_a?(::Array) || value.is_a?(::Hash) - ::ActiveSupport::JSON.encode(value) - else - value - end + ::ActiveSupport::JSON.encode(value) end def accessor diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 4ff0740cfb..a3a5241780 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -32,7 +32,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, value) return false if value.nil? - raw_new_value = serialize(value) + raw_new_value = encoded(value) raw_old_value.nil? != raw_new_value.nil? || subtype.changed_in_place?(raw_old_value, raw_new_value) end @@ -52,6 +52,12 @@ module ActiveRecord def default_value?(value) value == coder.load(nil) end + + def encoded(value) + unless default_value?(value) + coder.dump(value) + end + end end end end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index 70988d84ff..7da49e43c7 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -2,6 +2,18 @@ module ActiveRecord module Type class Time < ActiveModel::Type::Time include Internal::Timezone + + class Value < DelegateClass(::Time) # :nodoc: + end + + def serialize(value) + case value = super + when ::Time + Value.new(value) + else + value + end + end end end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 4a80cda0b8..1f59276137 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -67,9 +67,6 @@ module ActiveRecord cast_type = klass.type_for_attribute(attribute_name) value = cast_type.serialize(value) value = klass.connection.type_cast(value) - if value.is_a?(String) && column.limit - value = value.to_s[0, column.limit] - end comparison = if !options[:case_sensitive] && !value.nil? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation |