sequel 5.56.0 → 5.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -414,6 +414,7 @@ module Sequel
414
414
  # 2 :: argument name
415
415
  # 3 :: argument mode (e.g. in, out, inout)
416
416
  # :behavior :: Should be IMMUTABLE, STABLE, or VOLATILE. PostgreSQL assumes VOLATILE by default.
417
+ # :parallel :: The thread safety attribute of the function. Should be SAFE, UNSAFE, RESTRICTED. PostgreSQL assumes UNSAFE by default.
417
418
  # :cost :: The estimated cost of the function, used by the query planner.
418
419
  # :language :: The language the function uses. SQL is the default.
419
420
  # :link_symbol :: For a dynamically loaded see function, the function's link symbol if different from the definition argument.
@@ -1117,6 +1118,7 @@ module Sequel
1117
1118
  #{opts[:behavior].to_s.upcase if opts[:behavior]}
1118
1119
  #{'STRICT' if opts[:strict]}
1119
1120
  #{'SECURITY DEFINER' if opts[:security_definer]}
1121
+ #{"PARALLEL #{opts[:parallel].to_s.upcase}" if opts[:parallel]}
1120
1122
  #{"COST #{opts[:cost]}" if opts[:cost]}
1121
1123
  #{"ROWS #{opts[:rows]}" if opts[:rows]}
1122
1124
  #{opts[:set].map{|k,v| " SET #{k} = #{v}"}.join("\n") if opts[:set]}
@@ -1245,6 +1247,10 @@ module Sequel
1245
1247
  def create_view_prefix_sql(name, options)
1246
1248
  sql = create_view_sql_append_columns("CREATE #{'OR REPLACE 'if options[:replace]}#{'TEMPORARY 'if options[:temp]}#{'RECURSIVE ' if options[:recursive]}#{'MATERIALIZED ' if options[:materialized]}VIEW #{quote_schema_table(name)}", options[:columns] || options[:recursive])
1247
1249
 
1250
+ if options[:security_invoker]
1251
+ sql += " WITH (security_invoker)"
1252
+ end
1253
+
1248
1254
  if tablespace = options[:tablespace]
1249
1255
  sql += " TABLESPACE #{quote_identifier(tablespace)}"
1250
1256
  end
@@ -1302,16 +1308,20 @@ module Sequel
1302
1308
  def index_definition_sql(table_name, index)
1303
1309
  cols = index[:columns]
1304
1310
  index_name = index[:name] || default_index_name(table_name, cols)
1311
+
1305
1312
  expr = if o = index[:opclass]
1306
1313
  "(#{Array(cols).map{|c| "#{literal(c)} #{o}"}.join(', ')})"
1307
1314
  else
1308
1315
  literal(Array(cols))
1309
1316
  end
1317
+
1310
1318
  if_not_exists = " IF NOT EXISTS" if index[:if_not_exists]
1311
1319
  unique = "UNIQUE " if index[:unique]
1312
1320
  index_type = index[:type]
1313
1321
  filter = index[:where] || index[:filter]
1314
1322
  filter = " WHERE #{filter_expr(filter)}" if filter
1323
+ nulls_distinct = " NULLS#{' NOT' if index[:nulls_distinct] == false} DISTINCT" unless index[:nulls_distinct].nil?
1324
+
1315
1325
  case index_type
1316
1326
  when :full_text
1317
1327
  expr = "(to_tsvector(#{literal(index[:language] || 'simple')}::regconfig, #{literal(dataset.send(:full_text_string_join, cols))}))"
@@ -1319,7 +1329,8 @@ module Sequel
1319
1329
  when :spatial
1320
1330
  index_type = :gist
1321
1331
  end
