aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorEileen M. Uchitelle <eileencodes@users.noreply.github.com>2018-10-07 07:57:26 -0500
committerGitHub <noreply@github.com>2018-10-07 07:57:26 -0500
commit651d8743a634837d47070503fde39e97df2e54d0 (patch)
treec4db90c7f197066101d766064e9fda87163d8f97 /activerecord
parentf8a3935ba17c5f99dafe8d9c92fd1de5936abca6 (diff)
parentcfeabbfdcbc27e78efd9e183ba74a2fc9a7a69a0 (diff)
downloadrails-651d8743a634837d47070503fde39e97df2e54d0.tar.gz
rails-651d8743a634837d47070503fde39e97df2e54d0.tar.bz2
rails-651d8743a634837d47070503fde39e97df2e54d0.zip
Merge pull request #34076 from gmcgibbon/fixtures_refactor
Fixtures refactor
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/lib/active_record/fixture_set/model_metadata.rb40
-rw-r--r--activerecord/lib/active_record/fixture_set/table_row.rb79
-rw-r--r--activerecord/lib/active_record/fixture_set/table_rows.rb129
-rw-r--r--activerecord/lib/active_record/fixtures.rb371
4 files changed, 376 insertions, 243 deletions
diff --git a/activerecord/lib/active_record/fixture_set/model_metadata.rb b/activerecord/lib/active_record/fixture_set/model_metadata.rb
new file mode 100644
index 0000000000..edc03939c8
--- /dev/null
+++ b/activerecord/lib/active_record/fixture_set/model_metadata.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class FixtureSet
+ class ModelMetadata # :nodoc:
+ def initialize(model_class, table_name)
+ @model_class = model_class
+ @table_name = table_name
+ end
+
+ def primary_key_name
+ @primary_key_name ||= @model_class && @model_class.primary_key
+ end
+
+ def primary_key_type
+ @primary_key_type ||= @model_class && @model_class.type_for_attribute(@model_class.primary_key).type
+ end
+
+ def has_primary_key_column?
+ @has_primary_key_column ||= primary_key_name &&
+ @model_class.columns.any? { |col| col.name == primary_key_name }
+ end
+
+ def timestamp_column_names
+ @timestamp_column_names ||=
+ %w(created_at created_on updated_at updated_on) & column_names
+ end
+
+ def inheritance_column_name
+ @inheritance_column_name ||= @model_class && @model_class.inheritance_column
+ end
+
+ private
+
+ def column_names
+ @column_names ||= @model_class.connection.columns(@table_name).collect(&:name)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixture_set/table_row.rb b/activerecord/lib/active_record/fixture_set/table_row.rb
new file mode 100644
index 0000000000..5f72c1df38
--- /dev/null
+++ b/activerecord/lib/active_record/fixture_set/table_row.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class FixtureSet
+ class TableRow # :nodoc:
+ def initialize(fixture, table_rows:, label:, now:)
+ @table_rows = table_rows
+ @label = label
+ @now = now
+ @row = fixture.to_hash
+ fill_row_model_attributes
+ end
+
+ def to_hash
+ @row
+ end
+
+ private
+
+ def model_metadata
+ @table_rows.model_metadata
+ end
+
+ def model_class
+ @table_rows.model_class
+ end
+
+ def fill_row_model_attributes
+ return unless model_class
+ fill_timestamps
+ interpolate_label
+ generate_primary_key
+ resolve_enums
+ @table_rows.resolve_sti_reflections(@row)
+ end
+
+ def reflection_class
+ @reflection_class ||= if @row.include?(model_metadata.inheritance_column_name)
+ @row[model_metadata.inheritance_column_name].constantize rescue model_class
+ else
+ model_class
+ end
+ end
+
+ def fill_timestamps
+ # fill in timestamp columns if they aren't specified and the model is set to record_timestamps
+ if model_class.record_timestamps
+ model_metadata.timestamp_column_names.each do |c_name|
+ @row[c_name] = @now unless @row.key?(c_name)
+ end
+ end
+ end
+
+ def interpolate_label
+ # interpolate the fixture label
+ @row.each do |key, value|
+ @row[key] = value.gsub("$LABEL", @label.to_s) if value.is_a?(String)
+ end
+ end
+
+ def generate_primary_key
+ # generate a primary key if necessary
+ if model_metadata.has_primary_key_column? && !@row.include?(model_metadata.primary_key_name)
+ @row[model_metadata.primary_key_name] = ActiveRecord::FixtureSet.identify(
+ @label, model_metadata.primary_key_type
+ )
+ end
+ end
+
+ def resolve_enums
+ model_class.defined_enums.each do |name, values|
+ if @row.include?(name)
+ @row[name] = values.fetch(@row[name], @row[name])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixture_set/table_rows.rb b/activerecord/lib/active_record/fixture_set/table_rows.rb
new file mode 100644
index 0000000000..e8335a2e10
--- /dev/null
+++ b/activerecord/lib/active_record/fixture_set/table_rows.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require "active_record/fixture_set/table_row"
+require "active_record/fixture_set/model_metadata"
+
+module ActiveRecord
+ class FixtureSet
+ class TableRows # :nodoc:
+ class ReflectionProxy # :nodoc:
+ def initialize(association)
+ @association = association
+ end
+
+ def join_table
+ @association.join_table
+ end
+
+ def name
+ @association.name
+ end
+
+ def primary_key_type
+ @association.klass.type_for_attribute(@association.klass.primary_key).type
+ end
+ end
+
+ class HasManyThroughProxy < ReflectionProxy # :nodoc:
+ def rhs_key
+ @association.foreign_key
+ end
+
+ def lhs_key
+ @association.through_reflection.foreign_key
+ end
+
+ def join_table
+ @association.through_reflection.table_name
+ end
+ end
+
+ def initialize(table_name, model_class:, fixtures:, config:)
+ @table_name = table_name
+ @model_class = model_class
+
+ # track any join tables we need to insert later
+ @tables = Hash.new { |h, table| h[table] = [] }
+
+ build_table_rows_from(fixtures, config)
+ end
+
+ attr_reader :table_name, :model_class
+
+ def to_hash
+ @tables.transform_values { |rows| rows.map(&:to_hash) }
+ end
+
+ def model_metadata
+ @model_metadata ||= ModelMetadata.new(model_class, table_name)
+ end
+
+ def resolve_sti_reflections(row)
+ # If STI is used, find the correct subclass for association reflection
+ reflection_class = reflection_class_for(row)
+
+ reflection_class._reflections.each_value do |association|
+ case association.macro
+ when :belongs_to
+ # Do not replace association name with association foreign key if they are named the same
+ fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
+
+ if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
+ if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
+ # support polymorphic belongs_to as "label (Type)"
+ row[association.foreign_type] = $1
+ end
+
+ fk_type = reflection_class.type_for_attribute(fk_name).type
+ row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
+ end
+ when :has_many
+ if association.options[:through]
+ add_join_records(row, HasManyThroughProxy.new(association))
+ end
+ end
+ end
+ end
+
+ private
+
+ def build_table_rows_from(fixtures, config)
+ now = config.default_timezone == :utc ? Time.now.utc : Time.now
+
+ @tables[table_name] = fixtures.map do |label, fixture|
+ TableRow.new(
+ fixture,
+ table_rows: self,
+ label: label,
+ now: now,
+ )
+ end
+ end
+
+ def reflection_class_for(row)
+ if row.include?(model_metadata.inheritance_column_name)
+ row[model_metadata.inheritance_column_name].constantize rescue model_class
+ else
+ model_class
+ end
+ end
+
+ def add_join_records(row, association)
+ # This is the case when the join table has no fixtures file
+ if (targets = row.delete(association.name.to_s))
+ table_name = association.join_table
+ column_type = association.primary_key_type
+ lhs_key = association.lhs_key
+ rhs_key = association.rhs_key
+
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
+ joins = targets.map do |target|
+ { lhs_key => row[model_metadata.primary_key_name],
+ rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
+ end
+ @tables[table_name].concat(joins)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index e697c30bb6..b090c76a38 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -8,6 +8,7 @@ require "active_support/dependencies"
require "active_support/core_ext/digest/uuid"
require "active_record/fixture_set/file"
require "active_record/fixture_set/render_context"
+require "active_record/fixture_set/table_rows"
require "active_record/test_fixtures"
require "active_record/errors"
@@ -442,60 +443,6 @@ module ActiveRecord
@@all_cached_fixtures = Hash.new { |h, k| h[k] = {} }
- def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
- config.pluralize_table_names ?
- fixture_set_name.singularize.camelize :
- fixture_set_name.camelize
- end
-
- def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
- "#{ config.table_name_prefix }"\
- "#{ fixture_set_name.tr('/', '_') }"\
- "#{ config.table_name_suffix }".to_sym
- end
-
- def self.reset_cache
- @@all_cached_fixtures.clear
- end
-
- def self.cache_for_connection(connection)
- @@all_cached_fixtures[connection]
- end
-
- def self.fixture_is_cached?(connection, table_name)
- cache_for_connection(connection)[table_name]
- end
-
- def self.cached_fixtures(connection, keys_to_fetch = nil)
- if keys_to_fetch
- cache_for_connection(connection).values_at(*keys_to_fetch)
- else
- cache_for_connection(connection).values
- end
- end
-
- def self.cache_fixtures(connection, fixtures_map)
- cache_for_connection(connection).update(fixtures_map)
- end
-
- def self.instantiate_fixtures(object, fixture_set, load_instances = true)
- if load_instances
- fixture_set.each do |fixture_name, fixture|
- begin
- object.instance_variable_set "@#{fixture_name}", fixture.find
- rescue FixtureClassNotFound
- nil
- end
- end
- end
- end
-
- def self.instantiate_all_loaded_fixtures(object, load_instances = true)
- all_loaded_fixtures.each_value do |fixture_set|
- instantiate_fixtures(object, fixture_set, load_instances)
- end
- end
-
cattr_accessor :all_loaded_fixtures, default: {}
class ClassCache
@@ -504,14 +451,16 @@ module ActiveRecord
@config = config
# Remove string values that aren't constants or subclasses of AR
- @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) }
+ @class_names.delete_if do |klass_name, klass|
+ !insert_class(@class_names, klass_name, klass)
+ end
end
def [](fs_name)
- @class_names.fetch(fs_name) {
+ @class_names.fetch(fs_name) do
klass = default_fixture_model(fs_name, @config).safe_constantize
insert_class(@class_names, fs_name, klass)
- }
+ end
end
private
@@ -530,38 +479,129 @@ module ActiveRecord
end
end
- def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
- fixture_set_names = Array(fixture_set_names).map(&:to_s)
- class_names = ClassCache.new class_names, config
+ class << self
+ def default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ config.pluralize_table_names ?
+ fixture_set_name.singularize.camelize :
+ fixture_set_name.camelize
+ end
+
+ def default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ "#{ config.table_name_prefix }"\
+ "#{ fixture_set_name.tr('/', '_') }"\
+ "#{ config.table_name_suffix }".to_sym
+ end
+
+ def reset_cache
+ @@all_cached_fixtures.clear
+ end
- # FIXME: Apparently JK uses this.
- connection = block_given? ? yield : ActiveRecord::Base.connection
+ def cache_for_connection(connection)
+ @@all_cached_fixtures[connection]
+ end
- files_to_read = fixture_set_names.reject { |fs_name|
- fixture_is_cached?(connection, fs_name)
- }
+ def fixture_is_cached?(connection, table_name)
+ cache_for_connection(connection)[table_name]
+ end
- unless files_to_read.empty?
- fixtures_map = {}
+ def cached_fixtures(connection, keys_to_fetch = nil)
+ if keys_to_fetch
+ cache_for_connection(connection).values_at(*keys_to_fetch)
+ else
+ cache_for_connection(connection).values
+ end
+ end
+
+ def cache_fixtures(connection, fixtures_map)
+ cache_for_connection(connection).update(fixtures_map)
+ end
+
+ def instantiate_fixtures(object, fixture_set, load_instances = true)
+ return unless load_instances
+ fixture_set.each do |fixture_name, fixture|
+ begin
+ object.instance_variable_set "@#{fixture_name}", fixture.find
+ rescue FixtureClassNotFound
+ nil
+ end
+ end
+ end
+
+ def instantiate_all_loaded_fixtures(object, load_instances = true)
+ all_loaded_fixtures.each_value do |fixture_set|
+ instantiate_fixtures(object, fixture_set, load_instances)
+ end
+ end
+
+ def create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
+ fixture_set_names = Array(fixture_set_names).map(&:to_s)
+ class_names = ClassCache.new class_names, config
+
+ # FIXME: Apparently JK uses this.
+ connection = block_given? ? yield : ActiveRecord::Base.connection
+
+ fixture_files_to_read = fixture_set_names.reject do |fs_name|
+ fixture_is_cached?(connection, fs_name)
+ end
+
+ if fixture_files_to_read.any?
+ fixtures_map = read_and_insert(
+ fixtures_directory,
+ fixture_files_to_read,
+ class_names,
+ connection,
+ )
+ cache_fixtures(connection, fixtures_map)
+ end
+ cached_fixtures(connection, fixture_set_names)
+ end
+
+ # Returns a consistent, platform-independent identifier for +label+.
+ # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
+ def identify(label, column_type = :integer)
+ if column_type == :uuid
+ Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
+ else
+ Zlib.crc32(label.to_s) % MAX_ID
+ end
+ end
- fixture_sets = files_to_read.map do |fs_name|
- klass = class_names[fs_name]
- conn = klass ? klass.connection : connection
- fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
+ # Superclass for the evaluation contexts used by ERB fixtures.
+ def context_class
+ @context_class ||= Class.new
+ end
+
+ private
+
+ def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc:
+ fixtures_map = {}
+ fixture_sets = fixture_files.map do |fixture_set_name|
+ klass = class_names[fixture_set_name]
+ conn = klass&.connection || connection
+ fixtures_map[fixture_set_name] = new( # ActiveRecord::FixtureSet.new
conn,
- fs_name,
+ fixture_set_name,
klass,
- ::File.join(fixtures_directory, fs_name))
+ ::File.join(fixtures_directory, fixture_set_name)
+ )
end
+ update_all_loaded_fixtures(fixtures_map)
- update_all_loaded_fixtures fixtures_map
- fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection }
+ insert(fixture_sets, connection)
+
+ fixtures_map
+ end
+
+ def insert(fixture_sets, connection) # :nodoc:
+ fixture_sets_by_connection = fixture_sets.group_by do |fixture_set|
+ fixture_set.model_class&.connection || connection
+ end
fixture_sets_by_connection.each do |conn, set|
table_rows_for_connection = Hash.new { |h, k| h[k] = [] }
- set.each do |fs|
- fs.table_rows.each do |table, rows|
+ set.each do |fixture_set|
+ fixture_set.table_rows.each do |table, rows|
table_rows_for_connection[table].unshift(*rows)
end
end
@@ -572,31 +612,13 @@ module ActiveRecord
set.each { |fs| conn.reset_pk_sequence!(fs.table_name) }
end
end
-
- cache_fixtures(connection, fixtures_map)
end
- cached_fixtures(connection, fixture_set_names)
- end
- # Returns a consistent, platform-independent identifier for +label+.
- # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
- def self.identify(label, column_type = :integer)
- if column_type == :uuid
- Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
- else
- Zlib.crc32(label.to_s) % MAX_ID
+ def update_all_loaded_fixtures(fixtures_map) # :nodoc:
+ all_loaded_fixtures.update(fixtures_map)
end
end
- # Superclass for the evaluation contexts used by ERB fixtures.
- def self.context_class
- @context_class ||= Class.new
- end
-
- def self.update_all_loaded_fixtures(fixtures_map) # :nodoc:
- all_loaded_fixtures.update(fixtures_map)
- end
-
attr_reader :table_name, :name, :fixtures, :model_class, :config
def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
@@ -634,152 +656,18 @@ module ActiveRecord
# Returns a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
- now = config.default_timezone == :utc ? Time.now.utc : Time.now
-
# allow a standard key to be used for doing defaults in YAML
fixtures.delete("DEFAULTS")
- # track any join tables we need to insert later
- rows = Hash.new { |h, table| h[table] = [] }
-
- rows[table_name] = fixtures.map do |label, fixture|
- row = fixture.to_hash
-
- if model_class
- # fill in timestamp columns if they aren't specified and the model is set to record_timestamps
- if model_class.record_timestamps
- timestamp_column_names.each do |c_name|
- row[c_name] = now unless row.key?(c_name)
- end
- end
-
- # interpolate the fixture label
- row.each do |key, value|
- row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String)
- end
-
- # generate a primary key if necessary
- if has_primary_key_column? && !row.include?(primary_key_name)
- row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
- end
-
- # Resolve enums
- model_class.defined_enums.each do |name, values|
- if row.include?(name)
- row[name] = values.fetch(row[name], row[name])
- end
- end
-
- # If STI is used, find the correct subclass for association reflection
- reflection_class =
- if row.include?(inheritance_column_name)
- row[inheritance_column_name].constantize rescue model_class
- else
- model_class
- end
-
- reflection_class._reflections.each_value do |association|
- case association.macro
- when :belongs_to
- # Do not replace association name with association foreign key if they are named the same
- fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
-
- if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
- if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
- # support polymorphic belongs_to as "label (Type)"
- row[association.foreign_type] = $1
- end
-
- fk_type = reflection_class.type_for_attribute(fk_name).type
- row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
- end
- when :has_many
- if association.options[:through]
- add_join_records(rows, row, HasManyThroughProxy.new(association))
- end
- end
- end
- end
-
- row
- end
- rows
- end
-
- class ReflectionProxy # :nodoc:
- def initialize(association)
- @association = association
- end
-
- def join_table
- @association.join_table
- end
-
- def name
- @association.name
- end
-
- def primary_key_type
- @association.klass.type_for_attribute(@association.klass.primary_key).type
- end
- end
-
- class HasManyThroughProxy < ReflectionProxy # :nodoc:
- def rhs_key
- @association.foreign_key
- end
-
- def lhs_key
- @association.through_reflection.foreign_key
- end
-
- def join_table
- @association.through_reflection.table_name
- end
+ TableRows.new(
+ table_name,
+ model_class: model_class,
+ fixtures: fixtures,
+ config: config,
+ ).to_hash
end
private
- def primary_key_name
- @primary_key_name ||= model_class && model_class.primary_key
- end
-
- def primary_key_type
- @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type
- end
-
- def add_join_records(rows, row, association)
- # This is the case when the join table has no fixtures file
- if (targets = row.delete(association.name.to_s))
- table_name = association.join_table
- column_type = association.primary_key_type
- lhs_key = association.lhs_key
- rhs_key = association.rhs_key
-
- targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- rows[table_name].concat targets.map { |target|
- { lhs_key => row[primary_key_name],
- rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
- }
- end
- end
-
- def has_primary_key_column?
- @has_primary_key_column ||= primary_key_name &&
- model_class.columns.any? { |c| c.name == primary_key_name }
- end
-
- def timestamp_column_names
- @timestamp_column_names ||=
- %w(created_at created_on updated_at updated_on) & column_names
- end
-
- def inheritance_column_name
- @inheritance_column_name ||= model_class && model_class.inheritance_column
- end
-
- def column_names
- @column_names ||= @connection.columns(@table_name).collect(&:name)
- end
def model_class=(class_name)
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
@@ -843,12 +731,9 @@ module ActiveRecord
alias :to_hash :fixture
def find
- if model_class
- model_class.unscoped do
- model_class.find(fixture[model_class.primary_key])
- end
- else
- raise FixtureClassNotFound, "No class attached to find."
+ raise FixtureClassNotFound, "No class attached to find." unless model_class
+ model_class.unscoped do
+ model_class.find(fixture[model_class.primary_key])
end
end
end