Skip to content

Commit 5e5f703

Browse files
committed
Add Database#{defer,immediate}_constraints on PostgreSQL for changing handling of deferrable constraints in a transaction
This allows you to easily check deferrable constraints at a specific point in a transaction, instead of waiting until the end of the transaction. For deferrable constraints that are initially immediate, this allows you to deter the checking of the constraints. I thought about using a single set_constraints method for this, but I think two separate methods results in more readable code.
1 parent 96e6d46 commit 5e5f703

File tree

4 files changed

+122
-3
lines changed

4 files changed

+122
-3
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
=== master
2+
3+
* Add Database#{defer,immediate}_constraints on PostgreSQL for changing handling of deferrable constraints in a transaction (jeremyevans)
4+
15
=== 5.74.0 (2023-11-01)
26

37
* Make generated columns show up in Database#schema when using SQLite 3.37+ (jeremyevans) (#2087)

lib/sequel/adapters/shared/postgres.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,25 @@ def database_type
498498
:postgres
499499
end
500500

501+
# For constraints that are deferrable, defer constraints until
502+
# transaction commit. Options:
503+
#
504+
# :constraints :: An identifier of the constraint, or an array of
505+
# identifiers for constraints, to apply this
506+
# change to specific constraints.
507+
# :server :: The server/shard on which to run the query.
508+
#
509+
# Examples:
510+
#
511+
# DB.defer_constraints
512+
# # SET CONSTRAINTS ALL DEFERRED
513+
#
514+
# DB.defer_constraints(constraints: [:c1, Sequel[:sc][:c2]])
515+
# # SET CONSTRAINTS "c1", "sc"."s2" DEFERRED
516+
def defer_constraints(opts=OPTS)
517+
_set_constraints(' DEFERRED', opts)
518+
end
519+
501520
# Use PostgreSQL's DO syntax to execute an anonymous code block. The code should
502521
# be the literal code string to use in the underlying procedural language. Options:
503522
#
@@ -611,6 +630,24 @@ def freeze
611630
super
612631
end
613632

633+
# Immediately apply deferrable constraints.
634+
#
635+
# :constraints :: An identifier of the constraint, or an array of
636+
# identifiers for constraints, to apply this
637+
# change to specific constraints.
638+
# :server :: The server/shard on which to run the query.
639+
#
640+
# Examples:
641+
#
642+
# DB.immediate_constraints
643+
# # SET CONSTRAINTS ALL IMMEDIATE
644+
#
645+
# DB.immediate_constraints(constraints: [:c1, Sequel[:sc][:c2]])
646+
# # SET CONSTRAINTS "c1", "sc"."s2" IMMEDIATE
647+
def immediate_constraints(opts=OPTS)
648+
_set_constraints(' IMMEDIATE', opts)
649+
end
650+
614651
# Use the pg_* system tables to determine indexes on a table
615652
def indexes(table, opts=OPTS)
616653
m = output_identifier_meth
@@ -1038,6 +1075,23 @@ def _schema_ds
10381075
end
10391076
end
10401077

1078+
# Internals of defer_constraints/immediate_constraints
1079+
def _set_constraints(type, opts)
1080+
execute_ddl(_set_constraints_sql(type, opts), opts)
1081+
end
1082+
1083+
# SQL to use for SET CONSTRAINTS
1084+
def _set_constraints_sql(type, opts)
1085+
sql = String.new
1086+
sql << "SET CONSTRAINTS "
1087+
if constraints = opts[:constraints]
1088+
dataset.send(:source_list_append, sql, Array(constraints))
1089+
else
1090+
sql << "ALL"
1091+
end
1092+
sql << type
1093+
end
1094+
10411095
def alter_table_add_column_sql(table, op)
10421096
"ADD COLUMN#{' IF NOT EXISTS' if op[:if_not_exists]} #{column_definition_sql(op)}"
10431097
end

spec/adapters/postgres_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,47 @@ def c.exec_prepared(*); super; nil end
479479
end
480480
end
481481