1322
- "CREATE #{unique}INDEX#{' CONCURRENTLY' if index[:concurrently]}#{if_not_exists} #{quote_identifier(index_name)} ON #{quote_schema_table(table_name)} #{"USING #{index_type} " if index_type}#{expr}#{" INCLUDE #{literal(Array(index[:include]))}" if index[:include]}#{" TABLESPACE #{quote_identifier(index[:tablespace])}" if index[:tablespace]}#{filter}"
1332
+
1333
+ "CREATE #{unique}INDEX#{' CONCURRENTLY' if index[:concurrently]}#{if_not_exists} #{quote_identifier(index_name)} ON #{quote_schema_table(table_name)} #{"USING #{index_type} " if index_type}#{expr}#{" INCLUDE #{literal(Array(index[:include]))}" if index[:include]}#{nulls_distinct}#{" TABLESPACE #{quote_identifier(index[:tablespace])}" if index[:tablespace]}#{filter}"
1323
1334
  end
1324
1335
 
1325
1336
  # Setup datastructures shared by all postgres adapters.
@@ -1525,7 +1536,7 @@ module Sequel
1525
1536
  LOCK_MODES = ['ACCESS SHARE', 'ROW SHARE', 'ROW EXCLUSIVE', 'SHARE UPDATE EXCLUSIVE', 'SHARE', 'SHARE ROW EXCLUSIVE', 'EXCLUSIVE', 'ACCESS EXCLUSIVE'].each(&:freeze).freeze
1526
1537
 
1527
1538
  Dataset.def_sql_method(self, :delete, [['if server_version >= 90100', %w'with delete from using where returning'], ['else', %w'delete from using where returning']])
1528
- Dataset.def_sql_method(self, :insert, [['if server_version >= 90500', %w'with insert into columns values conflict returning'], ['elsif server_version >= 90100', %w'with insert into columns values returning'], ['else', %w'insert into columns values returning']])
1539
+ Dataset.def_sql_method(self, :insert, [['if server_version >= 90500', %w'with insert into columns override values conflict returning'], ['elsif server_version >= 90100', %w'with insert into columns values returning'], ['else', %w'insert into columns values returning']])
1529
1540
  Dataset.def_sql_method(self, :select, [['if opts[:values]', %w'values order limit'], ['elsif server_version >= 80400', %w'with select distinct columns from join where group having window compounds order limit lock'], ['else', %w'select distinct columns from join where group having compounds order limit lock']])
1530
1541
  Dataset.def_sql_method(self, :update, [['if server_version >= 90100', %w'with update table set from where returning'], ['else', %w'update table set from where returning']])
1531
1542
 
@@ -1758,6 +1769,41 @@ module Sequel
1758
1769
  nil
1759
1770
  end
1760
1771
 
1772
+ # Return a dataset with a WHEN MATCHED THEN DO NOTHING clause added to the
1773
+ # MERGE statement. If a block is passed, treat it as a virtual row and
1774
+ # use it as additional conditions for the match.
1775
+ #
1776
+ # merge_do_nothing_when_matched
1777
+ # # WHEN MATCHED THEN DO NOTHING
1778
+ #
1779
+ # merge_do_nothing_when_matched{a > 30}
1780
+ # # WHEN MATCHED AND (a > 30) THEN DO NOTHING
1781
+ def merge_do_nothing_when_matched(&block)
1782
+ _merge_when(:type=>:matched, &block)
1783
+ end
1784
+
1785
+ # Return a dataset with a WHEN NOT MATCHED THEN DO NOTHING clause added to the
1786
+ # MERGE statement. If a block is passed, treat it as a virtual row and
1787
+ # use it as additional conditions for the match.
1788
+ #
1789
+ # merge_do_nothing_when_not_matched
1790
+ # # WHEN NOT MATCHED THEN DO NOTHING
1791
+ #
1792
+ # merge_do_nothing_when_not_matched{a > 30}
1793
+ # # WHEN NOT MATCHED AND (a > 30) THEN DO NOTHING
1794
+ def merge_do_nothing_when_not_matched(&block)
1795
+ _merge_when(:type=>:not_matched, &block)
1796
+ end
1797
+
1798
+ # Support OVERRIDING USER|SYSTEM VALUE for MERGE INSERT.
1799
+ def merge_insert(*values, &block)
1800
+ h = {:type=>:insert, :values=>values}
1801
+ if override = @opts[:override]
1802
+ h[:override] = insert_override_sql(String.new)
1803
+ end
1804
+ _merge_when(h, &block)
1805
+ end
1806
+
1761
1807
  # Use OVERRIDING USER VALUE for INSERT statements, so that identity columns
