Skip to content
This repository has been archived by the owner on Apr 17, 2018. It is now read-only.

Allow the foreign keys generated for relationships to be DEFERRABLE. #13

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down
14 changes: 13 additions & 1 deletion lib/data_mapper/constraints/adapters/do_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm definitely nitpicking here, but this should be [String, NilClass].

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to change it if that's the project's standard, but YARD's documentation says to use [String, nil]. See the first example under Declaring Types in YARD's Getting Started guide.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right you are. NilClass is widely used as a YARD type declaration in the DM codebase, but that may be leftover from earlier YARD conventions.

Thanks for the correction!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's also true you might find stuff like [TrueClass, FalseClass], but I'm told that the official way is [Boolean].

For singletons I would probably rather use the more common name rather than the class name.

# 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
Expand All @@ -141,14 +152,15 @@ 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)}
FOREIGN KEY (#{source_keys.join(', ')})
REFERENCES #{quote_name(target_storage_name)} (#{target_keys.join(', ')})
ON DELETE #{constraint_type}
ON UPDATE #{constraint_type}
#{deferrable}
SQL
end

Expand Down
6 changes: 2 additions & 4 deletions lib/data_mapper/constraints/adapters/oracle_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 8 additions & 1 deletion lib/data_mapper/constraints/relationship/one_to_many.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Relationship
module OneToMany

attr_reader :constraint
attr_reader :constraint_deferrable

# @api private
def enforce_destroy_constraint(resource)
Expand All @@ -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
#
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/dm-constraints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
114 changes: 114 additions & 0 deletions spec/integration/constraints_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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