Skip to content

Commit 326df13

Browse files
committed
Support MERGE RETURNING on PostgreSQL 17+
1 parent 77fdd94 commit 326df13

File tree

3 files changed

+127
-2
lines changed

3 files changed

+127
-2
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
=== master
22

3+
* Support MERGE RETURNING on PostgreSQL 17+ (jeremyevans)
4+
35
* Remove use of logger library in bin/sequel (jeremyevans)
46

57
* Support :connect_opts_proc Database option for late binding options (jeremyevans) (#2164)

lib/sequel/adapters/shared/postgres.rb

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,6 +2058,16 @@ def lock(mode, opts=OPTS)
20582058
nil
20592059
end
20602060

2061+
# Support MERGE RETURNING on PostgreSQL 17+.
2062+
def merge(&block)
2063+
sql = merge_sql
2064+
if uses_returning?(:merge)
2065+
returning_fetch_rows(sql, &block)
2066+
else
2067+
execute_ddl(sql)
2068+
end
2069+
end
2070+
20612071
# Return a dataset with a WHEN MATCHED THEN DO NOTHING clause added to the
20622072
# MERGE statement. If a block is passed, treat it as a virtual row and
20632073
# use it as additional conditions for the match.
@@ -2170,9 +2180,14 @@ def supports_nowait?
21702180
true
21712181
end
21722182

2173-
# Returning is always supported.
2183+
# MERGE RETURNING is supported on PostgreSQL 17+. Other RETURNING is supported
2184+
# on all supported PostgreSQL versions.
21742185
def supports_returning?(type)
2175-
true
2186+
if type == :merge
2187+
server_version >= 170000
2188+
else
2189+
true
2190+
end
21762191
end
21772192

21782193
# PostgreSQL supports pattern matching via regular expressions
@@ -2295,6 +2310,12 @@ def _merge_matched_sql(sql, data)
22952310
end
22962311
alias _merge_not_matched_sql _merge_matched_sql
22972312

2313+
# Support MERGE RETURNING on PostgreSQL 17+.
2314+
def _merge_when_sql(sql)
2315+
super
2316+
insert_returning_sql(sql) if uses_returning?(:merge)
2317+
end
2318+
22982319
# Format TRUNCATE statement with PostgreSQL specific options.
22992320
def _truncate_sql(table)
23002321
to = @opts[:truncate_opts] || OPTS

spec/adapters/postgres_spec.rb

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5928,3 +5928,105 @@ def c2.name; 'Bar' end
59285928
end
59295929
end
59305930
end
5931+
5932+
5933+
describe "MERGE RETURNING" do
5934+
before(:all) do
5935+
@db = DB
5936+
@db.create_table!(:m1){Integer :i1; Integer :a}
5937+
@db.create_table!(:m2){Integer :i2; Integer :b}
5938+
@m1 = @db[:m1].returning
5939+
@m2 = @db[:m2]
5940+
end
5941+
after do
5942+
@m1.delete
5943+
@m2.delete
5944+
end
5945+
after(:all) do
5946+
@db.drop_table?(:m1, :m2)
5947+
end
5948+
5949+
def merge(ds)
5950+
a = []
5951+
ds.merge{|h| a << h}
5952+
a
5953+
end
5954+
5955+
def check(ds)
5956+
@m2.insert(1, 2)
5957+
5958+
@m1.all.must_equal []
5959+
5960+
# INSERT
5961+
merge(ds).must_equal [{:i2=>1, :b=>2, :i1=>1, :a=>13}]
5962+
@m1.all.must_equal [{:i1=>1, :a=>13}]
5963+
5964+
# UPDATE
5965+
merge(ds).must_equal [{:i2=>1, :b=>2, :i1=>12, :a=>35}]
5966+
@m1.all.must_equal [{:i1=>12, :a=>35}]
5967+
5968+
# UPDATE with specific RETURNING columns
5969+
@m1.update(:i1=>1, :a=>13)
5970+
merge(ds.returning(:b)).must_equal [{:b=>2}]
5971+
@m1.all.must_equal [{:i1=>12, :a=>35}]
5972+
5973+
# DELETE MATCHING current row, INSERT NOT MATCHED new row
5974+
@m2.insert(12, 3)
5975+
merge(ds).must_equal [{:i2=>1, :b=>2, :i1=>1, :a=>13}, {:i2=>12, :b=>3, :i1=>12, :a=>35}]
5976+
@m1.all.must_equal [{:i1=>1, :a=>13}]
5977+
5978+
# MATCHED DO NOTHING
5979+
@m2.where(:i2=>12).delete
5980+
@m1.update(:a=>51)
5981+
merge(ds).must_equal []
5982+
@m1.all.must_equal [{:i1=>1, :a=>51}]
5983+
5984+
# NOT MATCHED DO NOTHING
5985+
@m1.delete
5986+
@m2.update(:b=>51)
5987+
merge(ds).must_equal []
5988+
@m1.all.must_equal []
5989+
end
5990+
5991+
it "should allow inserts, updates, and deletes based on conditions in a single MERGE statement" do
5992+
ds = @m1.
5993+
merge_using(:m2, :i1=>:i2).
5994+
merge_insert(:i1=>Sequel[:i2], :a=>Sequel[:b]+11){b <= 50}.
5995+
merge_delete{{:a => 30..50}}.
5996+
merge_update(:i1=>Sequel[:i1]+:i2+10, :a=>Sequel[:a]+:b+20){a <= 50}
5997+
check(ds)
5998+
end
5999+
6000+
it "should support WITH clauses" do
6001+
ds = @m1.
6002+
with(:m3, @db[:m2]).
6003+
merge_using(:m3, :i1=>:i2).
6004+
merge_insert(:i1=>Sequel[:i2], :a=>Sequel[:b]+11){b <= 50}.
6005+
merge_delete{{:a => 30..50}}.
6006+
merge_update(:i1=>Sequel[:i1]+:i2+10, :a=>Sequel[:a]+:b+20){a <= 50}
6007+
check(ds)
6008+
end if DB.dataset.supports_cte?
6009+
6010+
it "should support inserts with just columns" do
6011+
ds = @m1.
6012+
merge_using(:m2, :i1=>:i2).
6013+
merge_insert(Sequel[:i2], Sequel[:b]+11){b <= 50}.
6014+
merge_delete{{:a => 30..50}}.
6015+
merge_update(:i1=>Sequel[:i1]+:i2+10, :a=>Sequel[:a]+:b+20){a <= 50}
6016+
check(ds)
6017+
end
6018+
6019+
it "should calls inserts, updates, and deletes without conditions" do
6020+
@m2.insert(1, 2)
6021+
ds = @m1.merge_using(:m2, :i1=>:i2)
6022+
6023+
merge(ds.merge_insert(:i2, :b)).must_equal [{:i2=>1, :b=>2, :i1=>1, :a=>2}]
6024+
@m1.all.must_equal [{:i1=>1, :a=>2}]
6025+
6026+
merge(ds.merge_update(:a=>Sequel[:a]+1)).must_equal [{:i2=>1, :b=>2, :i1=>1, :a=>3}]
6027+
@m1.all.must_equal [{:i1=>1, :a=>3}]
6028+
6029+
merge(ds.merge_delete).must_equal [{:i2=>1, :b=>2, :i1=>1, :a=>3}]
6030+
@m1.all.must_equal []
6031+
end
6032+
end if DB.server_version >= 170000

0 commit comments

Comments
 (0)