1762
1808
  # always use the user supplied value, and an error is not raised for identity
1763
1809
  # columns that are GENERATED ALWAYS.
@@ -1825,6 +1871,11 @@ module Sequel
1825
1871
  true
1826
1872
  end
1827
1873
 
1874
+ # PostgreSQL 15+ supports MERGE.
1875
+ def supports_merge?
1876
+ server_version >= 150000
1877
+ end
1878
+
1828
1879
  # PostgreSQL supports NOWAIT.
1829
1880
  def supports_nowait?
1830
1881
  true
@@ -1935,6 +1986,22 @@ module Sequel
1935
1986
 
1936
1987
  private
1937
1988
 
1989
+ # Append the INSERT sql used in a MERGE
1990
+ def _merge_insert_sql(sql, data)
1991
+ sql << " THEN INSERT "
1992
+ columns, values = _parse_insert_sql_args(data[:values])
1993
+ _insert_columns_sql(sql, columns)
1994
+ if override = data[:override]
1995
+ sql << override
1996
+ end
1997
+ _insert_values_sql(sql, values)
1998
+ end
1999
+
2000
+ def _merge_matched_sql(sql, data)
2001
+ sql << " THEN DO NOTHING"
2002
+ end
2003
+ alias _merge_not_matched_sql _merge_matched_sql
2004
+
1938
2005
  # Format TRUNCATE statement with PostgreSQL specific options.
1939
2006
  def _truncate_sql(table)
1940
2007
  to = @opts[:truncate_opts] || OPTS
@@ -2011,14 +2078,13 @@ module Sequel
2011
2078
  end
2012
2079
 
2013
2080
  # Support OVERRIDING SYSTEM|USER VALUE in insert statements
2014
- def insert_values_sql(sql)
2081
+ def insert_override_sql(sql)
2015
2082
  case opts[:override]
2016
2083
  when :system
2017
2084
  sql << " OVERRIDING SYSTEM VALUE"
2018
2085
  when :user
2019
2086
  sql << " OVERRIDING USER VALUE"
2020
2087
  end
2021
- super
2022
2088
  end
2023
2089
 
2024
2090
  # For multiple table support, PostgreSQL requires at least
@@ -146,6 +146,9 @@ module Sequel
146
146
  #
147
147
  # :generated_type :: Set the type of column when using :generated_always_as,
148
148
  # should be :virtual or :stored to force a type.
149
+ # :on_update_current_timestamp :: Use ON UPDATE CURRENT TIMESTAMP when defining the column,
150
+ # which will update the column value to CURRENT_TIMESTAMP
151
+ # on every UPDATE.
149
152
  #
150
153
  # Microsoft SQL Server specific options:
151
154
  #
@@ -259,6 +262,7 @@ module Sequel
259
262
  # operations on the table while the index is being
260
263
  # built.
261
264
  # :if_not_exists :: Only create the index if an index of the same name doesn't already exist.
265
+ # :nulls_distinct :: Set whether separate NULLs should be considered distinct values in unique indexes.
262
266
  # :opclass :: Set an opclass to use for all columns (per-column opclasses require
263
267
  # custom SQL).
264
268
  # :tablespace :: Specify tablespace for index.
@@ -295,6 +295,9 @@ module Sequel
295
295
  # in a subquery, if you are providing a Dataset as the source
296
296
  # argument, if should probably call the union method with the