482+
it "should have #immediate_constraints and #defer_constraints for deferring/checking deferrable constraints" do
483+
@db.create_table(:tmp_dolls) do
484+
primary_key :id
485+
foreign_key(:x, :tmp_dolls, :foreign_key_constraint_name=>:x_fk, :deferrable=>true)
486+
foreign_key(:y, :tmp_dolls, :foreign_key_constraint_name=>:y_fk, :deferrable=>true)
487+
end
488+
489+
ds = @db[:tmp_dolls]
490+
@db.transaction do
491+
@db.immediate_constraints
492+
ds.insert(:id=>1)
493+
@db.defer_constraints
494+
ds.insert(:id=>2, :x=>1, :y=>3)
495+
proc{@db.immediate_constraints}.must_raise Sequel::ForeignKeyConstraintViolation
496+
end
497+
@db[:tmp_dolls].must_be_empty
498+
499+
@db.transaction do
500+
@db.immediate_constraints
501+
@db.defer_constraints(:constraints=>:y_fk)
502+
ds.insert(:id=>1, :x=>1, :y=>2)
503+
proc{@db.immediate_constraints}.must_raise Sequel::ForeignKeyConstraintViolation
504+
end
505+
@db[:tmp_dolls].must_be_empty
506+
507+
@db.transaction do
508+
@db.defer_constraints
509+
ds.insert(:id=>1, :x=>1, :y=>2)
510+
proc{@db.immediate_constraints(:constraints=>:y_fk)}.must_raise Sequel::ForeignKeyConstraintViolation
511+
end
512+
@db[:tmp_dolls].must_be_empty
513+
514+
@db.transaction do
515+
@db.immediate_constraints
516+
@db.defer_constraints(:constraints=>[:x_fk, :y_fk])
517+
ds.insert(:id=>1, :x=>3, :y=>2)
518+
ds.update(:id=>1, :x=>1, :y=>1)
519+
end
520+
@db[:tmp_dolls].count.must_equal 1
521+
end
522+
482523
it "should have #check_constraints method for getting check constraints" do
483524
@db.create_table(:tmp_dolls) do
484525
Integer :i

spec/core/mock_adapter_spec.rb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def foo
145145
rs.must_equal [{:a=>1}] * 2
146146
end
147147

148-
it "should be able to set an exception to raise by setting the :fetch option to an exception class " do
148+
it "should be able to set an exception to raise by setting the :fetch option to an exception class" do
149149
db = Sequel.mock(:fetch=>ArgumentError)
150150
proc{db[:t].all}.must_raise(Sequel::DatabaseError)
151151
begin
@@ -212,7 +212,7 @@ def foo
212212
db[:b].delete.must_equal 1
213213
end
214214

215-
it "should be able to set an exception to raise by setting the :numrows option to an exception class " do
215+
it "should be able to set an exception to raise by setting the :numrows option to an exception class" do
216216
db = Sequel.mock(:numrows=>ArgumentError)
217217
proc{db[:t].update(:a=>1)}.must_raise(Sequel::DatabaseError)
218218
begin
@@ -280,7 +280,7 @@ def foo
280280
db[:b].insert(:a=>1).must_equal 1
281281
end
282282

283-
it "should be able to set an exception to raise by setting the :autoid option to an exception class " do
283+
it "should be able to set an exception to raise by setting the :autoid option to an exception class" do
284284
db = Sequel.mock(:autoid=>ArgumentError)
285285
proc{db[:t].insert(:a=>1)}.must_raise(Sequel::DatabaseError)
286286
begin
@@ -869,6 +869,26 @@ def @db.schema(x) [[:id, {:primary_key=>false, :auto_increment=>false}]] end
869869
it "should recognize 40P01 SQL state as a serialization failure" do
870870
@db.send(:database_specific_error_class_from_sqlstate, '40P01').must_equal Sequel::SerializationFailure
871871
end
872+
873+
it "should use correct SQL for defer_constraints and immediate_constraints" do
874+
@db.defer_constraints
875+
@db.sqls.must_equal ['SET CONSTRAINTS ALL DEFERRED']
876+
@db.immediate_constraints
877+
@db.sqls.must_equal ['SET CONSTRAINTS ALL IMMEDIATE']
878+
879+
@db.defer_constraints(:constraints=>:a)
880+
@db.sqls.must_equal ['SET CONSTRAINTS "a" DEFERRED']
881+
@db.immediate_constraints(:constraints=>[:a, :b])
882+
@db.sqls.must_equal ['SET CONSTRAINTS "a", "b" IMMEDIATE']
883+
end
884+
885+
it "should correctly handle defer_constraints and immediate_constraints :server option" do
886+
db = Sequel.connect("mock://postgres", :servers=>{:test=>{}})
887+
db.defer_constraints(:server=>:test)
888+
db.sqls.must_equal ['SET CONSTRAINTS ALL DEFERRED -- test']
889+
db.immediate_constraints(:server=>:test)
890+
db.sqls.must_equal ['SET CONSTRAINTS ALL IMMEDIATE -- test']
891+
end
872892
end
873893

874894
describe "MySQL support" do

0 commit comments

Comments
 (0)