diff --git a/README.rdoc b/README.rdoc index 7b26acf..8d3c15b 100644 --- a/README.rdoc +++ b/README.rdoc @@ -19,7 +19,6 @@ pure ruby. By default a relationship will PROTECT its children. - === Cardinality Notes * 1:1 * Applicable constraints: [:set_nil, :skip, :protect, :destroy] @@ -31,13 +30,24 @@ By default a relationship will PROTECT its children. * Applicable constraints: [:skip, :protect, :destroy, :destroy!] +=== Deferrability + +Optionally, a constraint may be made deferrable. This only affects the generated +DDL, not any behavior in ruby. Set deferrability by adding the option ++:constraint_deferrable+ to the relationship with one of these values: + + - false constraint is always evaluated immediately; it may not be deferred + (default) + - true or :initially_deferred constraint may be deferred and is deferred by default + - :initially_immediate constraint may be deferred but is immediate by default + === Examples # 1:M Example class Post has n, :comments # equivalent to: - # has n, :comments, :constraint => :protect + # has n, :comments, :constraint => :protect, :constraint_deferrable => false end # M:M Example diff --git a/lib/data_mapper/constraints/adapters/do_adapter.rb b/lib/data_mapper/constraints/adapters/do_adapter.rb index b674a3f..7ca13f1 100644 --- a/lib/data_mapper/constraints/adapters/do_adapter.rb +++ b/lib/data_mapper/constraints/adapters/do_adapter.rb @@ -57,12 +57,21 @@ def create_relationship_constraint(relationship) return false if constraint_type.nil? + deferrable_control = + case relationship.inverse.constraint_deferrable + when true, :initially_deferred + 'DEFERRABLE INITIALLY DEFERRED' + when :initially_immediate + 'DEFERRABLE INITIALLY IMMEDIATE' + end + source_keys = relationship.source_key.map { |p| property_to_column_name(p, false) } target_keys = relationship.target_key.map { |p| property_to_column_name(p, false) } create_constraints_statement = create_constraints_statement( constraint_name, constraint_type, + deferrable_control, source_storage_name, source_keys, target_storage_name, @@ -128,6 +137,8 @@ module SQL # name of the foreign key constraint # @param [String] constraint_type # type of constraint to ALTER source_storage_name with + # @param [String,nil] deferrable + # the SQL string indicating the deferrablity of the constraint # @param [String] source_storage_name # name of table to ALTER with constraint # @param [Array(String)] source_keys @@ -141,7 +152,7 @@ module SQL # SQL DDL Statement to create a constraint # # @api private - def create_constraints_statement(constraint_name, constraint_type, source_storage_name, source_keys, target_storage_name, target_keys) + def create_constraints_statement(constraint_name, constraint_type, deferrable, source_storage_name, source_keys, target_storage_name, target_keys) DataMapper::Ext::String.compress_lines(<<-SQL) ALTER TABLE #{quote_name(source_storage_name)} ADD CONSTRAINT #{quote_name(constraint_name)} @@ -149,6 +160,7 @@ def create_constraints_statement(constraint_name, constraint_type, source_storag REFERENCES #{quote_name(target_storage_name)} (#{target_keys.join(', ')}) ON DELETE #{constraint_type} ON UPDATE #{constraint_type} + #{deferrable} SQL end diff --git a/lib/data_mapper/constraints/adapters/oracle_adapter.rb b/lib/data_mapper/constraints/adapters/oracle_adapter.rb index ef903fa..af4c48b 100644 --- a/lib/data_mapper/constraints/adapters/oracle_adapter.rb +++ b/lib/data_mapper/constraints/adapters/oracle_adapter.rb @@ -28,15 +28,13 @@ def constraint_exists?(storage_name, constraint_name) # @see DataMapper::Constraints::Adapters::DataObjectsAdapter#create_constraints_statement # # @api private - # - # TODO: is it desirable to always set `INITIALLY DEFERRED DEFERRABLE`? - def create_constraints_statement(storage_name, constraint_name, constraint_type, foreign_keys, reference_storage_name, reference_keys) + def create_constraints_statement(storage_name, constraint_name, constraint_type, deferrable, foreign_keys, reference_storage_name, reference_keys) DataMapper::Ext::String.compress_lines(<<-SQL) ALTER TABLE #{quote_name(storage_name)} ADD CONSTRAINT #{quote_name(constraint_name)} FOREIGN KEY (#{foreign_keys.join(', ')}) REFERENCES #{quote_name(reference_storage_name)} (#{reference_keys.join(', ')}) - INITIALLY DEFERRED DEFERRABLE + #{deferrable} SQL end diff --git a/lib/data_mapper/constraints/relationship/one_to_many.rb b/lib/data_mapper/constraints/relationship/one_to_many.rb index d9ef308..42c044a 100644 --- a/lib/data_mapper/constraints/relationship/one_to_many.rb +++ b/lib/data_mapper/constraints/relationship/one_to_many.rb @@ -4,6 +4,7 @@ module Relationship module OneToMany attr_reader :constraint + attr_reader :constraint_deferrable # @api private def enforce_destroy_constraint(resource) @@ -28,7 +29,7 @@ def enforce_destroy_constraint(resource) private ## - # Adds the delete constraint options to a relationship + # Adds the delete & defer constraint options to a relationship # # @param params [*ARGS] Arguments passed to Relationship#initialize # @@ -43,6 +44,7 @@ def initialize(*args) def set_constraint @constraint = @options.fetch(:constraint, :protect) || :skip + @constraint_deferrable = @options.fetch(:constraint_deferrable, false) end # Checks that the constraint type is appropriate to the relationship @@ -69,6 +71,11 @@ def assert_valid_constraint unless VALID_CONSTRAINT_VALUES.include?(@constraint) raise ArgumentError, ":constraint option must be one of #{VALID_CONSTRAINT_VALUES.to_a.join(', ')}" end + + return unless @constraint_validation + unless VALID_DEFERABLE_VALUES.include?(@constraint_validation) + raise ArgumentError, ":constraint_validation option must be one of #{VALID_DEFERABLE_VALUES.to_a.collect(&:inspect).join(', ')}" + end end end # module OneToMany diff --git a/lib/dm-constraints.rb b/lib/dm-constraints.rb index ae15b8b..3184b57 100644 --- a/lib/dm-constraints.rb +++ b/lib/dm-constraints.rb @@ -15,5 +15,6 @@ module DataMapper module Constraints VALID_CONSTRAINT_VALUES = [ :protect, :destroy, :destroy!, :set_nil, :skip ].to_set.freeze + VALID_DEFERABLE_VALUES = [ false, true, :initially_deferred, :initially_immediate ].to_set.freeze end end diff --git a/spec/integration/constraints_spec.rb b/spec/integration/constraints_spec.rb index b6138a6..e30ef77 100644 --- a/spec/integration/constraints_spec.rb +++ b/spec/integration/constraints_spec.rb @@ -625,6 +625,120 @@ class ::Author end.should raise_error(ArgumentError) end end + + # TODO: it's mostly the test itself which is only known to work + # on PostgreSQL. The feature itself will work on any database + # that supports [NOT ]DEFERRABLE and INITIALLY + # [DEFERRED|IMMEDIATE], including, e.g., Oracle, but excluding, + # e.g., MySQL. + supported_by :postgres do + describe 'with :constraint_deferrable =>' do + def deferrability_metadata(relationship) + adapter = DataMapper.repository(:default).adapter + constraint_name = # use private method from this lib for robustness + adapter.send(:constraint_name, relationship.child_model.storage_name, relationship.name) + adapter.select( + "SELECT is_deferrable, initially_deferred FROM information_schema.table_constraints WHERE constraint_name=?", + constraint_name).first + end + + shared_examples_for 'not deferred' do + it 'is not deferrable' do + deferrability_metadata(Comment.relationships[:article]).is_deferrable.should == 'NO' + end + + it 'is initially immediate' do + deferrability_metadata(Comment.relationships[:article]).initially_deferred.should == 'NO' + end + end + + shared_examples_for 'all deferred' do + it 'is deferrable' do + deferrability_metadata(Comment.relationships[:article]).is_deferrable.should == 'YES' + end + + it 'is initially deferred' do + deferrability_metadata(Comment.relationships[:article]).initially_deferred.should == 'YES' + end + end + + describe 'not set' do + before :all do + class ::Article + has n, :comments + end + + class ::Comment + belongs_to :article + end + end + + it_should_behave_like 'not deferred' + end + + describe 'false' do + before :all do + class ::Article + has n, :comments, :constraint_deferrable => false + end + + class ::Comment + belongs_to :article + end + end + + it_should_behave_like 'not deferred' + end + + describe 'true' do + before :all do + class ::Article + has n, :comments, :constraint_deferrable => true + end + + class ::Comment + belongs_to :article + end + end + + it_should_behave_like 'all deferred' + end + + describe ':initially_deferred' do + before :all do + class ::Article + has n, :comments, :constraint_deferrable => :initially_deferred + end + + class ::Comment + belongs_to :article + end + end + + it_should_behave_like 'all deferred' + end + + describe ':initially_immediate' do + before :all do + class ::Article + has n, :comments, :constraint_deferrable => :initially_immediate + end + + class ::Comment + belongs_to :article + end + end + + it 'is deferrable' do + deferrability_metadata(Comment.relationships[:article]).is_deferrable.should == 'YES' + end + + it 'is initially immediate' do + deferrability_metadata(Comment.relationships[:article]).initially_deferred.should == 'NO' + end + end + end + end end end end