297
297
  # all: true and from_self: false options.
298
+ # :security_invoker :: Set the security_invoker property on the view, making
299
+ # the access to the view use the current user's permissions,
300
+ # instead of the view owner's permissions.
298
301
  # :tablespace :: The tablespace to use for materialized views.
299
302
  def create_view(name, source, options = OPTS)
300
303
  execute_ddl(create_view_sql(name, source, options))
@@ -457,6 +457,55 @@ module Sequel
457
457
  _aggregate(:max, arg)
458
458
  end
459
459
 
460
+ # Execute a MERGE statement, which allows for INSERT, UPDATE, and DELETE
461
+ # behavior in a single query, based on whether rows from a source table
462
+ # match rows in the current table, based on the join conditions.
463
+ #
464
+ # Unless the dataset uses static SQL, to use #merge, you must first have
465
+ # called #merge_using to specify the merge source and join conditions.
466
+ # You will then likely to call one or more of the following methods
467
+ # to specify MERGE behavior by adding WHEN [NOT] MATCHED clauses:
468
+ #
469
+ # * #merge_insert
470
+ # * #merge_update
471
+ # * #merge_delete
472
+ #
473
+ # The WHEN [NOT] MATCHED clauses are added to the SQL in the order these
474
+ # methods were called on the dataset. If none of these methods are
475
+ # called, an error is raised.
476
+ #
477
+ # Example:
478
+ #
479
+ # DB[:m1]
480
+ # merge_using(:m2, i1: :i2).
481
+ # merge_insert(i1: :i2, a: Sequel[:b]+11).
482
+ # merge_delete{a > 30}.
483
+ # merge_update(i1: Sequel[:i1]+:i2+10, a: Sequel[:a]+:b+20).
484
+ # merge
485
+ #
486
+ # SQL:
487
+ #
488
+ # MERGE INTO m1 USING m2 ON (i1 = i2)
489
+ # WHEN NOT MATCHED THEN INSERT (i1, a) VALUES (i2, (b + 11))
490
+ # WHEN MATCHED AND (a > 30) THEN DELETE
491
+ # WHEN MATCHED THEN UPDATE SET i1 = (i1 + i2 + 10), a = (a + b + 20)
492
+ #
493
+ # On PostgreSQL, two additional merge methods are supported, for the
494
+ # PostgreSQL-specific DO NOTHING syntax.
495
+ #
496
+ # * #merge_do_nothing_when_matched
497
+ # * #merge_do_nothing_when_not_matched
498
+ #
499
+ # This method is supported on Oracle, but Oracle's MERGE support is
500
+ # non-standard, and has the following issues:
501
+ #
502
+ # * DELETE clause requires UPDATE clause
503
+ # * DELETE clause requires a condition
504
+ # * DELETE clause only affects rows updated by UPDATE clause
505
+ def merge
506
+ execute_ddl(merge_sql)
507
+ end
508
+
460
509
  # Returns the minimum value for the given column/expression.
461
510
  # Uses a virtual row block if no argument is given.
462
511
  #
@@ -125,6 +125,11 @@ module Sequel
125
125
  false
126
126
  end
127
127
 
128
+ # Whether the MERGE statement is supported, false by default.
129
+ def supports_merge?
130
+ false
131
+ end
132
+
128
133
  # Whether modifying joined datasets is supported, false by default.
129
134
  def supports_modifying_joins?
130
135
  false
@@ -678,6 +678,56 @@ module Sequel
678
678
  clone(:lock => style)
679
679
  end
680
680
 
