From 30cf540e85e61ba4ee9aa11c85d31f064ec4ef8a Mon Sep 17 00:00:00 2001 From: Rhett Sutphin Date: Tue, 6 Sep 2011 18:09:22 -0500 Subject: [PATCH 1/2] Allow the FKs generated for relationships to be deferrable. This commit adds a new option for relationships -- :constraint_deferrable. The values available for it are documented in the README. If set, it will cause the migrations that generate the relationship's constraint to include DEFERRABLE and INITIALLY DEFERRED or IMMEDIATE. Caveat: the Oracle adapter change has not been tested. --- README.rdoc | 14 ++- .../constraints/adapters/do_adapter.rb | 14 ++- .../constraints/adapters/oracle_adapter.rb | 6 +- .../constraints/relationship/one_to_many.rb | 9 +- lib/dm-constraints.rb | 1 + spec/integration/constraints_spec.rb | 114 ++++++++++++++++++ 6 files changed, 150 insertions(+), 8 deletions(-) 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..0d007f2 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 only works on + # PostgreSQL and MySQL (and any other database which has FKs and + # information_schema). The feature itself will work on any + # database that supports [NOT ]DEFERRABLE and INITIALLY + # [DEFERRED|IMMEDIATE], including, e.g., Oracle. + supported_by :postgres, :mysql 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 From dfdd71adc5b8254da2c6565f6e19871878b36012 Mon Sep 17 00:00:00 2001 From: Rhett Sutphin Date: Tue, 6 Sep 2011 19:16:34 -0500 Subject: [PATCH 2/2] MySQL doesn't support DEFERRABLE after all. --- spec/integration/constraints_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/integration/constraints_spec.rb b/spec/integration/constraints_spec.rb index 0d007f2..e30ef77 100644 --- a/spec/integration/constraints_spec.rb +++ b/spec/integration/constraints_spec.rb @@ -626,12 +626,12 @@ class ::Author end end - # TODO: it's mostly the test itself which only works on - # PostgreSQL and MySQL (and any other database which has FKs and - # information_schema). The feature itself will work on any - # database that supports [NOT ]DEFERRABLE and INITIALLY - # [DEFERRED|IMMEDIATE], including, e.g., Oracle. - supported_by :postgres, :mysql do + # 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