Skip to content

Commit

Permalink
Support MERGE WHEN NOT MATCHED BY SOURCE on PostgreSQL 17+
Browse files Browse the repository at this point in the history
This adds the following dataset methods on PostgreSQL:

* merge_delete_when_not_matched_by_source
* merge_update_when_not_matched_by_source
* merge_do_nothing_when_not_matched_by_source

These are similar to the existing merge_delete, merge_update,
and merge_do_nothing_when_matched, except they use
WHEN NOT MATCHED BY SOURCE instead of WHEN MATCHED.

This replaces the _merge_matched_sql and _merge_not_matched_sql
private dataset methods with _merge_do_nothing_sql.
  • Loading branch information
jeremyevans committed Jul 1, 2024
1 parent 0fb4446 commit 0c7294e
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== master

* Support MERGE WHEN NOT MATCHED BY SOURCE on PostgreSQL 17+ (jeremyevans)

* Add stdio_logger extension for a minimal logger that Sequel::Database can use (jeremyevans)

* Simplify Database#inspect output to database_type, host, database, and user (jeremyevans)
Expand Down
42 changes: 40 additions & 2 deletions lib/sequel/adapters/shared/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2068,6 +2068,19 @@ def merge(&block)
end
end

# Return a dataset with a WHEN NOT MATCHED BY SOURCE THEN DELETE clause added to the
# MERGE statement. If a block is passed, treat it as a virtual row and
# use it as additional conditions for the match.
#
# merge_delete_not_matched_by_source
# # WHEN NOT MATCHED BY SOURCE THEN DELETE
#
# merge_delete_not_matched_by_source{a > 30}
# # WHEN NOT MATCHED BY SOURCE AND (a > 30) THEN DELETE
def merge_delete_when_not_matched_by_source(&block)
_merge_when(:type=>:delete_not_matched_by_source, &block)
end

# Return a dataset with a WHEN MATCHED THEN DO NOTHING clause added to the
# MERGE statement. If a block is passed, treat it as a virtual row and
# use it as additional conditions for the match.
Expand All @@ -2094,6 +2107,19 @@ def merge_do_nothing_when_not_matched(&block)
_merge_when(:type=>:not_matched, &block)
end

# Return a dataset with a WHEN NOT MATCHED BY SOURCE THEN DO NOTHING clause added to the
# MERGE BY SOURCE statement. If a block is passed, treat it as a virtual row and
# use it as additional conditions for the match.
#
# merge_do_nothing_when_not_matched_by_source
# # WHEN NOT MATCHED BY SOURCE THEN DO NOTHING
#
# merge_do_nothing_when_not_matched_by_source{a > 30}
# # WHEN NOT MATCHED BY SOURCE AND (a > 30) THEN DO NOTHING
def merge_do_nothing_when_not_matched_by_source(&block)
_merge_when(:type=>:not_matched_by_source, &block)
end

# Support OVERRIDING USER|SYSTEM VALUE for MERGE INSERT.
def merge_insert(*values, &block)
h = {:type=>:insert, :values=>values}
Expand All @@ -2103,6 +2129,19 @@ def merge_insert(*values, &block)
_merge_when(h, &block)
end

# Return a dataset with a WHEN NOT MATCHED BY SOURCE THEN UPDATE clause added to the
# MERGE statement. If a block is passed, treat it as a virtual row and
# use it as additional conditions for the match.
#
# merge_update_not_matched_by_source(i1: Sequel[:i1]+:i2+10, a: Sequel[:a]+:b+20)
# # WHEN NOT MATCHED BY SOURCE THEN UPDATE SET i1 = (i1 + i2 + 10), a = (a + b + 20)
#
# merge_update_not_matched_by_source(i1: :i2){a > 30}
# # WHEN NOT MATCHED BY SOURCE AND (a > 30) THEN UPDATE SET i1 = i2
def merge_update_when_not_matched_by_source(values, &block)
_merge_when(:type=>:update_not_matched_by_source, :values=>values, &block)
end