681
+ # Return a dataset with a WHEN MATCHED THEN DELETE clause added to the
682
+ # MERGE statement. If a block is passed, treat it as a virtual row and
683
+ # use it as additional conditions for the match.
684
+ #
685
+ # merge_delete
686
+ # # WHEN MATCHED THEN DELETE
687
+ #
688
+ # merge_delete{a > 30}
689
+ # # WHEN MATCHED AND (a > 30) THEN DELETE
690
+ def merge_delete(&block)
691
+ _merge_when(:type=>:delete, &block)
692
+ end
693
+
694
+ # Return a dataset with a WHEN NOT MATCHED THEN INSERT clause added to the
695
+ # MERGE statement. If a block is passed, treat it as a virtual row and
696
+ # use it as additional conditions for the match.
697
+ #
698
+ # The arguments provided can be any arguments that would be accepted by
699
+ # #insert.
700
+ #
701
+ # merge_insert(i1: :i2, a: Sequel[:b]+11)
702
+ # # WHEN NOT MATCHED THEN INSERT (i1, a) VALUES (i2, (b + 11))
703
+ #
704
+ # merge_insert(:i2, Sequel[:b]+11){a > 30}
705
+ # # WHEN NOT MATCHED AND (a > 30) THEN INSERT VALUES (i2, (b + 11))
706
+ def merge_insert(*values, &block)
707
+ _merge_when(:type=>:insert, :values=>values, &block)
708
+ end
709
+
710
+ # Return a dataset with a WHEN MATCHED THEN UPDATE clause added to the
711
+ # MERGE statement. If a block is passed, treat it as a virtual row and
712
+ # use it as additional conditions for the match.
713
+ #
714
+ # merge_update(i1: Sequel[:i1]+:i2+10, a: Sequel[:a]+:b+20)
715
+ # # WHEN MATCHED THEN UPDATE SET i1 = (i1 + i2 + 10), a = (a + b + 20)
716
+ #
717
+ # merge_update(i1: :i2){a > 30}
718
+ # # WHEN MATCHED AND (a > 30) THEN UPDATE SET i1 = i2
719
+ def merge_update(values, &block)
720
+ _merge_when(:type=>:update, :values=>values, &block)
721
+ end
722
+
723
+ # Return a dataset with the source and join condition to use for the MERGE statement.
724
+ #
725
+ # merge_using(:m2, i1: :i2)
726
+ # # USING m2 ON (i1 = i2)
727
+ def merge_using(source, join_condition)
728
+ clone(:merge_using => [source, join_condition].freeze)
729
+ end
730
+
681
731
  # Returns a cloned dataset without a row_proc.
682
732
  #
683
733
  # ds = DB[:items].with_row_proc(:invert.to_proc)
@@ -1287,6 +1337,18 @@ module Sequel
1287
1337
  end
1288
1338
  end
1289
1339
 
1340
+ # Append to the current MERGE WHEN clauses.
1341
+ # Mutates the hash to add the conditions, if a virtual row block is passed.
1342
+ def _merge_when(hash, &block)
1343
+ hash[:conditions] = Sequel.virtual_row(&block) if block
1344
+
1345
+ if merge_when = @opts[:merge_when]
1346
+ clone(:merge_when => (merge_when.dup << hash.freeze).freeze)
1347
+ else
1348
+ clone(:merge_when => [hash.freeze].freeze)
1349
+ end
1350
+ end
1351
+
1290
1352
  # Add the given filter condition. Arguments:
1291
1353
  # clause :: Symbol or which SQL clause to effect, should be :where or :having
1292
1354
  # cond :: The filter condition to add
@@ -24,29 +24,7 @@ module Sequel
24
24
 
25
25
  check_insert_allowed!
26
26
 
27
- columns = []
28
-
29
- case values.size
30
- when 0
31
- return insert_sql(OPTS)
32
- when 1
33
- case vals = values[0]
34
- when Hash
35
- values = []
36
- vals.each do |k,v|
37
- columns << k
38
- values << v
39
- end
40
- when Dataset, Array, LiteralString
41
- values = vals
42
- end
43
- when 2
44
- if (v0 = values[0]).is_a?(Array) && ((v1 = values[1]).is_a?(Array) || v1.is_a?(Dataset) || v1.is_a?(LiteralString))
45
- columns, values = v0, v1
46
- raise(Error, "Different number of values and columns given to insert_sql") if values.is_a?(Array) and columns.length != values.length
47
- end
48
- end
49
-
27
+ columns, values = _parse_insert_sql_args(values)
50
28
  if values.is_a?(Array) && values.empty? && !insert_supports_empty_values?
