diff options
Diffstat (limited to 'activerecord/lib/active_record')
41 files changed, 346 insertions, 765 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 04ad45f5da..462b3066ab 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1181,7 +1181,8 @@ module ActiveRecord # [collection=objects] # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt> # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is - # direct. + # direct by default. You can specify <tt>dependent: :destroy</tt> or + # <tt>dependent: :nullify</tt> to override this. # [collection_singular_ids] # Returns an array of the associated objects' ids # [collection_singular_ids=ids] diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 24aa47bb51..6c83058202 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -18,7 +18,8 @@ module ActiveRecord through_records = owners.map do |owner| association = owner.association through_reflection.name - [owner, Array(association.reader)] + center = target_records_from_association(association) + [owner, Array(center)] end reset_association owners, through_reflection.name @@ -49,7 +50,7 @@ module ActiveRecord rhs_records = middles.flat_map { |r| association = r.association source_reflection.name - association.reader + target_records_from_association(association) }.compact rhs_records.sort_by { |rhs| record_offset[rhs] } @@ -91,6 +92,10 @@ module ActiveRecord scope end + + def target_records_from_association(association) + association.loaded? ? association.target : association.reader + end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index fc12c3f45a..bac5a38a5d 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -331,15 +331,30 @@ module ActiveRecord validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) unless valid = record.valid?(validation_context) if reflection.options[:autosave] + indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) + record.errors.each do |attribute, message| - if index.nil? || (!reflection.options[:index_errors] && !ActiveRecord::Base.index_nested_attribute_errors) - attribute = "#{reflection.name}.#{attribute}" - else + if indexed_attribute attribute = "#{reflection.name}[#{index}].#{attribute}" + else + attribute = "#{reflection.name}.#{attribute}" end errors[attribute] << message errors[attribute].uniq! end + + record.errors.details.each_key do |attribute| + if indexed_attribute + reflection_attribute = "#{reflection.name}[#{index}].#{attribute}" + else + reflection_attribute = "#{reflection.name}.#{attribute}" + end + + record.errors.details[attribute].each do |error| + errors.details[reflection_attribute] << error + errors.details[reflection_attribute].uniq! + end + end else errors.add(reflection.name) end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 9782e58299..4a31a1aa84 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -261,7 +261,7 @@ module ActiveRecord #:nodoc: # The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * RecordInvalid - raised by {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] and + # * RecordInvalid - raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!] # when the record is invalid. # * RecordNotFound - No record responded to the {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method. diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 4058affec3..854f9776a3 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -175,21 +175,6 @@ module ActiveRecord # end # end # - # The callback macros usually accept a symbol for the method they're supposed to run, but you can also - # pass a "method string", which will then be evaluated within the binding of the callback. Example: - # - # class Topic < ActiveRecord::Base - # before_destroy 'self.class.delete_all "parent_id = #{id}"' - # end - # - # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback - # is triggered. Also note that these inline callbacks can be stacked just like the regular ones: - # - # class Topic < ActiveRecord::Base - # before_destroy 'self.class.delete_all "parent_id = #{id}"', - # 'puts "Evaluated after parents are destroyed"' - # end - # # == <tt>before_validation*</tt> returning statements # # If the +before_validation+ callback throws +:abort+, the process will be 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 848aeb821c..0ac5e80119 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -58,8 +58,8 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds select_rows(to_sql(arel, binds), name, binds).map(&:first) end @@ -115,20 +115,21 @@ module ActiveRecord # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds, pk, sequence_name) - id_value || last_inserted_id(value) + insert_sql(to_sql(arel, binds), name, pk, id_value, sequence_name, binds) end + alias create insert # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) exec_update(to_sql(arel, binds), name, binds) end + alias update_sql update # Executes the delete statement and returns the number of rows affected. def delete(arel, name = nil, binds = []) exec_delete(to_sql(arel, binds), name, binds) end + alias delete_sql delete # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ @@ -208,7 +209,7 @@ module ActiveRecord # * You are joining an existing open transaction # * You are creating a nested (savepoint) transaction # - # The mysql, mysql2 and postgresql adapters support setting the 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. @@ -344,17 +345,18 @@ module ActiveRecord # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in # an UPDATE statement, so in the MySQL adapters we redefine this to do that. - def join_to_update(update, select) #:nodoc: - key = update.key + def join_to_update(update, select, key) # :nodoc: subselect = subquery_for(key, select) update.where key.in(subselect) end + alias join_to_delete join_to_update - def join_to_delete(delete, select, key) #:nodoc: - subselect = subquery_for(key, select) - - delete.where key.in(subselect) + # Executes an INSERT query and returns the new record's ID + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) + sql, binds = sql_for_insert(sql, pk, id_value, sequence_name, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) + id_value || last_inserted_id(value) end protected @@ -375,22 +377,6 @@ module ActiveRecord exec_query(sql, name, binds, prepare: true) end - # Returns the last auto-generated ID from the affected table. - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - execute(sql, name) - id_value - end - - # Executes the update statement and returns the number of rows affected. - def update_sql(sql, name = nil) - execute(sql, name) - end - - # Executes the delete statement and returns the number of rows affected. - def delete_sql(sql, name = nil) - update_sql(sql, name) - end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) [sql, binds] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 9ec0a67c8f..bcc41acaa1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -93,7 +93,7 @@ module ActiveRecord # Override to return the quoted table name for assignment. Defaults to # table quoting. # - # This works for mysql and mysql2 where table.column can be used to + # This works for mysql2 where table.column can be used to # resolve ambiguity. # # We override this in the sqlite3 and postgresql adapters to use only 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 e252ddb4cf..797662d07c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -61,8 +61,8 @@ module ActiveRecord end def schema_limit(column) - limit = column.limit || native_database_types[column.type][:limit] - limit.inspect if limit + limit = column.limit + limit.inspect if limit && limit != native_database_types[column.type][:limit] end def schema_precision(column) 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 ac77011b83..70868ebd03 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -82,11 +82,10 @@ module ActiveRecord # def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name).map(&:to_s) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) checks = [] - checks << lambda { |i| i.name == index_name } checks << lambda { |i| i.columns == column_names } checks << lambda { |i| i.unique } if options[:unique] + checks << lambda { |i| i.name == options[:name].to_s } if options[:name] indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end @@ -700,15 +699,15 @@ module ActiveRecord # Removes the given index from the table. # - # Removes the +index_accounts_on_column+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, :branch_id # - # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: :branch_id # - # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table. + # Removes the index on +branch_id+ and +party_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: [:branch_id, :party_id] # @@ -1033,11 +1032,12 @@ module ActiveRecord end # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT. - # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they + # PostgreSQL, MySQL, and Oracle overrides this for custom DISTINCT syntax - they # require the order columns appear in the SELECT. # # columns_for_distinct("posts.id", ["posts.created_at desc"]) - def columns_for_distinct(columns, orders) #:nodoc: + # + def columns_for_distinct(columns, orders) # :nodoc: columns end @@ -1131,21 +1131,35 @@ module ActiveRecord end def index_name_for_remove(table_name, options = {}) - index_name = index_name(table_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) - unless index_name_exists?(table_name, index_name, true) - if options.is_a?(Hash) && options.has_key?(:name) - options_without_column = options.dup - options_without_column.delete :column - index_name_without_column = index_name(table_name, options_without_column) + checks = [] - return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) - end + if options.is_a?(Hash) + checks << lambda { |i| i.name == options[:name].to_s } if options.has_key?(:name) + column_names = Array(options[:column]).map(&:to_s) + else + column_names = Array(options).map(&:to_s) + end - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + if column_names.any? + checks << lambda { |i| i.columns.join('_and_') == column_names.join('_and_') } end - index_name + raise ArgumentError "No name or columns specified" if checks.none? + + matching_indexes = indexes(table_name).select { |i| checks.all? { |check| check[i] } } + + if matching_indexes.count > 1 + raise ArgumentError, "Multiple indexes found on #{table_name} columns #{column_names}. " \ + "Specify an index name from #{matching_indexes.map(&:name).join(', ')}" + elsif matching_indexes.none? + raise ArgumentError, "No indexes found on #{table_name} with the options provided." + else + matching_indexes.first.name + end end def rename_table_indexes(table_name, new_name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 295a7bed87..14d04a6388 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -33,6 +33,7 @@ module ActiveRecord class NullTransaction #:nodoc: def initialize; end + def state; end def closed?; true; end def open?; false; end def joinable?; false; end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 4b6912c616..dce34a208f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -23,6 +23,7 @@ module ActiveRecord autoload :TableDefinition autoload :Table autoload :AlterTable + autoload :ReferenceDefinition end autoload_at 'active_record/connection_adapters/abstract/connection_pool' do @@ -376,7 +377,7 @@ module ActiveRecord end # Provides access to the underlying database driver for this adapter. For - # example, this method returns a Mysql object in case of MysqlAdapter, + # example, this method returns a Mysql2::Client object in case of Mysql2Adapter, # and a PGconn object in case of PostgreSQLAdapter. # # This is useful for when you need to call a proprietary method such as @@ -391,19 +392,17 @@ module ActiveRecord def release_savepoint(name = nil) end - def case_sensitive_modifier(node, table_attribute) - node - end - def case_sensitive_comparison(table, attribute, column, value) - table_attr = table[attribute] - value = case_sensitive_modifier(value, table_attr) unless value.nil? - table_attr.eq(value) + if value.nil? + table[attribute].eq(value) + else + table[attribute].eq(Arel::Nodes::BindParam.new) + end end def case_insensitive_comparison(table, attribute, column, value) if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(value)) + table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new)) else case_sensitive_comparison(table, attribute, column, value) end 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 2bc736d63f..828e46637d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -31,8 +31,6 @@ module ActiveRecord def extract_default if blob_or_text_column? @default = null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(default) - @default = nil end end @@ -42,11 +40,11 @@ module ActiveRecord end def blob_or_text_column? - sql_type =~ /blob/i || type == :text + /\A(?:tiny|medium|long)?blob\b/ === sql_type || type == :text end def unsigned? - /unsigned/ === sql_type + /\bunsigned\z/ === sql_type end def case_sensitive? @@ -59,17 +57,6 @@ module ActiveRecord private - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string) but we can for others (integer, datetime, boolean, - # and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string?(default) - type != :string && !null && default == '' - end - def assert_valid_default(default) if blob_or_text_column? && default.present? raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" @@ -106,12 +93,11 @@ module ActiveRecord ## # :singleton-method: - # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> - # as boolean. If you wish to disable this emulation (which was the default - # behavior in versions 0.13.1 and earlier) you can add the following line + # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> + # as boolean. If you wish to disable this emulation you can add the following line # to your application.rb file: # - # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false + # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false class_attribute :emulate_booleans self.emulate_booleans = true @@ -400,18 +386,13 @@ module ActiveRecord log(sql, name) { @connection.query(sql) } end - # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in an abstract way without concerning ourselves about whether it needs to be - # explicitly freed or not. - def execute_and_free(sql, name = nil) #:nodoc: + # Mysql2Adapter doesn't have to free a result after using it, but we use this method + # to write stuff in an abstract way without concerning ourselves about whether it + # needs to be explicitly freed or not. + def execute_and_free(sql, name = nil) # :nodoc: yield execute(sql, name) end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.affected_rows - end - def begin_db_transaction execute "BEGIN" end @@ -432,7 +413,7 @@ module ActiveRecord # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support # these, we must use a subquery. - def join_to_update(update, select) #:nodoc: + def join_to_update(update, select, key) # :nodoc: if select.limit || select.offset || select.orders.any? super else @@ -638,6 +619,7 @@ module ActiveRecord # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) + create_table_info_cache.delete(table_name) if create_table_info_cache.key?(table_name) execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -766,16 +748,11 @@ module ActiveRecord SQL end - def case_sensitive_modifier(node, table_attribute) - node = Arel::Nodes.build_quoted node, table_attribute - Arel::Nodes::Bin.new(node) - end - def case_sensitive_comparison(table, attribute, column, value) - if column.case_sensitive? - table[attribute].eq(value) - else + if value.nil? || column.case_sensitive? super + else + table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) end end @@ -783,10 +760,25 @@ module ActiveRecord if column.case_sensitive? super else - table[attribute].eq(value) + table[attribute].eq(Arel::Nodes::BindParam.new) end end + # In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use + # DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for + # distinct queries, and requires that the ORDER BY include the distinct column. + # See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html + def columns_for_distinct(columns, orders) # :nodoc: + order_columns = orders.reject(&:blank?).map { |s| + # Convert Arel node to string + s = s.to_sql unless s.is_a?(String) + # Remove any ASC/DESC modifiers + s.gsub(/\s+(?:ASC|DESC)\b/i, '') + }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } + + [super, *order_columns].join(', ') + end + def strict_mode? self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end @@ -839,7 +831,7 @@ module ActiveRecord def register_integer_type(mapping, key, options) # :nodoc: mapping.register_type(key) do |sql_type| - if /unsigned/i =~ sql_type + if /\bunsigned\z/ === sql_type Type::UnsignedInteger.new(options) else Type::Integer.new(options) @@ -966,11 +958,13 @@ module ActiveRecord subsubselect = select.clone subsubselect.projections = [key] + # Materialize subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + subsubselect.distinct unless select.limit || select.offset || select.orders.any? + subselect = Arel::SelectManager.new(select.engine) subselect.project Arel.sql(key.name) - # Materialized subquery by adding distinct - # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.from subsubselect.distinct.as('__active_record_temp') + subselect.from subsubselect.as('__active_record_temp') end def mariadb? @@ -1033,9 +1027,12 @@ module ActiveRecord end end + def create_table_info_cache # :nodoc: + @create_table_info_cache ||= {} + end + def create_table_info(table_name) # :nodoc: - @create_table_info_cache = {} - @create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + 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: @@ -1102,11 +1099,8 @@ module ActiveRecord end end - ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql) ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) - ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) - ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql) ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2) end end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 08d46fca96..f633892dee 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -175,7 +175,7 @@ module ActiveRecord rescue Gem::LoadError => e raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e - raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace + raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end adapter_method = "#{spec[:adapter]}_connection" diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index ca7dfda80d..157e75dbf7 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -11,6 +11,30 @@ module ActiveRecord args.each { |name| column(name, :blob, options) } end + def tinyblob(*args, **options) + args.each { |name| column(name, :tinyblob, options) } + end + + def mediumblob(*args, **options) + args.each { |name| column(name, :mediumblob, options) } + end + + def longblob(*args, **options) + args.each { |name| column(name, :longblob, options) } + end + + def tinytext(*args, **options) + args.each { |name| column(name, :tinytext, options) } + end + + def mediumtext(*args, **options) + args.each { |name| column(name, :mediumtext, options) } + end + + def longtext(*args, **options) + args.each { |name| column(name, :longtext, options) } + end + def json(*args, **options) args.each { |name| column(name, :json, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 6590e0140d..c3c5b660fd 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -11,8 +11,13 @@ module ActiveRecord config[:username] = 'root' if config[:username].nil? config[:flags] ||= 0 + if Mysql2::Client.const_defined? :FOUND_ROWS - config[:flags] |= Mysql2::Client::FOUND_ROWS + if config[:flags].kind_of? Array + config[:flags].push "FOUND_ROWS".freeze + else + config[:flags] |= Mysql2::Client::FOUND_ROWS + end end client = Mysql2::Client.new(config) @@ -94,33 +99,15 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== #++ - # FIXME: re-enable the following once a "better" query_cache solution is in core - # - # The overrides below perform much better than the originals in AbstractAdapter - # because we're able to take advantage of mysql2's lazy-loading capabilities - # - # # Returns a record hash with the column names as keys and column values - # # as values. - # def select_one(sql, name = nil) - # result = execute(sql, name) - # result.each(as: :hash) do |r| - # return r - # end - # end - # - # # Returns a single value from a record - # def select_value(sql, name = nil) - # result = execute(sql, name) - # if first = result.first - # first.first - # end - # end - # - # # Returns an array of the values of the first column in a select: - # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - # def select_values(sql, name = nil) - # execute(sql, name).map { |row| row.first } - # 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) + 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+. @@ -149,12 +136,6 @@ module ActiveRecord alias exec_without_stmt exec_query - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - super - id_value || @connection.last_id - end - alias :create :insert_sql - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) execute to_sql(sql, binds), name end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb deleted file mode 100644 index 76f1b91e6b..0000000000 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ /dev/null @@ -1,481 +0,0 @@ -require 'active_record/connection_adapters/abstract_mysql_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'active_support/core_ext/hash/keys' - -gem 'mysql', '~> 2.9' -require 'mysql' - -class Mysql # :nodoc: all - class Time - # Used for casting DateTime fields to a MySQL friendly Time. - # This was documented in 48498da0dfed5239ea1eafb243ce47d7e3ce9e8e - def to_date - Date.new(year, month, day) - end - end - class Stmt; include Enumerable end - class Result; include Enumerable end -end - -module ActiveRecord - module ConnectionHandling # :nodoc: - # Establishes a connection to the database that's used by all Active Record objects. - def mysql_connection(config) - config = config.symbolize_keys - host = config[:host] - port = config[:port] - socket = config[:socket] - username = config[:username] ? config[:username].to_s : 'root' - password = config[:password].to_s - database = config[:database] - - mysql = Mysql.init - mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey] - - default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0 - default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS) - options = [host, username, password, database, port, socket, default_flags] - ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) - rescue Mysql::Error => error - if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError - else - raise - end - end - end - - module ConnectionAdapters - # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with - # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). - # - # Options: - # - # * <tt>:host</tt> - Defaults to "localhost". - # * <tt>:port</tt> - Defaults to 3306. - # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". - # * <tt>:username</tt> - Defaults to "root" - # * <tt>:password</tt> - Defaults to nothing. - # * <tt>:database</tt> - The name of the database. No default, must be provided. - # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/set-statement.html). - # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. - # - class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL'.freeze - - class StatementPool < ConnectionAdapters::StatementPool - private - - def dealloc(stmt) - stmt[:stmt].close - end - end - - def initialize(connection, logger, connection_options, config) - super - @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @client_encoding = nil - @connection_options = connection_options - connect - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # HELPER METHODS =========================================== - - def each_hash(result) # :nodoc: - if block_given? - result.each_hash do |row| - row.symbolize_keys! - yield row - end - else - to_enum(:each_hash, result) - end - end - - def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: - field = set_field_encoding(field) - super - end - - def error_number(exception) # :nodoc: - exception.errno if exception.respond_to?(:errno) - end - - # QUOTING ================================================== - - def quote_string(string) #:nodoc: - @connection.quote(string) - end - - #-- - # CONNECTION MANAGEMENT ==================================== - #++ - - def active? - if @connection.respond_to?(:stat) - @connection.stat - else - @connection.query 'select 1' - end - - # mysql-ruby doesn't raise an exception when stat fails. - if @connection.respond_to?(:errno) - @connection.errno.zero? - else - true - end - rescue Mysql::Error - false - end - - def reconnect! - super - disconnect! - connect - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - @connection.close rescue nil - end - - def reset! - if @connection.respond_to?(:change_user) - # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to - # reset the connection is to change the user to the same user. - @connection.change_user(@config[:username], @config[:password], @config[:database]) - configure_connection - end - end - - #-- - # DATABASE STATEMENTS ====================================== - #++ - - def select_all(arel, name = nil, binds = []) - if ExplainRegistry.collect? && prepared_statements - unprepared_statement { super } - else - super - end - end - - def select_rows(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = exec_query(sql, name, binds).rows - @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped - rows - end - - # Clears the prepared statements cache. - def clear_cache! - super - @statements.clear - end - - # Taken from here: - # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb - # Author: TOMITA Masahiro <tommy@tmtm.org> - ENCODINGS = { - "armscii8" => nil, - "ascii" => Encoding::US_ASCII, - "big5" => Encoding::Big5, - "binary" => Encoding::ASCII_8BIT, - "cp1250" => Encoding::Windows_1250, - "cp1251" => Encoding::Windows_1251, - "cp1256" => Encoding::Windows_1256, - "cp1257" => Encoding::Windows_1257, - "cp850" => Encoding::CP850, - "cp852" => Encoding::CP852, - "cp866" => Encoding::IBM866, - "cp932" => Encoding::Windows_31J, - "dec8" => nil, - "eucjpms" => Encoding::EucJP_ms, - "euckr" => Encoding::EUC_KR, - "gb2312" => Encoding::EUC_CN, - "gbk" => Encoding::GBK, - "geostd8" => nil, - "greek" => Encoding::ISO_8859_7, - "hebrew" => Encoding::ISO_8859_8, - "hp8" => nil, - "keybcs2" => nil, - "koi8r" => Encoding::KOI8_R, - "koi8u" => Encoding::KOI8_U, - "latin1" => Encoding::ISO_8859_1, - "latin2" => Encoding::ISO_8859_2, - "latin5" => Encoding::ISO_8859_9, - "latin7" => Encoding::ISO_8859_13, - "macce" => Encoding::MacCentEuro, - "macroman" => Encoding::MacRoman, - "sjis" => Encoding::SHIFT_JIS, - "swe7" => nil, - "tis620" => Encoding::TIS_620, - "ucs2" => Encoding::UTF_16BE, - "ujis" => Encoding::EucJP_ms, - "utf8" => Encoding::UTF_8, - "utf8mb4" => Encoding::UTF_8, - } - - # Get the client encoding for this database - def client_encoding - return @client_encoding if @client_encoding - - result = exec_query( - "select @@character_set_client", - 'SCHEMA') - @client_encoding = ENCODINGS[result.rows.last.last] - end - - def exec_query(sql, name = 'SQL', binds = [], prepare: false) - if without_prepared_statement?(binds) - result_set, affected_rows = exec_without_stmt(sql, name) - else - result_set, affected_rows = exec_stmt(sql, name, binds, cache_stmt: prepare) - end - - yield affected_rows if block_given? - - result_set - end - - def last_inserted_id(result) - @connection.insert_id - end - - module Fields # :nodoc: - class DateTime < Type::DateTime # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class Time < Type::Time # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - 2000, - 01, - 01, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class << self - TYPES = Type::HashLookupTypeMap.new # :nodoc: - - delegate :register_type, :alias_type, to: :TYPES - - def find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES.lookup(Mysql::Field::TYPE_LONG) - else - TYPES.lookup(field.type) - end - end - end - - register_type Mysql::Field::TYPE_TINY, Type::Boolean.new - register_type Mysql::Field::TYPE_LONG, Type::Integer.new - alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG - alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - - register_type Mysql::Field::TYPE_DATE, Type::Date.new - register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new - register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Type::Float.new - end - - def initialize_type_map(m) # :nodoc: - super - register_class_with_precision m, %r(datetime)i, Fields::DateTime - register_class_with_precision m, %r(time)i, Fields::Time - end - - def exec_without_stmt(sql, name = 'SQL') # :nodoc: - # Some queries, like SHOW CREATE TABLE don't work through the prepared - # statement API. For those queries, we need to use this method. :'( - log(sql, name) do - result = @connection.query(sql) - affected_rows = @connection.affected_rows - - if result - types = {} - fields = [] - result.fetch_fields.each { |field| - field_name = field.name - fields << field_name - - if field.decimals > 0 - types[field_name] = Type::Decimal.new - else - types[field_name] = Fields.find_type field - end - } - - result_set = ActiveRecord::Result.new(fields, result.to_a, types) - result.free - else - result_set = ActiveRecord::Result.new([], []) - end - - [result_set, affected_rows] - end - end - - def execute_and_free(sql, name = nil) # :nodoc: - result = execute(sql, name) - ret = yield result - result.free - ret - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super sql, name - id_value || @connection.insert_id - end - alias :create :insert_sql - - def exec_delete(sql, name, binds) # :nodoc: - affected_rows = 0 - - exec_query(sql, name, binds) do |n| - affected_rows = n - end - - affected_rows - end - alias :exec_update :exec_delete - - def begin_db_transaction #:nodoc: - exec_query "BEGIN" - end - - private - - def exec_stmt(sql, name, binds, cache_stmt: false) - cache = {} - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - - log(sql, name, binds) do - if !cache_stmt - stmt = @connection.prepare(sql) - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - end - - begin - stmt.execute(*type_casted_binds) - rescue Mysql::Error => e - # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older MySQL versions, we - # need to close the statement and delete the statement from the - # cache. - if !cache_stmt - stmt.close - else - @statements.delete sql - end - raise e - end - - cols = nil - if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map(&:name) - metadata.free - end - - result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols - affected_rows = stmt.affected_rows - - stmt.free_result - stmt.close if !cache_stmt - - [result_set, affected_rows] - end - end - - def connect - encoding = @config[:encoding] - if encoding - @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil - end - - if @config[:sslca] || @config[:sslkey] - @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) - end - - @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] - @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] - @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] - - @connection.real_connect(*@connection_options) - - # reconnect must be set after real_connect is called, because real_connect sets it to false internally - @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) - - configure_connection - end - - # Many Rails applications monkey-patch a replacement of the configure_connection method - # and don't call 'super', so leave this here even though it looks superfluous. - def configure_connection - super - end - - def select(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = super - @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped - rows - end - - # Returns the full version of the connected MySQL server. - def full_version - @full_version ||= @connection.server_info - end - - def set_field_encoding(field_name) - field_name.force_encoding(client_encoding) - if internal_enc = Encoding.default_internal - field_name = field_name.encode!(internal_enc) - end - field_name - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 0e0c0e993a..11a151edd5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -52,8 +52,8 @@ module ActiveRecord end end - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) execute_and_clear(sql, name, binds) do |result| if result.nfields > 0 @@ -73,25 +73,13 @@ module ActiveRecord end # Executes an INSERT query and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) # :nodoc: unless pk # Extract the table from the insert sql. Yuck. table_ref = extract_table_ref_from_insert_sql(sql) pk = primary_key(table_ref) if table_ref end - - if pk && use_insert_returning? - select_value("#{sql} RETURNING #{quote_column_name(pk)}") - elsif pk - super - last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) - else - super - end - end - - def create - super.insert + super end # The internal PostgreSQL identifier of the money data type. @@ -175,12 +163,6 @@ module ActiveRecord alias :exec_update :exec_delete def sql_for_insert(sql, pk, id_value, sequence_name, binds) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - if pk && use_insert_returning? sql = "#{sql} RETURNING #{quote_column_name(pk)}" end @@ -202,11 +184,6 @@ module ActiveRecord end end - # Executes an UPDATE query and returns the number of affected tuples. - def update_sql(sql, name = nil) - super.cmd_tuples - end - # Begins a transaction. def begin_db_transaction execute "BEGIN" 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 a48d64f7bd..67e727d8ed 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -169,15 +169,18 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name, name = nil) - result = query(<<-SQL, 'SCHEMA') - SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid - FROM pg_class t - INNER JOIN pg_index d ON t.oid = d.indrelid - INNER JOIN pg_class i ON d.indexrelid = i.oid - WHERE i.relkind = 'i' - AND d.indisprimary = 'f' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + 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 + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + LEFT JOIN pg_namespace n ON n.oid = i.relnamespace + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table.identifier}' + AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'} ORDER BY i.relname SQL @@ -504,14 +507,27 @@ module ActiveRecord end def remove_index(table_name, options = {}) #:nodoc: - index_name = index_name_for_remove(table_name, options) + table = Utils.extract_schema_qualified_name(table_name.to_s) + + if options.is_a?(Hash) && options.key?(:name) + provided_index = Utils.extract_schema_qualified_name(options[:name].to_s) + + options[:name] = provided_index.identifier + table = PostgreSQL::Name.new(provided_index.schema, table.identifier) unless table.schema.present? + + if provided_index.schema.present? && table.schema != provided_index.schema + raise ArgumentError.new("Index schema '#{provided_index.schema}' does not match table schema '#{table.schema}'") + end + end + + index_to_remove = PostgreSQL::Name.new(table.schema, index_name_for_remove(table.to_s, options)) algorithm = - if Hash === options && options.key?(:algorithm) + if options.is_a?(Hash) && options.key?(:algorithm) index_algorithms.fetch(options[:algorithm]) do raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") end end - execute "DROP INDEX #{algorithm} #{quote_table_name(index_name)}" + execute "DROP INDEX #{algorithm} #{quote_table_name(index_to_remove)}" end # Renames an index of a table. Raises error if length of new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index aa43854d01..e313a34c3a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -390,12 +390,12 @@ module ActiveRecord "average" => "avg", } - protected + # Returns the version of the connected PostgreSQL server. + def postgresql_version + @connection.server_version + end - # Returns the version of the connected PostgreSQL server. - def postgresql_version - @connection.server_version - end + protected # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html FOREIGN_KEY_VIOLATION = "23503" @@ -816,9 +816,8 @@ module ActiveRecord ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) - ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:point, OID::Rails51Point, adapter: :postgresql) ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql) - ActiveRecord::Type.register(:rails_5_1_point, OID::Rails51Point, adapter: :postgresql) ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 163cbb875f..d1893f35f5 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -280,22 +280,6 @@ module ActiveRecord log(sql, name) { @connection.execute(sql) } end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.changes - end - - def delete_sql(sql, name = nil) #:nodoc: - sql += " WHERE 1=1" unless sql =~ /WHERE/i - super sql, name - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super - id_value || @connection.last_insert_row_id - end - alias :create :insert_sql - def select_rows(sql, name = nil, binds = []) exec_query(sql, name, binds).rows end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index aedef54928..a8b3d03ba5 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -8,7 +8,7 @@ module ActiveRecord # example for regular databases (MySQL, PostgreSQL, etc): # # ActiveRecord::Base.establish_connection( - # adapter: "mysql", + # adapter: "mysql2", # host: "localhost", # username: "myuser", # password: "mypass", diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 7ded96f8fb..04519f4dc3 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -124,7 +124,7 @@ module ActiveRecord def deserialize(value) return if value.nil? - mapping.key(value.to_i) + mapping.key(value) end def serialize(value) diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 1cd2c2ef8c..e5906b6756 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -272,7 +272,7 @@ module ActiveRecord # * You are joining an existing open transaction # * You are creating a nested (savepoint) transaction # - # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level. + # The mysql2 and postgresql adapters support setting the transaction isolation level. class TransactionIsolationError < ActiveRecordError end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index deb7d7d06d..ed1bbf5dcd 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -401,7 +401,7 @@ module ActiveRecord # It's possible to set the fixture's model class directly in the YAML file. # This is helpful when fixtures are loaded outside tests and # +set_fixture_class+ is not available (e.g. - # when running <tt>rake db:fixtures:load</tt>). + # when running <tt>rails db:fixtures:load</tt>). # # _fixture: # model_class: User diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index a388b529c9..ecf4046bff 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -8,7 +8,7 @@ module ActiveRecord MAJOR = 5 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index f5753fe426..f699e83ab3 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -126,9 +126,9 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize(message = nil) if !message && defined?(Rails.env) - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}.") + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate RAILS_ENV=#{::Rails.env}") elsif !message - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate.") + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate") else super end @@ -337,16 +337,16 @@ module ActiveRecord # end # # To run migrations against the currently configured database, use - # <tt>rake db:migrate</tt>. This will update the database by running all of the + # <tt>rails db:migrate</tt>. This will update the database by running all of the # pending migrations, creating the <tt>schema_migrations</tt> table # (see "About the schema_migrations table" section below) if missing. It will also # invoke the db:schema:dump task, which will update your db/schema.rb file # to match the structure of your database. # # To roll the database back to a previous migration version, use - # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which + # <tt>rails db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which # you wish to downgrade. Alternatively, you can also use the STEP option if you - # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback + # wish to rollback last few migrations. <tt>rails db:migrate STEP=2</tt> will rollback # the latest two migrations. # # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, @@ -579,7 +579,7 @@ module ActiveRecord FileUtils.cd Rails.root do current_config = Base.connection_config Base.clear_all_connections! - system("bin/rake db:test:prepare") + system("bin/rails db:test:prepare") # Establish a new connection, the old database may be gone (db:test:prepare uses purge) Base.establish_connection(current_config) end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 4c8db8a2d5..1b94573870 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -28,6 +28,43 @@ module ActiveRecord options[:null] = true if options[:null].nil? super end + + def index_exists?(table_name, column_name, options = {}) + column_names = Array(column_name).map(&:to_s) + options[:name] = + if options.key?(:name).present? + options[:name].to_s + else + index_name(table_name, column: column_names) + end + super + end + + def remove_index(table_name, options = {}) + options = { column: options } unless options.is_a?(Hash) + options[:name] = index_name_for_remove(table_name, options) + super(table_name, options) + end + + private + + def index_name_for_remove(table_name, options = {}) + index_name = index_name(table_name, options) + + unless index_name_exists?(table_name, index_name, true) + if options.is_a?(Hash) && options.has_key?(:name) + options_without_column = options.dup + options_without_column.delete :column + index_name_without_column = index_name(table_name, options_without_column) + + return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + end + + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + + index_name + end end class V4_2 < V5_0 diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index b82923494f..f26c8471bc 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -398,7 +398,7 @@ module ActiveRecord If you'd like the new behavior today, you can add this line: - attribute :#{column.name}, :rails_5_1_point#{array_arguments} + attribute :#{column.name}, :point#{array_arguments} WARNING end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 2744673c12..38916f7376 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -57,8 +57,10 @@ module ActiveRecord console do |app| require "active_record/railties/console_sandbox" if app.sandbox? require "active_record/base" - console = ActiveSupport::Logger.new(STDERR) - Rails.logger.extend ActiveSupport::Logger.broadcast console + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT) + console = ActiveSupport::Logger.new(STDERR) + Rails.logger.extend ActiveSupport::Logger.broadcast console + end end runner do @@ -69,6 +71,7 @@ module ActiveRecord ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true self.default_timezone = :utc + self.time_zone_aware_types = ActiveRecord::Base.time_zone_aware_types end end @@ -134,8 +137,8 @@ Oops - You have a database configured, but it doesn't exist yet! Here's how to get started: 1. Configure your database in config/database.yml. - 2. Run `bin/rake db:create` to create the database. - 3. Run `bin/rake db:setup` to load your database schema. + 2. Run `bin/rails db:create` to create the database. + 3. Run `bin/rails db:setup` to load your database schema. end_warning raise end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index c1203fb745..ead9407391 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -177,7 +177,7 @@ db_namespace = namespace :db do pending_migrations.each do |pending_migration| puts ' %4d %s' % [pending_migration.version, pending_migration.name] end - abort %{Run `rake db:migrate` to update your database then try again.} + abort %{Run `rails db:migrate` to update your database then try again.} end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 2cf19c76c5..6aa490908b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -132,7 +132,7 @@ module ActiveRecord # ==== Examples # # users = User.where(name: 'Oscar') - # users.create # => #<User id: 3, name: "oscar", ...> + # users.create # => #<User id: 3, name: "Oscar", ...> # # users.create(name: 'fxn') # users.create # => #<User id: 4, name: "fxn", ...> @@ -371,11 +371,11 @@ module ActiveRecord stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) stmt.table(table) - stmt.key = table[primary_key] if joins_values.any? - @klass.connection.join_to_update(stmt, arel) + @klass.connection.join_to_update(stmt, arel, table[primary_key]) else + stmt.key = table[primary_key] stmt.take(arel.limit) stmt.order(*arel.orders) stmt.wheres = arel.constraints diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 435cef901b..3cbb12a09d 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -117,9 +117,9 @@ module ActiveRecord # def first(limit = nil) if limit - find_nth_with_limit(offset_index, limit) + find_nth_with_limit_and_offset(0, limit, offset: offset_index) else - find_nth(0, offset_index) + find_nth 0 end end @@ -169,7 +169,7 @@ module ActiveRecord # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4) # Person.where(["user_name = :u", { u: user_name }]).second def second - find_nth(1, offset_index) + find_nth 1 end # Same as #second but raises ActiveRecord::RecordNotFound if no record @@ -185,7 +185,7 @@ module ActiveRecord # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5) # Person.where(["user_name = :u", { u: user_name }]).third def third - find_nth(2, offset_index) + find_nth 2 end # Same as #third but raises ActiveRecord::RecordNotFound if no record @@ -201,7 +201,7 @@ module ActiveRecord # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6) # Person.where(["user_name = :u", { u: user_name }]).fourth def fourth - find_nth(3, offset_index) + find_nth 3 end # Same as #fourth but raises ActiveRecord::RecordNotFound if no record @@ -217,7 +217,7 @@ module ActiveRecord # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7) # Person.where(["user_name = :u", { u: user_name }]).fifth def fifth - find_nth(4, offset_index) + find_nth 4 end # Same as #fifth but raises ActiveRecord::RecordNotFound if no record @@ -233,7 +233,7 @@ module ActiveRecord # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44) # Person.where(["user_name = :u", { u: user_name }]).forty_two def forty_two - find_nth(41, offset_index) + find_nth 41 end # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record @@ -442,6 +442,8 @@ module ActiveRecord end def find_some(ids) + return find_some_ordered(ids) unless order_values.present? + result = where(primary_key => ids).to_a expected_size = @@ -463,6 +465,21 @@ module ActiveRecord end end + def find_some_ordered(ids) + ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] + + result = except(:limit, :offset).where(primary_key => ids).to_a + + if result.size == ids.size + pk_type = @klass.type_for_attribute(primary_key) + + records_by_id = result.index_by(&:id) + ids.map { |id| records_by_id.fetch(pk_type.cast(id)) } + else + raise_record_not_found_exception!(ids, result.size, ids.size) + end + end + def find_take if loaded? @records.first @@ -471,27 +488,39 @@ module ActiveRecord end end - def find_nth(index, offset) + def find_nth(index, offset = nil) if loaded? @records[index] else - offset += index - @offsets[offset] ||= find_nth_with_limit(offset, 1).first + # TODO: once the offset argument is removed we rely on offset_index + # within find_nth_with_limit, rather than pass it in via + # find_nth_with_limit_and_offset + if offset + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing an offset argument to find_nth is deprecated, + please use Relation#offset instead. + MSG + else + offset = offset_index + end + @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first end end def find_nth!(index) - find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + find_nth(index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end - def find_nth_with_limit(offset, limit) + def find_nth_with_limit(index, limit) + # TODO: once the offset argument is removed from find_nth, + # find_nth_with_limit_and_offset can be merged into this method relation = if order_values.empty? && primary_key order(arel_table[primary_key].asc) else self end - relation = relation.offset(offset) unless offset.zero? + relation = relation.offset(index) unless index.zero? relation.limit(limit).to_a end @@ -507,5 +536,16 @@ module ActiveRecord end end end + + private + + def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc: + if loaded? + @records[index, limit] + else + index += offset + find_nth_with_limit(index, limit) + end + end end end diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb index 92340216ed..8945cb0cc5 100644 --- a/activerecord/lib/active_record/relation/from_clause.rb +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -25,7 +25,7 @@ module ActiveRecord end def self.empty - new(nil, nil) + @empty ||= new(nil, nil) end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 983bf019bc..5635b4215b 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -54,16 +54,17 @@ module ActiveRecord end end + FROZEN_EMPTY_ARRAY = [].freeze Relation::MULTI_VALUE_METHODS.each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_values # def select_values - @values[:#{name}] || [] # @values[:select] || [] - end # end - # - def #{name}_values=(values) # def select_values=(values) - assert_mutability! # assert_mutability! - @values[:#{name}] = values # @values[:select] = values - end # end + def #{name}_values + @values[:#{name}] || FROZEN_EMPTY_ARRAY + end + + def #{name}_values=(values) + assert_mutability! + @values[:#{name}] = values + end CODE end @@ -116,8 +117,9 @@ module ActiveRecord result end + FROZEN_EMPTY_HASH = {}.freeze def create_with_value # :nodoc: - @values[:create_with] || {} + @values[:create_with] || FROZEN_EMPTY_HASH end alias extensions extending_values @@ -649,8 +651,8 @@ module ActiveRecord # they must differ only by #where (if no #group has been defined) or #having (if a #group is # present). Neither relation may have a #limit, #offset, or #distinct set. # - # Post.where("id = 1").or(Post.where("id = 2")) - # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) + # Post.where("id = 1").or(Post.where("author_id = 3")) + # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3')) # def or(other) spawn.or!(other) @@ -1190,7 +1192,7 @@ module ActiveRecord def structurally_compatible_for_or?(other) Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } && (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } && - (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") } + (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") == other.send("#{m}_clause") } end def new_where_clause diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb index 0a1814b3dd..dbd08811fa 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -24,9 +24,7 @@ module ActiveRecord end # :stopdoc: - ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| - payload = args.last - + ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload| QueryRegistry.queries << payload[:sql] end # :startdoc: @@ -34,14 +32,14 @@ module ActiveRecord class QueryRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry - attr_accessor :queries + attr_reader :queries def initialize - reset + @queries = [] end def reset - @queries = [] + @queries.clear end end end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 1f000b3f0f..2c2d6cfa47 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -81,7 +81,7 @@ module ActiveRecord end def self.empty - new([], []) + @empty ||= new([], []) end protected diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index a81ff98e49..dbf172a577 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -22,6 +22,7 @@ module ActiveRecord parts = predicate_builder.build_from_hash(attributes) when Arel::Nodes::Node parts = [opts] + binds = other else raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 4e89ba4dd1..2bfc5ff7ae 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -213,7 +213,7 @@ module ActiveRecord end # TODO: Deprecate this - def quoted_id + def quoted_id # :nodoc: self.class.quote_value(@attributes[self.class.primary_key].value_for_database) end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 7b8f27699a..6a9af5d1b4 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -250,7 +250,7 @@ module ActiveRecord def check_schema_file(filename) unless File.exist?(filename) - message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} + message = %{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.} message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails) Kernel.abort message end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 100df9c6c6..7a49322e06 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -94,8 +94,6 @@ module ActiveRecord ArJdbcMySQL::Error elsif defined?(Mysql2) Mysql2::Error - elsif defined?(Mysql) - Mysql::Error else StandardError end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index aa2794f120..a376e2a17f 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -73,15 +73,18 @@ module ActiveRecord value = value.to_s[0, column.limit] end - value = Arel::Nodes::Quoted.new(value) - comparison = if !options[:case_sensitive] && !value.nil? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else klass.connection.case_sensitive_comparison(table, attribute, column, value) end - klass.unscoped.where(comparison) + if value.nil? + klass.unscoped.where(comparison) + else + bind = Relation::QueryAttribute.new(attribute.to_s, value, Type::Value.new) + klass.unscoped.where(comparison, bind) + end rescue RangeError klass.none end @@ -231,7 +234,6 @@ module ActiveRecord # # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: # - # * ActiveRecord::ConnectionAdapters::MysqlAdapter. # * ActiveRecord::ConnectionAdapters::Mysql2Adapter. # * ActiveRecord::ConnectionAdapters::SQLite3Adapter. # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter. |