# Use OVERRIDING USER VALUE for INSERT statements, so that identity columns
# always use the user supplied value, and an error is not raised for identity
# columns that are GENERATED ALWAYS.
Expand Down Expand Up @@ -2305,10 +2344,9 @@ def _merge_insert_sql(sql, data)
_insert_values_sql(sql, values)
end

def _merge_matched_sql(sql, data)
def _merge_do_nothing_sql(sql, data)
sql << " THEN DO NOTHING"
end
alias _merge_not_matched_sql _merge_matched_sql

# Support MERGE RETURNING on PostgreSQL 17+.
def _merge_when_sql(sql)
Expand Down
13 changes: 13 additions & 0 deletions lib/sequel/dataset/sql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -901,18 +901,31 @@ def _merge_delete_sql(sql, data)
MERGE_TYPE_SQL = {
:insert => ' WHEN NOT MATCHED',
:delete => ' WHEN MATCHED',
:delete_not_matched_by_source => ' WHEN NOT MATCHED BY SOURCE',
:update => ' WHEN MATCHED',
:update_not_matched_by_source => ' WHEN NOT MATCHED BY SOURCE',
:matched => ' WHEN MATCHED',
:not_matched => ' WHEN NOT MATCHED',
:not_matched_by_source => ' WHEN NOT MATCHED BY SOURCE',
}.freeze
private_constant :MERGE_TYPE_SQL

MERGE_NORMALIZE_TYPE_MAP = {
:delete_not_matched_by_source => :delete,
:update_not_matched_by_source => :update,
:matched => :do_nothing,
:not_matched => :do_nothing,
:not_matched_by_source => :do_nothing,
}.freeze
private_constant :MERGE_NORMALIZE_TYPE_MAP

# Add the WHEN clauses to the MERGE SQL
def _merge_when_sql(sql)
raise Error, "no WHEN [NOT] MATCHED clauses provided for MERGE" unless merge_when = @opts[:merge_when]
merge_when.each do |data|
type = data[:type]
sql << MERGE_TYPE_SQL[type]
type = MERGE_NORMALIZE_TYPE_MAP[type] || type
_merge_when_conditions_sql(sql, data)
send(:"_merge_#{type}_sql", sql, data)
end
Expand Down
45 changes: 45 additions & 0 deletions spec/adapters/postgres_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6030,3 +6030,48 @@ def check(ds)
@m1.all.must_equal []
end
end if DB.server_version >= 170000

describe "MERGE WHEN NOT MATCHED BY SOURCE" do
before(:all) do
@db = DB
@db.create_table!(:m1){Integer :i1; Integer :a}
@db.create_table!(:m2){Integer :i2}
@m1 = @db[:m1]
end
after do
@m1.delete
end
after(:all) do
@db.drop_table?(:m1, :m2)
end

it "should allow inserts, updates, do nothings, and deletes based on conditions in a single MERGE statement" do
ds = @m1.merge_using(:m2, :i1=>:i2)

@m1.insert(1, 2)
@m1.all.must_equal [{:i1=>1, :a=>2}]

ds.merge_do_nothing_when_not_matched_by_source.merge
@m1.all.must_equal [{:i1=>1, :a=>2}]

ds.merge_update_when_not_matched_by_source(:i1=>Sequel[:i1]+10, :a=>Sequel[:a]+20).merge
@m1.all.must_equal [{:i1=>11, :a=>22}]

ds.merge_delete_when_not_matched_by_source.merge
@m1.all.must_equal []

@m1.insert(1, 100)
@m1.insert(2, 40)
@m1.insert(3, 20)
@m1.insert(4, 5)

# conditions
ds.
merge_do_nothing_when_not_matched_by_source{a > 50}.
merge_delete_when_not_matched_by_source{a > 30}.
merge_update_when_not_matched_by_source(:a=>Sequel[:a]+20){a > 10}.
merge_update_when_not_matched_by_source(:a=>Sequel[:a]-20).
merge
@m1.order(:i1).all.must_equal [{:i1=>1, :a=>100}, {:i1=>3, :a=>40}, {:i1=>4, :a=>-15}]
end
end if DB.server_version >= 170000

0 comments on commit 0c7294e

Please sign in to comment.