51
29
  columns, values = insert_empty_columns_values
52
30
  elsif values.is_a?(Dataset) && hoist_cte?(values) && supports_cte?(:insert)
@@ -112,6 +90,31 @@ module Sequel
112
90
  end
113
91
  end
114
92
 
93
+ # The SQL to use for the MERGE statement.
94
+ def merge_sql
95
+ raise Error, "This database doesn't support MERGE" unless supports_merge?
96
+ if sql = opts[:sql]
97
+ return static_sql(sql)
98
+ end
99
+ if sql = cache_get(:_merge_sql)
100
+ return sql
101
+ end
102
+ source, join_condition = @opts[:merge_using]
103
+ raise Error, "No USING clause for MERGE" unless source
104
+ sql = @opts[:append_sql] || sql_string_origin
105
+
106
+ select_with_sql(sql)
107
+ sql << "MERGE INTO "
108
+ source_list_append(sql, @opts[:from])
109
+ sql << " USING "
110
+ identifier_append(sql, source)
111
+ sql << " ON "
112
+ literal_append(sql, join_condition)
113
+ _merge_when_sql(sql)
114
+ cache_set(:_merge_sql, sql) if cache_sql?
115
+ sql
116
+ end
117
+
115
118
  # Returns an array of insert statements for inserting multiple records.
116
119
  # This method is used by +multi_insert+ to format insert statements and
117
120
  # expects a keys array and and an array of value arrays.
@@ -850,6 +853,83 @@ module Sequel
850
853
 
851
854
  private
852
855
 
856
+ # Append the INSERT sql used in a MERGE
857
+ def _merge_insert_sql(sql, data)
858
+ sql << " THEN INSERT"
859
+ columns, values = _parse_insert_sql_args(data[:values])
860
+ _insert_columns_sql(sql, columns)
861
+ _insert_values_sql(sql, values)
862
+ end
863
+
864
+ def _merge_update_sql(sql, data)
865
+ sql << " THEN UPDATE SET "
866
+ update_sql_values_hash(sql, data[:values])
867
+ end
868
+
869
+ def _merge_delete_sql(sql, data)
870
+ sql << " THEN DELETE"
871
+ end
872
+
873
+ # Mapping of merge types to related SQL
874
+ MERGE_TYPE_SQL = {
875
+ :insert => ' WHEN NOT MATCHED',
876
+ :delete => ' WHEN MATCHED',
877
+ :update => ' WHEN MATCHED',
878
+ :matched => ' WHEN MATCHED',
879
+ :not_matched => ' WHEN NOT MATCHED',
880
+ }.freeze
881
+ private_constant :MERGE_TYPE_SQL
882
+
883
+ # Add the WHEN clauses to the MERGE SQL
884
+ def _merge_when_sql(sql)
885
+ raise Error, "no WHEN [NOT] MATCHED clauses provided for MERGE" unless merge_when = @opts[:merge_when]
886
+ merge_when.each do |data|
887
+ type = data[:type]
888
+ sql << MERGE_TYPE_SQL[type]
889
+ _merge_when_conditions_sql(sql, data)
890
+ send(:"_merge_#{type}_sql", sql, data)
891
+ end
892
+ end
893
+
894
+ # Append MERGE WHEN conditions, if there are conditions provided.
895
+ def _merge_when_conditions_sql(sql, data)
896
+ if data.has_key?(:conditions)
897
+ sql << " AND "
898
+ literal_append(sql, data[:conditions])
899
+ end
900
+ end
901
+
902
+ # Parse the values passed to insert_sql, returning columns and values
903
+ # to use for the INSERT. Returned columns is always an array, but can be empty
904
+ # for an INSERT without explicit column references. Returned values can be an
905
+ # array, dataset, or literal string.
906
+ def _parse_insert_sql_args(values)
907
+ columns = []
908
+
909
+ case values.size
910
+ when 0
911
+ values = []
912
+ when 1
913
+ case vals = values[0]
914
+ when Hash
915
+ values = []
916
+ vals.each do |k,v|
917
+ columns << k
918
+ values << v
919
+ end
920
+ when Dataset, Array, LiteralString
921
+ values = vals
922
+ end
923
+ when 2
924
+ if (v0 = values[0]).is_a?(Array) && ((v1 = values[1]).is_a?(Array) || v1.is_a?(Dataset) || v1.is_a?(LiteralString))
925
+ columns, values = v0, v1
926
+ raise(Error, "Different number of values and columns given to insert_sql") if values.is_a?(Array) and columns.length != values.length
927
+ end
928
+ end
929
+
930
+ [columns, values]
931
+ end
932
+
853
933
  # Formats the truncate statement. Assumes the table given has already been
