Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 8036

Show
Ignore:
Timestamp:
10/26/07 05:56:46 (9 months ago)
Author:
bitsweat
Message:

Foxy fixtures. Adapter#disable_referential_integrity. Closes #9981.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/activerecord/CHANGELOG

    r8032 r8036  
    11*SVN* 
     2 
     3* Foxy fixtures, from rathole (http://svn.geeksomnia.com/rathole/trunk/README) 
     4    - stable, autogenerated IDs 
     5    - specify associations (belongs_to, has_one, has_many) by label, not ID 
     6    - specify HABTM associations as inline lists 
     7    - autofill timestamp columns 
     8    - support YAML defaults 
     9    - fixture label interpolation 
     10  Enabled for fixtures that correspond to a model class and don't specify a primary key value.  #9981 [jbarnette] 
    211 
    312* Add docs explaining how to protect all attributes using attr_accessible with no arguments. Closes #9631 [boone, rmm5t] 
  • trunk/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

    r7932 r8036  
    6969      def quote_table_name(name) 
    7070        name 
     71      end 
     72 
     73      # REFERENTIAL INTEGRITY ==================================== 
     74 
     75      # Override to turn off referential integrity while executing +&block+ 
     76      def disable_referential_integrity(&block) 
     77        yield 
    7178      end 
    7279 
  • trunk/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb

    r8015 r8036  
    225225      end 
    226226 
     227      # REFERENTIAL INTEGRITY ==================================== 
     228 
     229      def disable_referential_integrity(&block) #:nodoc: 
     230        old = select_value("SELECT @@FOREIGN_KEY_CHECKS") 
     231 
     232        begin 
     233          update("SET FOREIGN_KEY_CHECKS = 0") 
     234          yield 
     235        ensure 
     236          update("SET FOREIGN_KEY_CHECKS = #{old}") 
     237        end 
     238      end 
    227239 
    228240      # CONNECTION MANAGEMENT ==================================== 
  • trunk/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

    r7801 r8036  
    365365      end 
    366366 
     367      # REFERENTIAL INTEGRITY ==================================== 
     368 
     369      def disable_referential_integrity(&block) #:nodoc: 
     370        execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) 
     371        yield 
     372      ensure 
     373        execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) 
     374      end 
    367375 
    368376      # DATABASE STATEMENTS ====================================== 
  • trunk/activerecord/lib/active_record/fixtures.rb

    r7932 r8036  
    216216#   2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. 
    217217#      Use InnoDB, MaxDB, or NDB instead. 
     218# 
     219# = Advanced YAML Fixtures 
     220# 
     221# YAML fixtures that don't specify an ID get some extra features: 
     222# 
     223# * Stable, autogenerated ID's 
     224# * Label references for associations (belongs_to, has_one, has_many) 
     225# * HABTM associations as inline lists 
     226# * Autofilled timestamp columns 
     227# * Fixture label interpolation 
     228# * Support for YAML defaults 
     229# 
     230# == Stable, autogenerated ID's 
     231# 
     232# Here, have a monkey fixture: 
     233# 
     234#   george: 
     235#     id: 1 
     236#     name: George the Monkey 
     237# 
     238#   reginald: 
     239#     id: 2 
     240#     name: Reginald the Pirate 
     241# 
     242# Each of these fixtures has two unique identifiers: one for the database 
     243# and one for the humans. Why don't we generate the primary key instead? 
     244# Hashing each fixture's label yields a consistent ID: 
     245# 
     246#   george: # generated id: 503576764 
     247#     name: George the Monkey 
     248# 
     249#   reginald: # generated id: 324201669 
     250#     name: Reginald the Pirate 
     251# 
     252# ActiveRecord looks at the fixture's model class, discovers the correct 
     253# primary key, and generates it right before inserting the fixture 
     254# into the database. 
     255# 
     256# The generated ID for a given label is constant, so we can discover 
     257# any fixture's ID without loading anything, as long as we know the label. 
     258# 
     259# == Label references for associations (belongs_to, has_one, has_many) 
     260# 
     261# Specifying foreign keys in fixtures can be very fragile, not to 
     262# mention difficult to read. Since ActiveRecord can figure out the ID of 
     263# and fixture from its label, you can specify FK's by label instead of ID. 
     264# 
     265# === belongs_to 
     266# 
     267# Let's break out some more monkeys and pirates. 
     268# 
     269#   ### in pirates.yml 
     270# 
     271#   reginald: 
     272#     id: 1 
     273#     name: Reginald the Pirate 
     274#     monkey_id: 1 
     275# 
     276#   ### in monkeys.yml 
     277# 
     278#   george: 
     279#     id: 1 
     280#     name: George the Monkey 
     281#     pirate_id: 1 
     282# 
     283# Add a few more monkeys and pirates and break this into multiple files, 
     284# and it gets pretty hard to keep track of what's going on. Let's 
     285# use labels instead of ID's: 
     286# 
     287#   ### in pirates.yml 
     288# 
     289#   reginald: 
     290#     name: Reginald the Pirate 
     291#     monkey: george 
     292# 
     293#   ### in monkeys.yml 
     294# 
     295#   george: 
     296#     name: George the Monkey 
     297#     pirate: reginald 
     298# 
     299# Pow! All is made clear. ActiveRecord reflects on the fixture's model class, 
     300# finds all the +belongs_to+ associations, and allows you to specify 
     301# a target *label* for the *association* (monkey: george) rather than 
     302# a target *id* for the *FK* (monkey_id: 1). 
     303# 
     304# === has_and_belongs_to_many 
     305# 
     306# Time to give our monkey some fruit. 
     307# 
     308#   ### in monkeys.yml 
     309# 
     310#   george: 
     311#     id: 1 
     312#     name: George the Monkey 
     313#     pirate_id: 1 
     314# 
     315#   ### in fruits.yml 
     316# 
     317#   apple: 
     318#     id: 1 
     319#     name: apple 
     320# 
     321#   orange: 
     322#     id: 2 
     323#     name: orange 
     324# 
     325#   grape: 
     326#     id: 3 
     327#     name: grape 
     328# 
     329#   ### in fruits_monkeys.yml 
     330# 
     331#   apple_george: 
     332#     fruit_id: 1 
     333#     monkey_id: 1 
     334# 
     335#   orange_george: 
     336#     fruit_id: 2 
     337#     monkey_id: 1 
     338# 
     339#   grape_george: 
     340#     fruit_id: 3 
     341#     monkey_id: 1 
     342# 
     343# Let's make the HABTM fixture go away. 
     344# 
     345#   ### in monkeys.yml 
     346# 
     347#   george: 
     348#     name: George the Monkey 
     349#     pirate: reginald 
     350#     fruits: apple, orange, grape 
     351# 
     352#   ### in fruits.yml 
     353# 
     354#   apple: 
     355#     name: apple 
     356# 
     357#   orange: 
     358#     name: orange 
     359# 
     360#   grape: 
     361#     name: grape 
     362# 
     363# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits 
     364# on George's fixture, but we could've just as easily specified a list 
     365# of monkeys on each fruit. As with +belongs_to+, ActiveRecord reflects on 
     366# the fixture's model class and discovers the +has_and_belongs_to_many+ 
     367# associations. 
     368# 
     369# == Autofilled timestamp columns 
     370# 
     371# If your table/model specifies any of ActiveRecord's 
     372# standard timestamp columns (created_at, created_on, updated_at, updated_on), 
     373# they will automatically be set to Time.now. 
     374# 
     375# If you've set specific values, they'll be left alone. 
     376# 
     377# == Fixture label interpolation 
     378# 
     379# The label of the current fixture is always available as a column value: 
     380# 
     381#   geeksomnia: 
     382#     name: Geeksomnia's Account 
     383#     subdomain: $LABEL 
     384# 
     385# Also, sometimes (like when porting older join table fixtures) you'll need 
     386# to be able to get ahold of the identifier for a given label. ERB 
     387# to the rescue: 
     388# 
     389#   george_reginald: 
     390#     monkey_id: <%= Fixtures.identify(:reginald) %> 
     391#     pirate_id: <%= Fixtures.identify(:george) %> 
     392# 
     393# == Support for YAML defaults 
     394# 
     395# You probably already know how to use YAML to set and reuse defaults in 
     396# your +database.yml+ file,. You can use the same technique in your fixtures: 
     397# 
     398#   DEFAULTS: &DEFAULTS 
     399#     created_on: <%= 3.weeks.ago.to_s(:db) %> 
     400# 
     401#   first: 
     402#     name: Smurf 
     403#     <<: *DEFAULTS 
     404# 
     405#   second: 
     406#     name: Fraggle 
     407#     <<: *DEFAULTS 
     408# 
     409# Any fixture labeled "DEFAULTS" is safely ignored. 
     410 
    218411class Fixtures < YAML::Omap 
    219412  DEFAULT_FILTER_RE = /\.ya?ml$/ 
     
    280473    unless table_names_to_fetch.empty? 
    281474      ActiveRecord::Base.silence do 
    282         fixtures_map = {} 
    283  
    284         fixtures = table_names_to_fetch.map do |table_name| 
    285           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)) 
    286         end 
    287  
    288         all_loaded_fixtures.update(fixtures_map) 
    289  
    290         connection.transaction(Thread.current['open_transactions'].to_i == 0) do 
    291           fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } 
    292           fixtures.each { |fixture| fixture.insert_fixtures } 
    293  
    294           # Cap primary key sequences to max(pk). 
    295           if connection.respond_to?(:reset_pk_sequence!) 
    296             table_names.each do |table_name| 
    297               connection.reset_pk_sequence!(table_name) 
     475        connection.disable_referential_integrity do 
     476          fixtures_map = {} 
     477 
     478          fixtures = table_names_to_fetch.map do |table_name| 
     479            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)) 
     480          end 
     481 
     482          all_loaded_fixtures.update(fixtures_map) 
     483 
     484          connection.transaction(Thread.current['open_transactions'].to_i == 0) do 
     485            fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } 
     486            fixtures.each { |fixture| fixture.insert_fixtures } 
     487 
     488            # Cap primary key sequences to max(pk). 
     489            if connection.respond_to?(:reset_pk_sequence!) 
     490              table_names.each do |table_name| 
     491                connection.reset_pk_sequence!(table_name) 
     492              end 
    298493            end 
    299494          end 
    300         end 
    301  
    302         cache_fixtures(connection, fixtures) 
     495 
     496          cache_fixtures(connection, fixtures) 
     497        end 
    303498      end 
    304499    end 
    305500    cached_fixtures(connection, table_names) 
     501  end 
     502 
     503  # Returns a consistent identifier for +label+. This will always 
     504  # be a positive integer, and will always be the same for a given 
     505  # label, assuming the same OS, platform, and version of Ruby. 
     506  def self.identify(label) 
     507    label.to_s.hash.abs 
    306508  end 
    307509 
     
    323525 
    324526  def insert_fixtures 
    325     values.each do |fixture| 
     527    now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now 
     528    now = now.to_s(:db) 
     529 
     530    # allow a standard key to be used for doing defaults in YAML 
     531    delete(assoc("DEFAULTS")) 
     532 
     533    # track any join tables we need to insert later 
     534    habtm_fixtures = Hash.new do |h, habtm| 
     535      h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil) 
     536    end 
     537 
     538    each do |label, fixture| 
     539      row = fixture.to_hash 
     540 
     541      if model_class && model_class < ActiveRecord::Base && !row[primary_key_name] 
     542        # fill in timestamp columns if they aren't specified 
     543        timestamp_column_names.each do |name| 
     544          row[name] = now unless row.key?(name) 
     545        end 
     546 
     547        # interpolate the fixture label 
     548        row.each do |key, value| 
     549          row[key] = label if value == "$LABEL" 
     550        end 
     551 
     552        # generate a primary key 
     553        row[primary_key_name] = Fixtures.identify(label) 
     554 
     555        model_class.reflect_on_all_associations.each do |association| 
     556          case association.macro 
     557          when :belongs_to 
     558            if value = row.delete(association.name.to_s) 
     559              fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s 
     560              row[fk_name] = Fixtures.identify(value) 
     561            end 
     562          when :has_and_belongs_to_many 
     563            if (targets = row.delete(association.name.to_s)) 
     564              targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) 
     565              join_fixtures = habtm_fixtures[association] 
     566 
     567              targets.each do |target| 
     568                join_fixtures["#{label}_#{target}"] = Fixture.new( 
     569                  { association.primary_key_name => Fixtures.identify(label), 
     570                    association.association_foreign_key => Fixtures.identify(target) }, nil) 
     571              end 
     572            end 
     573          end 
     574        end 
     575      end 
     576 
    326577      @connection.insert_fixture(fixture, @table_name) 
    327578    end 
     579 
     580    # insert any HABTM join tables we discovered 
     581    habtm_fixtures.values.each do |fixture| 
     582      fixture.delete_existing_fixtures 
     583      fixture.insert_fixtures 
     584    end 
    328585  end 
    329586 
    330587  private 
     588    class HabtmFixtures < ::Fixtures #:nodoc: 
     589      def read_fixture_files; end 
     590    end 
     591 
     592    def model_class 
     593      @model_class ||= @class_name.is_a?(Class) ? 
     594        @class_name : @class_name.constantize rescue nil 
     595    end 
     596 
     597    def primary_key_name 
     598      @primary_key_name ||= model_class && model_class.primary_key 
     599    end 
     600 
     601    def timestamp_column_names 
     602      @timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name| 
     603        column_names.include?(name) 
     604      end 
     605    end 
     606 
     607    def column_names 
     608      @column_names ||= @connection.columns(@table_name).collect(&:name) 
     609    end 
     610 
    331611    def read_fixture_files 
    332612      if File.file?(yaml_file_path) 
  • trunk/activerecord/test/associations_test.rb

    r8030 r8036  
    549549    assert_kind_of Array, client_ary 
    550550    assert_equal 2, client_ary.size 
    551     assert_equal client, client_ary.first 
     551    assert client_ary.include?(client) 
    552552  end 
    553553 
  • trunk/activerecord/test/associations/eager_test.rb

    r8006 r8036  
    253253 
    254254  def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope 
    255     posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id', :include => :comments, :order => 'posts.id DESC', :limit => 2) 
     255    posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :order => 'posts.id DESC', :limit => 2) 
    256256    posts_with_scoped_order = Post.with_scope(:find => {:order => 'posts.id DESC'}) do 
    257       Post.find(:all, :conditions => 'comments.id', :include => :comments, :limit => 2) 
     257      Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :limit => 2) 
    258258    end 
    259259    assert_equal posts_with_explicit_order, posts_with_scoped_order 
  • trunk/activerecord/test/fixtures_test.rb

    r7959 r8036  
    88require 'fixtures/course' 
    99require 'fixtures/category' 
     10require 'fixtures/parrot' 
     11require 'fixtures/pirate' 
     12require 'fixtures/treasure' 
    1013 
    1114class FixturesTest < Test::Unit::TestCase 
     
    447450  end 
    448451end 
     452 
     453class FoxyFixturesTest < Test::Unit::TestCase 
     454  fixtures :parrots, :parrots_pirates, :pirates, :treasures 
     455 
     456  def test_identifies_strings 
     457    assert_equal(Fixtures.identify("foo"), Fixtures.identify("foo")) 
     458    assert_not_equal(Fixtures.identify("foo"), Fixtures.identify("FOO")) 
     459  end 
     460 
     461  def test_identifies_symbols 
     462    assert_equal(Fixtures.identify(:foo), Fixtures.identify(:foo)) 
     463  end 
     464 
     465  TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on) 
     466 
     467  def test_populates_timestamp_columns 
     468    TIMESTAMP_COLUMNS.each do |property| 
     469      assert_not_nil(parrots(:george).send(property), "should set #{property}") 
     470    end 
     471  end 
     472 
     473  def test_populates_all_columns_with_the_same_time 
     474    last = nil 
     475 
     476    TIMESTAMP_COLUMNS.each do |property| 
     477      current = parrots(:george).send(property) 
     478      last ||= current 
     479 
     480      assert_equal(last, current) 
     481      last = current 
     482    end 
     483  end 
     484 
     485  def test_only_populates_columns_that_exist 
     486    assert_not_nil(pirates(:blackbeard).created_on) 
     487    assert_not_nil(pirates(:blackbeard).updated_on) 
     488  end 
     489 
     490  def test_preserves_existing_fixture_data 
     491    assert_equal(2.weeks.ago.to_date, pirates(:redbeard).created_on.to_date) 
     492    assert_equal(2.weeks.ago.to_date, pirates(:redbeard).updated_on.to_date) 
     493  end 
     494 
     495  def test_generates_unique_ids 
     496    assert_not_nil(parrots(:george).id) 
     497    assert_not_equal(parrots(:george).id, parrots(:louis).id) 
     498  end 
     499 
     500  def test_resolves_belongs_to_symbols 
     501    assert_equal(parrots(:george), pirates(:blackbeard).parrot) 
     502  end 
     503 
     504  def test_supports_join_tables 
     505    assert(pirates(:blackbeard).parrots.include?(parrots(:george))) 
     506    assert(pirates(:blackbeard).parrots.include?(parrots(:louis))) 
     507    assert(parrots(:george).pirates.include?(pirates(:blackbeard))) 
     508  end 
     509 
     510  def test_supports_inline_habtm 
     511    assert(parrots(:george).treasures.include?(treasures(:diamond))) 
     512    assert(parrots(:george).treasures.include?(treasures(:sapphire))) 
     513    assert(!parrots(:george).treasures.include?(treasures(:ruby))) 
     514  end 
     515 
     516  def test_supports_yaml_arrays 
     517    assert(parrots(:louis).treasures.include?(treasures(:diamond))) 
     518    assert(parrots(:louis).treasures.include?(treasures(:sapphire))) 
     519  end 
     520 
     521  def test_strips_DEFAULTS_key 
     522    assert_raise(StandardError) { parrots(:DEFAULTS) } 
     523 
     524    # this lets us do YAML defaults and not have an extra fixture entry 
     525    %w(sapphire ruby).each { |t| assert(parrots(:davey).treasures.include?(treasures(t))) } 
     526  end 
     527 
     528  def test_supports_label_interpolation 
     529    assert_equal("frederick", parrots(:frederick).name) 
     530  end 
     531end 
  • trunk/activerecord/test/fixtures/db_definitions/schema.rb

    r8000 r8036  
    296296    t.column :type, :string 
    297297  end 
     298 
     299  create_table :parrots, :force => true do |t| 
     300    t.column :name, :string 
     301    t.column :created_at, :datetime 
     302    t.column :created_on, :datetime 
     303    t.column :updated_at, :datetime 
     304    t.column :updated_on, :datetime 
     305  end 
     306 
     307  create_table :pirates, :force => true do |t| 
     308    t.column :catchphrase, :string 
     309    t.column :parrot_id, :integer 
     310    t.column :created_on, :datetime 
     311    t.column :updated_on, :datetime 
     312  end 
     313 
     314  create_table :parrots_pirates, :id => false, :force => true do |t| 
     315    t.column :parrot_id, :integer 
     316    t.column :pirate_id, :integer 
     317  end 
     318 
     319  create_table :treasures, :force => true do |t| 
     320    t.column :name, :string 
     321  end 
     322 
     323  create_table :parrots_treasures, :id => false, :force => true do |t| 
     324    t.column :parrot_id, :integer 
     325    t.column :treasure_id, :integer 
     326  end 
    298327end