Skip to content
Browse files

Foxy fixtures. Adapter#disable_referential_integrity. Closes #9981.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8036 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 742694e commit 49eafd8c3620bf8e46d21d447fc634a12c8280ab @jeremy jeremy committed Oct 26, 2007
View
9 activerecord/CHANGELOG
@@ -1,5 +1,14 @@
*SVN*
+* Foxy fixtures, from rathole (http://svn.geeksomnia.com/rathole/trunk/README)
+ - stable, autogenerated IDs
+ - specify associations (belongs_to, has_one, has_many) by label, not ID
+ - specify HABTM associations as inline lists
+ - autofill timestamp columns
+ - support YAML defaults
+ - fixture label interpolation
+ Enabled for fixtures that correspond to a model class and don't specify a primary key value. #9981 [jbarnette]
+
* Add docs explaining how to protect all attributes using attr_accessible with no arguments. Closes #9631 [boone, rmm5t]
* Update add_index documentation to use new options api. Closes #9787 [kamal]
View
7 activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -70,6 +70,13 @@ def quote_table_name(name)
name
end
+ # REFERENTIAL INTEGRITY ====================================
+
+ # Override to turn off referential integrity while executing +&block+
+ def disable_referential_integrity(&block)
+ yield
+ end
+
# CONNECTION MANAGEMENT ====================================
# Is this connection active and ready to perform queries?
View
12 activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -224,6 +224,18 @@ def quoted_false
"0"
end
+ # REFERENTIAL INTEGRITY ====================================
+
+ def disable_referential_integrity(&block) #:nodoc:
+ old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
+
+ begin
+ update("SET FOREIGN_KEY_CHECKS = 0")
+ yield
+ ensure
+ update("SET FOREIGN_KEY_CHECKS = #{old}")
+ end
+ end
# CONNECTION MANAGEMENT ====================================
View
8 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -364,6 +364,14 @@ def quoted_date(value) #:nodoc:
end
end
+ # REFERENTIAL INTEGRITY ====================================
+
+ def disable_referential_integrity(&block) #:nodoc:
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
+ yield
+ ensure
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
+ end
# DATABASE STATEMENTS ======================================
View
310 activerecord/lib/active_record/fixtures.rb
@@ -215,6 +215,199 @@ class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
# the results of your transaction until Active Record supports nested transactions or savepoints (in progress.)
# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
# Use InnoDB, MaxDB, or NDB instead.
+#
+# = Advanced YAML Fixtures
+#
+# YAML fixtures that don't specify an ID get some extra features:
+#
+# * Stable, autogenerated ID's
+# * Label references for associations (belongs_to, has_one, has_many)
+# * HABTM associations as inline lists
+# * Autofilled timestamp columns
+# * Fixture label interpolation
+# * Support for YAML defaults
+#
+# == Stable, autogenerated ID's
+#
+# Here, have a monkey fixture:
+#
+# george:
+# id: 1
+# name: George the Monkey
+#
+# reginald:
+# id: 2
+# name: Reginald the Pirate
+#
+# Each of these fixtures has two unique identifiers: one for the database
+# and one for the humans. Why don't we generate the primary key instead?
+# Hashing each fixture's label yields a consistent ID:
+#
+# george: # generated id: 503576764
+# name: George the Monkey
+#
+# reginald: # generated id: 324201669
+# name: Reginald the Pirate
+#
+# ActiveRecord looks at the fixture's model class, discovers the correct
+# primary key, and generates it right before inserting the fixture
+# into the database.
+#
+# The generated ID for a given label is constant, so we can discover
+# any fixture's ID without loading anything, as long as we know the label.
+#
+# == Label references for associations (belongs_to, has_one, has_many)
+#
+# Specifying foreign keys in fixtures can be very fragile, not to
+# mention difficult to read. Since ActiveRecord can figure out the ID of
+# and fixture from its label, you can specify FK's by label instead of ID.
+#
+# === belongs_to
+#
+# Let's break out some more monkeys and pirates.
+#
+# ### in pirates.yml
+#
+# reginald:
+# id: 1
+# name: Reginald the Pirate
+# monkey_id: 1
+#
+# ### in monkeys.yml
+#
+# george:
+# id: 1
+# name: George the Monkey
+# pirate_id: 1
+#
+# Add a few more monkeys and pirates and break this into multiple files,
+# and it gets pretty hard to keep track of what's going on. Let's
+# use labels instead of ID's:
+#
+# ### in pirates.yml
+#
+# reginald:
+# name: Reginald the Pirate
+# monkey: george
+#
+# ### in monkeys.yml
+#
+# george:
+# name: George the Monkey
+# pirate: reginald
+#
+# Pow! All is made clear. ActiveRecord reflects on the fixture's model class,
+# finds all the +belongs_to+ associations, and allows you to specify
+# a target *label* for the *association* (monkey: george) rather than
+# a target *id* for the *FK* (monkey_id: 1).
+#
+# === has_and_belongs_to_many
+#
+# Time to give our monkey some fruit.
+#
+# ### in monkeys.yml
+#
+# george:
+# id: 1
+# name: George the Monkey
+# pirate_id: 1
+#
+# ### in fruits.yml
+#
+# apple:
+# id: 1
+# name: apple
+#
+# orange:
+# id: 2
+# name: orange
+#
+# grape:
+# id: 3
+# name: grape
+#
+# ### in fruits_monkeys.yml
+#
+# apple_george:
+# fruit_id: 1
+# monkey_id: 1
+#
+# orange_george:
+# fruit_id: 2
+# monkey_id: 1
+#
+# grape_george:
+# fruit_id: 3
+# monkey_id: 1
+#
+# Let's make the HABTM fixture go away.
+#
+# ### in monkeys.yml
+#
+# george:
+# name: George the Monkey
+# pirate: reginald
+# fruits: apple, orange, grape
+#
+# ### in fruits.yml
+#
+# apple:
+# name: apple
+#
+# orange:
+# name: orange
+#
+# grape:
+# name: grape
+#
+# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
+# on George's fixture, but we could've just as easily specified a list
+# of monkeys on each fruit. As with +belongs_to+, ActiveRecord reflects on
+# the fixture's model class and discovers the +has_and_belongs_to_many+
+# associations.
+#
+# == Autofilled timestamp columns
+#
+# If your table/model specifies any of ActiveRecord's
+# standard timestamp columns (created_at, created_on, updated_at, updated_on),
+# they will automatically be set to Time.now.
+#
+# If you've set specific values, they'll be left alone.
+#
+# == Fixture label interpolation
+#
+# The label of the current fixture is always available as a column value:
+#
+# geeksomnia:
+# name: Geeksomnia's Account
+# subdomain: $LABEL
+#
+# Also, sometimes (like when porting older join table fixtures) you'll need
+# to be able to get ahold of the identifier for a given label. ERB
+# to the rescue:
+#
+# george_reginald:
+# monkey_id: <%= Fixtures.identify(:reginald) %>
+# pirate_id: <%= Fixtures.identify(:george) %>
+#
+# == Support for YAML defaults
+#
+# You probably already know how to use YAML to set and reuse defaults in
+# your +database.yml+ file,. You can use the same technique in your fixtures:
+#
+# DEFAULTS: &DEFAULTS
+# created_on: <%= 3.weeks.ago.to_s(:db) %>
+#
+# first:
+# name: Smurf
+# <<: *DEFAULTS
+#
+# second:
+# name: Fraggle
+# <<: *DEFAULTS
+#
+# Any fixture labeled "DEFAULTS" is safely ignored.
+
class Fixtures < YAML::Omap
DEFAULT_FILTER_RE = /\.ya?ml$/
@@ -279,32 +472,41 @@ def self.create_fixtures(fixtures_directory, table_names, class_names = {})
unless table_names_to_fetch.empty?
ActiveRecord::Base.silence do
- fixtures_map = {}
+ connection.disable_referential_integrity do
+ fixtures_map = {}
- fixtures = table_names_to_fetch.map do |table_name|
- fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
- end
+ fixtures = table_names_to_fetch.map do |table_name|
+ fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
+ end
- all_loaded_fixtures.update(fixtures_map)
+ all_loaded_fixtures.update(fixtures_map)
- connection.transaction(Thread.current['open_transactions'].to_i == 0) do
- fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
- fixtures.each { |fixture| fixture.insert_fixtures }
+ connection.transaction(Thread.current['open_transactions'].to_i == 0) do
+ fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
+ fixtures.each { |fixture| fixture.insert_fixtures }
- # Cap primary key sequences to max(pk).
- if connection.respond_to?(:reset_pk_sequence!)
- table_names.each do |table_name|
- connection.reset_pk_sequence!(table_name)
+ # Cap primary key sequences to max(pk).
+ if connection.respond_to?(:reset_pk_sequence!)
+ table_names.each do |table_name|
+ connection.reset_pk_sequence!(table_name)
+ end
end
end
- end
- cache_fixtures(connection, fixtures)
+ cache_fixtures(connection, fixtures)
+ end
end
end
cached_fixtures(connection, table_names)
end
+ # Returns a consistent identifier for +label+. This will always
+ # be a positive integer, and will always be the same for a given
+ # label, assuming the same OS, platform, and version of Ruby.
+ def self.identify(label)
+ label.to_s.hash.abs
+ end
+
attr_reader :table_name
def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
@@ -322,12 +524,90 @@ def delete_existing_fixtures
end
def insert_fixtures
- values.each do |fixture|
+ now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
+ now = now.to_s(:db)
+
+ # allow a standard key to be used for doing defaults in YAML
+ delete(assoc("DEFAULTS"))
+
+ # track any join tables we need to insert later
+ habtm_fixtures = Hash.new do |h, habtm|
+ h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil)
+ end
+
+ each do |label, fixture|
+ row = fixture.to_hash
+
+ if model_class && model_class < ActiveRecord::Base && !row[primary_key_name]
+ # fill in timestamp columns if they aren't specified
+ timestamp_column_names.each do |name|
+ row[name] = now unless row.key?(name)
+ end
+
+ # interpolate the fixture label
+ row.each do |key, value|
+ row[key] = label if value == "$LABEL"
+ end
+
+ # generate a primary key
+ row[primary_key_name] = Fixtures.identify(label)
+
+ model_class.reflect_on_all_associations.each do |association|
+ case association.macro
+ when :belongs_to
+ if value = row.delete(association.name.to_s)
+ fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
+ row[fk_name] = Fixtures.identify(value)
+ end
+ when :has_and_belongs_to_many
+ if (targets = row.delete(association.name.to_s))
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
+ join_fixtures = habtm_fixtures[association]
+
+ targets.each do |target|
+ join_fixtures["#{label}_#{target}"] = Fixture.new(
+ { association.primary_key_name => Fixtures.identify(label),
+ association.association_foreign_key => Fixtures.identify(target) }, nil)
+ end
+ end
+ end
+ end
+ end
+
@connection.insert_fixture(fixture, @table_name)
end
+
+ # insert any HABTM join tables we discovered
+ habtm_fixtures.values.each do |fixture|
+ fixture.delete_existing_fixtures
+ fixture.insert_fixtures
+ end
end
private
+ class HabtmFixtures < ::Fixtures #:nodoc:
+ def read_fixture_files; end
+ end
+
+ def model_class
+ @model_class ||= @class_name.is_a?(Class) ?
+ @class_name : @class_name.constantize rescue nil
+ end
+
+ def primary_key_name
+ @primary_key_name ||= model_class && model_class.primary_key
+ end
+
+ def timestamp_column_names
+ @timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name|
+ column_names.include?(name)
+ end
+ end
+
+ def column_names
+ @column_names ||= @connection.columns(@table_name).collect(&:name)
+ end
+
def read_fixture_files
if File.file?(yaml_file_path)
read_yaml_fixture_files
View
4 activerecord/test/associations/eager_test.rb
@@ -252,9 +252,9 @@ def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the
end
def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope
- posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id', :include => :comments, :order => 'posts.id DESC', :limit => 2)
+ posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :order => 'posts.id DESC', :limit => 2)
posts_with_scoped_order = Post.with_scope(:find => {:order => 'posts.id DESC'}) do
- Post.find(:all, :conditions => 'comments.id', :include => :comments, :limit => 2)
+ Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :limit => 2)
end
assert_equal posts_with_explicit_order, posts_with_scoped_order
end
View
2 activerecord/test/associations_test.rb
@@ -548,7 +548,7 @@ def test_find_string_ids_when_using_finder_sql
client_ary = firm.clients_using_finder_sql.find("2", "3")
assert_kind_of Array, client_ary
assert_equal 2, client_ary.size
- assert_equal client, client_ary.first
+ assert client_ary.include?(client)
end
def test_find_all
View
29 activerecord/test/fixtures/db_definitions/schema.rb
@@ -295,4 +295,33 @@ def create_table(*args, &block)
t.column :city, :string, :null => false
t.column :type, :string
end
+
+ create_table :parrots, :force => true do |t|
+ t.column :name, :string
+ t.column :created_at, :datetime
+ t.column :created_on, :datetime
+ t.column :updated_at, :datetime
+ t.column :updated_on, :datetime
+ end
+
+ create_table :pirates, :force => true do |t|
+ t.column :catchphrase, :string
+ t.column :parrot_id, :integer
+ t.column :created_on, :datetime
+ t.column :updated_on, :datetime
+ end
+
+ create_table :parrots_pirates, :id => false, :force => true do |t|
+ t.column :parrot_id, :integer
+ t.column :pirate_id, :integer
+ end
+
+ create_table :treasures, :force => true do |t|
+ t.column :name, :string
+ end
+
+ create_table :parrots_treasures, :id => false, :force => true do |t|
+ t.column :parrot_id, :integer
+ t.column :treasure_id, :integer
+ end
end
View
4 activerecord/test/fixtures/parrot.rb
@@ -0,0 +1,4 @@
+class Parrot < ActiveRecord::Base
+ has_and_belongs_to_many :pirates
+ has_and_belongs_to_many :treasures
+end
View
16 activerecord/test/fixtures/parrots.yml
@@ -0,0 +1,16 @@
+george:
+ name: "Curious George"
+ treasures: diamond, sapphire
+
+louis:
+ name: "King Louis"
+ treasures: [diamond, sapphire]
+
+frederick:
+ name: $LABEL
+
+DEFAULTS: &DEFAULTS
+ treasures: sapphire, ruby
+
+davey:
+ <<: *DEFAULTS
View
7 activerecord/test/fixtures/parrots_pirates.yml
@@ -0,0 +1,7 @@
+george_blackbeard:
+ parrot_id: <%= Fixtures.identify(:george) %>
+ pirate_id: <%= Fixtures.identify(:blackbeard) %>
+
+louis_blackbeard:
+ parrot_id: <%= Fixtures.identify(:louis) %>
+ pirate_id: <%= Fixtures.identify(:blackbeard) %>
View
4 activerecord/test/fixtures/pirate.rb
@@ -0,0 +1,4 @@
+class Pirate < ActiveRecord::Base
+ belongs_to :parrot
+ has_and_belongs_to_many :parrots
+end
View
9 activerecord/test/fixtures/pirates.yml
@@ -0,0 +1,9 @@
+blackbeard:
+ catchphrase: "Yar."
+ parrot: george
+
+redbeard:
+ catchphrase: "Avast!"
+ parrot: louis
+ created_on: <%= 2.weeks.ago.to_s(:db) %>
+ updated_on: <%= 2.weeks.ago.to_s(:db) %>
View
3 activerecord/test/fixtures/treasure.rb
@@ -0,0 +1,3 @@
+class Treasure < ActiveRecord::Base
+ has_and_belongs_to_many :parrots
+end
View
8 activerecord/test/fixtures/treasures.yml
@@ -0,0 +1,8 @@
+diamond:
+ name: $LABEL
+
+sapphire:
+ name: $LABEL
+
+ruby:
+ name: $LABEL
View
83 activerecord/test/fixtures_test.rb
@@ -7,6 +7,9 @@
require 'fixtures/joke'
require 'fixtures/course'
require 'fixtures/category'
+require 'fixtures/parrot'
+require 'fixtures/pirate'
+require 'fixtures/treasure'
class FixturesTest < Test::Unit::TestCase
self.use_instantiated_fixtures = true
@@ -446,3 +449,83 @@ def test_cache
assert_equal 'Welcome to the weblog', posts(:welcome).title
end
end
+
+class FoxyFixturesTest < Test::Unit::TestCase
+ fixtures :parrots, :parrots_pirates, :pirates, :treasures
+
+ def test_identifies_strings
+ assert_equal(Fixtures.identify("foo"), Fixtures.identify("foo"))
+ assert_not_equal(Fixtures.identify("foo"), Fixtures.identify("FOO"))
+ end
+
+ def test_identifies_symbols
+ assert_equal(Fixtures.identify(:foo), Fixtures.identify(:foo))
+ end
+
+ TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on)
+
+ def test_populates_timestamp_columns
+ TIMESTAMP_COLUMNS.each do |property|
+ assert_not_nil(parrots(:george).send(property), "should set #{property}")
+ end
+ end
+
+ def test_populates_all_columns_with_the_same_time
+ last = nil
+
+ TIMESTAMP_COLUMNS.each do |property|
+ current = parrots(:george).send(property)
+ last ||= current
+
+ assert_equal(last, current)
+ last = current
+ end
+ end
+
+ def test_only_populates_columns_that_exist
+ assert_not_nil(pirates(:blackbeard).created_on)
+ assert_not_nil(pirates(:blackbeard).updated_on)
+ end
+
+ def test_preserves_existing_fixture_data
+ assert_equal(2.weeks.ago.to_date, pirates(:redbeard).created_on.to_date)
+ assert_equal(2.weeks.ago.to_date, pirates(:redbeard).updated_on.to_date)
+ end
+
+ def test_generates_unique_ids
+ assert_not_nil(parrots(:george).id)
+ assert_not_equal(parrots(:george).id, parrots(:louis).id)
+ end
+
+ def test_resolves_belongs_to_symbols
+ assert_equal(parrots(:george), pirates(:blackbeard).parrot)
+ end
+
+ def test_supports_join_tables
+ assert(pirates(:blackbeard).parrots.include?(parrots(:george)))
+ assert(pirates(:blackbeard).parrots.include?(parrots(:louis)))
+ assert(parrots(:george).pirates.include?(pirates(:blackbeard)))
+ end
+
+ def test_supports_inline_habtm
+ assert(parrots(:george).treasures.include?(treasures(:diamond)))
+ assert(parrots(:george).treasures.include?(treasures(:sapphire)))
+ assert(!parrots(:george).treasures.include?(treasures(:ruby)))
+ end
+
+ def test_supports_yaml_arrays
+ assert(parrots(:louis).treasures.include?(treasures(:diamond)))
+ assert(parrots(:louis).treasures.include?(treasures(:sapphire)))
+ end
+
+ def test_strips_DEFAULTS_key
+ assert_raise(StandardError) { parrots(:DEFAULTS) }
+
+ # this lets us do YAML defaults and not have an extra fixture entry
+ %w(sapphire ruby).each { |t| assert(parrots(:davey).treasures.include?(treasures(t))) }
+ end
+
+ def test_supports_label_interpolation
+ assert_equal("frederick", parrots(:frederick).name)
+ end
+end

0 comments on commit 49eafd8

Please sign in to comment.
Something went wrong with that request. Please try again.