854
934
  # literalized.
855
935
  def _truncate_sql(table)
@@ -1165,7 +1245,10 @@ module Sequel
1165
1245
  end
1166
1246
 
1167
1247
  def insert_columns_sql(sql)
1168
- columns = opts[:columns]
1248
+ _insert_columns_sql(sql, opts[:columns])
1249
+ end
1250
+
1251
+ def _insert_columns_sql(sql, columns)
1169
1252
  if columns && !columns.empty?
1170
1253
  sql << ' ('
1171
1254
  identifier_list_append(sql, columns)
@@ -1184,7 +1267,11 @@ module Sequel
1184
1267
  end
1185
1268
 
1186
1269
  def insert_values_sql(sql)
1187
- case values = opts[:values]
1270
+ _insert_values_sql(sql, opts[:values])
1271
+ end
1272
+
1273
+ def _insert_values_sql(sql, values)
1274
+ case values
1188
1275
  when Array
1189
1276
  if values.empty?
1190
1277
  sql << " DEFAULT VALUES"
@@ -0,0 +1,139 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The is_distinct_from extension adds the ability to use the
4
+ # SQL standard IS DISTINCT FROM operator, which is similar to the
5
+ # not equals operator, except that NULL values are considered
6
+ # equal. Only PostgreSQL and H2 currently support this operator. On
7
+ # other databases, support is emulated.
8
+ #
9
+ # First, you need to load the extension into the database:
10
+ #
11
+ # DB.extension :is_distinct_from
12
+ #
13
+ # Then you can use the Sequel.is_distinct_from to create the expression
14
+ # objects:
15
+ #
16
+ # expr = Sequel.is_distinct_from(:column_a, :column_b)
17
+ # # (column_a IS DISTINCT FROM column_b)
18
+ #
19
+ # You can also use the +is_distinct_from+ method on most Sequel expressions:
20
+ #
21
+ # expr = Sequel[:column_a].is_distinct_from(:column_b)
22
+ # # (column_a IS DISTINCT FROM column_b)
23
+ #
24
+ # These expressions can be used in your datasets, or anywhere else that
25
+ # Sequel expressions are allowed:
26
+ #
27
+ # DB[:table].where(expr)
28
+ #
29
+ # Related module: Sequel::SQL::IsDistinctFrom
30
+
31
+ #
32
+ module Sequel
33
+ module SQL
34
+ module Builders
35
+ # Return a IsDistinctFrom expression object, using the IS DISTINCT FROM operator
36
+ # with the given left hand side and right hand side.
37
+ def is_distinct_from(lhs, rhs)
38
+ BooleanExpression.new(:NOOP, IsDistinctFrom.new(lhs, rhs))
39
+ end
40
+ end
41
+
42
+ # Represents an SQL expression using the IS DISTINCT FROM operator.
43
+ class IsDistinctFrom < GenericExpression
44
+ # These methods are added to expressions, allowing them to return IS DISTINCT
45
+ # FROM expressions based on the receiving expression.
46
+ module Methods
47
+ # Return a IsDistinctFrom expression, using the IS DISTINCT FROM operator,
48
+ # with the receiver as the left hand side and the argument as the right hand side.
49
+ def is_distinct_from(rhs)
50
+ BooleanExpression.new(:NOOP, IsDistinctFrom.new(self, rhs))
51
+ end
52
+ end
53
+
54
+ # These methods are added to datasets using the is_distinct_from extension
55
+ # extension, for the purposes of correctly literalizing IsDistinctFrom
56
+ # expressions for the appropriate database type.
57
+ module DatasetMethods
58
+ # Append the SQL fragment for the IS DISTINCT FROM expression to the SQL query.
59
+ def is_distinct_from_sql_append(sql, idf)
60
+ lhs = idf.lhs
61
+ rhs = idf.rhs
62
+
63
+ if supports_is_distinct_from?
64
+ sql << "("
65
+ literal_append(sql, lhs)
66
+ sql << " IS DISTINCT FROM "
67
+ literal_append(sql, rhs)
68
+ sql << ")"
69
+ elsif db.database_type == :derby && (lhs == nil || rhs == nil)
70
+ if lhs == nil && rhs == nil
71
+ sql << literal_false
72
+ elsif lhs == nil
73
+ literal_append(sql, ~Sequel.expr(rhs=>nil))
74
+ else
75
+ literal_append(sql, ~Sequel.expr(lhs=>nil))
76
+ end
77
+ else
78
+ literal_append(sql, Sequel.case({(Sequel.expr(lhs=>rhs) | [[lhs, nil], [rhs, nil]]) => 0}, 1) => 1)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # Whether the database supports IS DISTINCT FROM.
85
+ def supports_is_distinct_from?
86
+ if defined?(super)
87
+ return super
88
+ end
89
+
90
+ case db.database_type
91
+ when :postgres, :h2
92
+ true
93
+ else
94
+ false
95
+ end
96
+ end
97
+ end
98
+
99
+ # The left hand side of the IS DISTINCT FROM expression.
100
+ attr_reader :lhs
101
+
102
+ # The right hand side of the IS DISTINCT FROM expression.
103
+ attr_reader :rhs
104
+
105
+ def initialize(lhs, rhs)
106
+ @lhs = lhs
107
+ @rhs = rhs
108
+ end
109
+
110
+ to_s_method :is_distinct_from_sql
111
+ end
112
+ end
113
+
114
+ class SQL::GenericExpression
115
+ include SQL::IsDistinctFrom::Methods
116
+ end
117
+
118
+ class LiteralString
119
+ include SQL::IsDistinctFrom::Methods
120
+ end
121
+
122
+ Dataset.register_extension(:is_distinct_from, SQL::IsDistinctFrom::DatasetMethods)
123
+ end
124
+
125
+ # :nocov:
126
+ if Sequel.core_extensions?
127
+ class Symbol
128
+ include Sequel::SQL::IsDistinctFrom::Methods
129
+ end
130
+ end
131
+
132
+ if defined?(Sequel::CoreRefinements)
133
+ module Sequel::CoreRefinements
134
+ refine Symbol do
135
+ send INCLUDE_METH, Sequel::SQL::IsDistinctFrom::Methods
136
+ end
137
+ end
138
+ end
139
+ # :nocov:
@@ -76,7 +76,7 @@
76
76
  #
77
77
  # This extension integrates with the pg_array extension. If you plan
78
78
  # to use arrays of hstore types, load the pg_array extension before the
79
- # pg_interval extension:
79
+ # pg_hstore extension:
80
80
  #
81
81
  # DB.extension :pg_array, :pg_hstore
82
82
  #