diff options
Diffstat (limited to 'activerecord/lib')
17 files changed, 129 insertions, 62 deletions
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 5d0927f17d..0bb63b97ae 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -17,6 +17,23 @@ module ActiveRecord # CollectionAssociation # HasManyAssociation + ForeignAssociation # HasManyThroughAssociation + ThroughAssociation + # + # Associations in Active Record are middlemen between the object that + # holds the association, known as the <tt>owner</tt>, and the associated + # result set, known as the <tt>target</tt>. Association metadata is available in + # <tt>reflection</tt>, which is an instance of <tt>ActiveRecord::Reflection::AssociationReflection</tt>. + # + # For example, given + # + # class Blog < ActiveRecord::Base + # has_many :posts + # end + # + # blog = Blog.first + # + # The association of <tt>blog.posts</tt> has the object +blog+ as its + # <tt>owner</tt>, the collection of its posts as <tt>target</tt>, and + # the <tt>reflection</tt> object represents a <tt>:has_many</tt> macro. class Association #:nodoc: attr_reader :owner, :target, :reflection diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index b0c0beac0e..c3d4eab562 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -209,8 +209,7 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if !find_target? - loaded! unless loaded? + if !find_target? || loaded? target.size elsif @association_ids @association_ids.size diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 78993cee8a..edcb44f0fc 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -2,11 +2,8 @@ module ActiveRecord module Associations - # Association proxies in Active Record are middlemen between the object that - # holds the association, known as the <tt>@owner</tt>, and the actual associated - # object, known as the <tt>@target</tt>. The kind of association any proxy is - # about is available in <tt>@reflection</tt>. That's an instance of the class - # ActiveRecord::Reflection::AssociationReflection. + # Collection proxies in Active Record are middlemen between an + # <tt>association</tt>, and its <tt>target</tt> result set. # # For example, given # @@ -16,14 +13,14 @@ module ActiveRecord # # blog = Blog.first # - # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as - # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and - # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro. + # The collection proxy returned by <tt>blog.posts</tt> is built from a + # <tt>:has_many</tt> <tt>association</tt>, and delegates to a collection + # of posts as the <tt>target</tt>. # - # This class delegates unknown methods to <tt>@target</tt> via - # <tt>method_missing</tt>. + # This class delegates unknown methods to the <tt>association</tt>'s + # relation class via a delegate cache. # - # The <tt>@target</tt> object is not \loaded until needed. For example, + # The <tt>target</tt> result set is not loaded until needed. For example, # # blog.posts.count # diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 6f67934a79..5972846940 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -52,7 +52,11 @@ module ActiveRecord # If the collection is empty the target is set to an empty array and # the loaded flag is set to true as well. def count_records - count = counter_cache_value || scope.count(:all) + count = if reflection.has_cached_counter? + owner._read_attribute(reflection.counter_cache_column).to_i + else + scope.count(:all) + end # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a @@ -62,14 +66,6 @@ module ActiveRecord [association_scope.limit_value, count].compact.min end - def counter_cache_value - reflection.has_cached_counter? ? owner._read_attribute(reflection.counter_cache_column).to_i : nil - end - - def find_target? - super && !counter_cache_value&.zero? - end - def update_counter(difference, reflection = reflection()) if reflection.has_cached_counter? owner.increment!(reflection.counter_cache_column, difference) 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 2cb0a2a4df..7d20825a75 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -15,7 +15,7 @@ module ActiveRecord end delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn, private: true private @@ -50,7 +50,7 @@ module ActiveRecord statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) end - if supports_foreign_keys_in_create? + if supports_foreign_keys? statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) end @@ -127,6 +127,9 @@ module ActiveRecord end def foreign_key_in_create(from_table, to_table, options) + prefix = ActiveRecord::Base.table_name_prefix + suffix = ActiveRecord::Base.table_name_suffix + to_table = "#{prefix}#{to_table}#{suffix}" options = foreign_key_options(from_table, to_table, options) accept ForeignKeyDefinition.new(from_table, to_table, 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 198898b764..a27751fd70 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -102,7 +102,7 @@ module ActiveRecord alias validated? validate? def export_name_on_schema_dump? - name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern + !ActiveRecord::SchemaDumper.fk_ignore_pattern.match?(name) if name end def defined_for?(to_table_ord = nil, to_table: nil, **options) @@ -398,10 +398,7 @@ 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]) + foreign_keys << [table_name, options] end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and @@ -522,6 +519,7 @@ module ActiveRecord # t.json # t.virtual # t.remove + # t.remove_foreign_key # t.remove_references # t.remove_belongs_to # t.remove_index @@ -695,6 +693,16 @@ module ActiveRecord @base.add_foreign_key(name, *args) end + # Removes the given foreign key from the table. + # + # t.remove_foreign_key(:authors) + # t.remove_foreign_key(column: :author_id) + # + # See {connection.remove_foreign_key}[rdoc-ref:SchemaStatements#remove_foreign_key] + def remove_foreign_key(*args) + @base.remove_foreign_key(name, *args) + end + # Checks to see if a foreign key exists. # # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) 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 d88e75d692..78e153bcc9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1029,9 +1029,7 @@ module ActiveRecord end def foreign_key_column_for(table_name) # :nodoc: - prefix = Base.table_name_prefix - suffix = Base.table_name_suffix - name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s + name = strip_table_name_prefix_and_suffix(table_name) "#{name.singularize}_id" end @@ -1328,6 +1326,12 @@ module ActiveRecord { column: column_names } end + def strip_table_name_prefix_and_suffix(table_name) + prefix = Base.table_name_prefix + suffix = Base.table_name_suffix + table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s + end + def foreign_key_name(table_name, options) options.fetch(:name) do identifier = "#{table_name}_#{options.fetch(:column)}_fk" diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 9a7d7285f2..3c9510e469 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -335,6 +335,7 @@ module ActiveRecord def supports_foreign_keys_in_create? supports_foreign_keys? end + deprecate :supports_foreign_keys_in_create? # Does this adapter support views? def supports_views? diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index 2394982a7d..0a637d81fa 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -52,6 +52,34 @@ module ActiveRecord end.compact end + def add_foreign_key(from_table, to_table, **options) + alter_table(from_table) do |definition| + to_table = strip_table_name_prefix_and_suffix(to_table) + definition.foreign_key(to_table, options) + end + end + + def remove_foreign_key(from_table, to_table = nil, **options) + to_table ||= options[:to_table] + options = options.except(:name, :to_table) + foreign_keys = foreign_keys(from_table) + + fkey = foreign_keys.detect do |fk| + table = to_table || begin + table = options[:column].to_s.delete_suffix("_id") + Base.pluralize_table_names ? table.pluralize : table + end + table = strip_table_name_prefix_and_suffix(table) + fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table) + fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s } + end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table}") + + alter_table(from_table, foreign_keys) do |definition| + fk_to_table = strip_table_name_prefix_and_suffix(fkey.to_table) + definition.foreign_keys.delete([fk_to_table, fkey.options]) + end + end + def create_schema_dumper(options) SQLite3::SchemaDumper.create(self, options) end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7b3630662b..9f28fdf749 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -121,7 +121,7 @@ module ActiveRecord true end - def supports_foreign_keys_in_create? + def supports_foreign_keys? true end @@ -320,6 +320,9 @@ module ActiveRecord def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc: alter_table(table_name) do |definition| definition.remove_column column_name + definition.foreign_keys.delete_if do |_, fk_options| + fk_options[:column] == column_name.to_s + end end end @@ -421,9 +424,8 @@ module ActiveRecord type.to_sym == :primary_key || options[:primary_key] end - def alter_table(table_name, options = {}) + def alter_table(table_name, foreign_keys = foreign_keys(table_name), **options) altered_table_name = "a#{table_name}" - foreign_keys = foreign_keys(table_name) caller = lambda do |definition| rename = options[:rename] || {} @@ -431,7 +433,8 @@ module ActiveRecord if column = rename[fk.options[:column]] fk.options[:column] = column end - definition.foreign_key(fk.to_table, fk.options) + to_table = strip_table_name_prefix_and_suffix(fk.to_table) + definition.foreign_key(to_table, fk.options) end yield definition if block_given? diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index 73adf66684..654a01f23b 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -157,7 +157,7 @@ module ActiveRecord configs else configs.map do |config| - ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config) + ActiveRecord::DatabaseConfigurations::UrlConfig.new(config.env_name, config.spec_name, url, config.config) end end else diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb index aad263695b..b5b5df074c 100644 --- a/activerecord/lib/active_record/middleware/database_selector.rb +++ b/activerecord/lib/active_record/middleware/database_selector.rb @@ -11,7 +11,7 @@ module ActiveRecord # behavior. # # The resolver class defines when the application should switch (i.e. read - # from the primary if a write occurred less than 2 seconds ago) and an + # from the primary if a write occurred less than 2 seconds ago) and a # resolver context class that sets a value that helps the resolver class # decide when to switch. # diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 94906a2943..abc939826b 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -77,18 +77,18 @@ module ActiveRecord class V5_1 < V5_2 def change_column(table_name, column_name, type, options = {}) - if adapter_name == "PostgreSQL" + if connection.adapter_name == "PostgreSQL" super(table_name, column_name, type, options.except(:default, :null, :comment)) - change_column_default(table_name, column_name, options[:default]) if options.key?(:default) - 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) + connection.change_column_default(table_name, column_name, options[:default]) if options.key?(:default) + connection.change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + connection.change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) else super end end def create_table(table_name, options = {}) - if adapter_name == "Mysql2" + if connection.adapter_name == "Mysql2" super(table_name, options: "ENGINE=InnoDB", **options) else super @@ -110,13 +110,13 @@ module ActiveRecord end def create_table(table_name, options = {}) - if adapter_name == "PostgreSQL" + if connection.adapter_name == "PostgreSQL" if options[:id] == :uuid && !options.key?(:default) options[:default] = "uuid_generate_v4()" end end - unless adapter_name == "Mysql2" && options[:id] == :bigint + unless connection.adapter_name == "Mysql2" && options[:id] == :bigint if [:integer, :bigint].include?(options[:id]) && !options.key?(:default) options[:default] = nil end @@ -190,7 +190,7 @@ module ActiveRecord if options[:name].present? options[:name].to_s else - index_name(table_name, column: column_names) + connection.index_name(table_name, column: column_names) end super end @@ -210,15 +210,17 @@ module ActiveRecord end def index_name_for_remove(table_name, options = {}) - index_name = index_name(table_name, options) + index_name = connection.index_name(table_name, options) - unless index_name_exists?(table_name, index_name) + unless connection.index_name_exists?(table_name, index_name) 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) + index_name_without_column = connection.index_name(table_name, options_without_column) - return index_name_without_column if index_name_exists?(table_name, index_name_without_column) + if connection.index_name_exists?(table_name, index_name_without_column) + return index_name_without_column + end end raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 2213fbefb4..510a275b4e 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -142,7 +142,7 @@ module ActiveRecord end end - # Deletes the row with a primary key matching the +id+ argument, using a + # Deletes the row with a primary key matching the +id+ argument, using an # SQL +DELETE+ statement, and returns the number of rows deleted. Active # Record objects are not instantiated, so the object's callbacks are not # executed, including any <tt>:dependent</tt> association options. diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 8c1b2e2be1..2a27f53f06 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -40,8 +40,7 @@ module ActiveRecord def find_by_sql(sql, binds = [], preparable: nil, &block) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) column_types = result_set.column_types.dup - cached_columns_hash = connection.schema_cache.columns_hash(table_name) - cached_columns_hash.each_key { |k| column_types.delete k } + attribute_types.each_key { |k| column_types.delete k } message_bus = ActiveSupport::Notifications.instrumenter payload = { diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index cef31bea94..bdd3c540bb 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -186,11 +186,9 @@ module ActiveRecord relation = apply_join_dependency relation.pluck(*column_names) else - disallow_raw_sql!(column_names) + klass.disallow_raw_sql!(column_names) relation = spawn - relation.select_values = column_names.map { |cn| - @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn - } + relation.select_values = column_names result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) } result.cast_values(klass.attribute_types) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index eb80aab701..75976aa8fc 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1052,11 +1052,13 @@ module ActiveRecord def arel_columns(columns) columns.flat_map do |field| - if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value - arel_attribute(field) - elsif Symbol === field - connection.quote_table_name(field.to_s) - elsif Proc === field + case field + when Symbol + field = field.to_s + arel_column(field) { connection.quote_table_name(field) } + when String + arel_column(field) { field } + when Proc field.call else field @@ -1064,6 +1066,16 @@ module ActiveRecord end end + def arel_column(field) + field = klass.attribute_alias(field) if klass.attribute_alias?(field) + + if klass.columns_hash.key?(field) && !from_clause.value + arel_attribute(field) + else + yield + end + end + def reverse_sql_order(order_query) if order_query.empty? return [arel_attribute(primary_key).desc] if primary_key |