sequel 5.23.0 → 5.28.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +62 -0
  3. data/README.rdoc +1 -1
  4. data/doc/cheat_sheet.rdoc +1 -0
  5. data/doc/postgresql.rdoc +2 -2
  6. data/doc/release_notes/5.24.0.txt +56 -0
  7. data/doc/release_notes/5.25.0.txt +32 -0
  8. data/doc/release_notes/5.26.0.txt +35 -0
  9. data/doc/release_notes/5.27.0.txt +21 -0
  10. data/doc/release_notes/5.28.0.txt +16 -0
  11. data/doc/testing.rdoc +11 -6
  12. data/lib/sequel/adapters/jdbc.rb +7 -1
  13. data/lib/sequel/adapters/jdbc/postgresql.rb +7 -13
  14. data/lib/sequel/adapters/mysql2.rb +0 -1
  15. data/lib/sequel/adapters/shared/mssql.rb +4 -2
  16. data/lib/sequel/adapters/shared/postgres.rb +30 -7
  17. data/lib/sequel/adapters/shared/sqlite.rb +7 -2
  18. data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -1
  19. data/lib/sequel/database/logging.rb +7 -1
  20. data/lib/sequel/database/schema_generator.rb +11 -2
  21. data/lib/sequel/database/schema_methods.rb +2 -0
  22. data/lib/sequel/dataset/features.rb +6 -0
  23. data/lib/sequel/dataset/query.rb +15 -2
  24. data/lib/sequel/dataset/sql.rb +17 -4
  25. data/lib/sequel/extensions/any_not_empty.rb +45 -0
  26. data/lib/sequel/extensions/exclude_or_null.rb +68 -0
  27. data/lib/sequel/extensions/pg_array_ops.rb +10 -6
  28. data/lib/sequel/extensions/pg_enum.rb +4 -1
  29. data/lib/sequel/extensions/pg_json.rb +1 -1
  30. data/lib/sequel/extensions/pg_json_ops.rb +124 -0
  31. data/lib/sequel/extensions/pg_range.rb +9 -0
  32. data/lib/sequel/extensions/sql_comments.rb +2 -2
  33. data/lib/sequel/model/base.rb +12 -5
  34. data/lib/sequel/plugins/association_multi_add_remove.rb +83 -0
  35. data/lib/sequel/plugins/caching.rb +3 -0
  36. data/lib/sequel/plugins/csv_serializer.rb +26 -9
  37. data/lib/sequel/plugins/dirty.rb +3 -9
  38. data/lib/sequel/plugins/nested_attributes.rb +7 -0
  39. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +89 -30
  40. data/lib/sequel/plugins/sharding.rb +11 -5
  41. data/lib/sequel/plugins/static_cache.rb +8 -3
  42. data/lib/sequel/plugins/static_cache_cache.rb +53 -0
  43. data/lib/sequel/plugins/typecast_on_load.rb +3 -2
  44. data/lib/sequel/sql.rb +4 -1
  45. data/lib/sequel/version.rb +1 -1
  46. data/spec/adapters/postgres_spec.rb +145 -17
  47. data/spec/adapters/sqlite_spec.rb +1 -1
  48. data/spec/bin_spec.rb +1 -1
  49. data/spec/core/database_spec.rb +20 -1
  50. data/spec/core/expression_filters_spec.rb +26 -7
  51. data/spec/core/schema_spec.rb +18 -0
  52. data/spec/core/spec_helper.rb +1 -1
  53. data/spec/core_extensions_spec.rb +1 -1
  54. data/spec/extensions/any_not_empty_spec.rb +23 -0
  55. data/spec/extensions/association_multi_add_remove_spec.rb +1041 -0
  56. data/spec/extensions/dirty_spec.rb +33 -0
  57. data/spec/extensions/exclude_or_null_spec.rb +15 -0
  58. data/spec/extensions/insert_conflict_spec.rb +26 -0
  59. data/spec/extensions/nested_attributes_spec.rb +48 -0
  60. data/spec/extensions/pg_array_ops_spec.rb +3 -3
  61. data/spec/extensions/pg_auto_constraint_validations_spec.rb +37 -0
  62. data/spec/extensions/pg_json_ops_spec.rb +67 -0
  63. data/spec/extensions/pg_range_spec.rb +35 -21
  64. data/spec/extensions/sharding_spec.rb +8 -0
  65. data/spec/extensions/spec_helper.rb +1 -1
  66. data/spec/extensions/static_cache_cache_spec.rb +35 -0
  67. data/spec/guards_helper.rb +1 -1
  68. data/spec/integration/dataset_test.rb +57 -17
  69. data/spec/integration/plugin_test.rb +1 -1
  70. data/spec/integration/schema_test.rb +9 -0
  71. data/spec/integration/spec_helper.rb +7 -1
  72. data/spec/model/spec_helper.rb +1 -1
  73. metadata +35 -3
@@ -869,9 +869,9 @@ module Sequel
869
869
  end
870
870
  end
871
871
 
872
- # SQLite does not natively support NULLS FIRST/LAST.
872
+ # SQLite supports NULLS FIRST/LAST natively in 3.30+.
873
873
  def requires_emulating_nulls_first?
874
- true
874
+ db.sqlite_version < 33000
875
875
  end
876
876
 
877
877
  # SQLite does not support FOR UPDATE, but silently ignore it
@@ -897,6 +897,11 @@ module Sequel
897
897
  false
898
898
  end
899
899
 
900
+ # SQLite 3.30 supports the FILTER clause for aggregate functions.
901
+ def supports_filtered_aggregates?
902
+ db.sqlite_version >= 33000
903
+ end
904
+
900
905
  # SQLite supports quoted function names.
901
906
  def supports_quoted_function_names?
902
907
  true
@@ -55,7 +55,7 @@ module Sequel
55
55
  NotNullConstraintViolation
56
56
  when 1062
57
57
  UniqueConstraintViolation
58
- when 1451, 1452
58
+ when 1451, 1452, 1216, 1217
59
59
  ForeignKeyConstraintViolation
60
60
  when 4025
61
61
  CheckConstraintViolation
@@ -35,7 +35,7 @@ module Sequel
35
35
  # Yield to the block, logging any errors at error level to all loggers,
36
36
  # and all other queries with the duration at warn or info level.
37
37
  def log_connection_yield(sql, conn, args=nil)
38
- return yield if @loggers.empty?
38
+ return yield if skip_logging?
39
39
  sql = "#{connection_info(conn) if conn && log_connection_info}#{sql}#{"; #{args.inspect}" if args}"
40
40
  timer = Sequel.start_timer
41
41
 
@@ -58,6 +58,12 @@ module Sequel
58
58
 
59
59
  private
60
60
 
61
+ # Determine if logging should be skipped. Defaults to true if no loggers
62
+ # have been specified.
63
+ def skip_logging?
64
+ @loggers.empty?
65
+ end
66
+
61
67
  # String including information about the connection, for use when logging
62
68
  # connection info.
63
69
  def connection_info(conn)
@@ -110,6 +110,9 @@ module Sequel
110
110
  # yet exist on referenced table (but will exist before the transaction commits).
111
111
  # Basically it adds DEFERRABLE INITIALLY DEFERRED on key creation.
112
112
  # If you use :immediate as the value, uses DEFERRABLE INITIALLY IMMEDIATE.
113
+ # :generated_always_as :: Specify a GENERATED ALWAYS AS column expression,
114
+ # if generated columns are supported (PostgreSQL 12+, MariaDB 5.2.0+,
115
+ # and MySQL 5.7.6+).
113
116
  # :index :: Create an index on this column. If given a hash, use the hash as the
114
117
  # options for the index.
115
118
  # :key :: For foreign key columns, the column in the associated table
@@ -126,15 +129,21 @@ module Sequel
126
129
  # be used if you have a single, nonautoincrementing primary key column
127
130
  # (use the primary_key method in that case).
128
131
  # :primary_key_constraint_name :: The name to give the primary key constraint
132
+ # :primary_key_deferrable :: Similar to :deferrable, but for the primary key constraint
133
+ # if :primary_key is used.
129
134
  # :type :: Overrides the type given as the argument. Generally not used by column
130
135
  # itself, but can be passed as an option to other methods that call column.
131
136
  # :unique :: Mark the column as unique, generally has the same effect as
132
137
  # creating a unique index on the column.
133
138
  # :unique_constraint_name :: The name to give the unique key constraint
139
+ # :unique_deferrable :: Similar to :deferrable, but for the unique constraint if :unique
140
+ # is used.
141
+ #
142
+ # PostgreSQL specific options:
143
+ #
144
+ # :identity :: Create an identity column.
134
145
  #
135
146
  # MySQL specific options:
136
- # :generated_always_as :: Specify a GENERATED ALWAYS AS column expression,
137
- # if generated columns are supported.
138
147
  # :generated_type :: Set the type of column when using :generated_always_as,
139
148
  # should be :virtual or :stored to force a type.
140
149
  def column(name, type, opts = OPTS)
@@ -586,6 +586,7 @@ module Sequel
586
586
  sql << " CONSTRAINT #{quote_identifier(name)}"
587
587
  end
588
588
  sql << ' PRIMARY KEY'
589
+ constraint_deferrable_sql_append(sql, column[:primary_key_deferrable])
589
590
  end
590
591
  end
591
592
 
@@ -606,6 +607,7 @@ module Sequel
606
607
  sql << " CONSTRAINT #{quote_identifier(name)}"
607
608
  end
608
609
  sql << ' UNIQUE'
610
+ constraint_deferrable_sql_append(sql, column[:unique_deferrable])
609
611
  end
610
612
  end
611
613
 
@@ -230,6 +230,12 @@ module Sequel
230
230
  supports_cte_in_subqueries?
231
231
  end
232
232
 
233
+ # Whether the dataset supports the FILTER clause for aggregate functions.
234
+ # If not, support is emulated using CASE.
235
+ def supports_filtered_aggregates?
236
+ false
237
+ end
238
+
233
239
  # Whether the database supports quoting function names.
234
240
  def supports_quoted_function_names?
235
241
  false
@@ -1237,6 +1237,20 @@ module Sequel
1237
1237
  self
1238
1238
  end
1239
1239
 
1240
+ # If invert is true, invert the condition.
1241
+ def _invert_filter(cond, invert)
1242
+ if invert
1243
+ SQL::BooleanExpression.invert(cond)
1244
+ else
1245
+ cond
1246
+ end
1247
+ end
1248
+
1249
+ # Add the given filter condition. Arguments:
1250
+ # clause :: Symbol or which SQL clause to effect, should be :where or :having
1251
+ # cond :: The filter condition to add
1252
+ # invert :: Whether the condition should be inverted (true or false)
1253
+ # combine :: How to combine the condition with an existing condition, should be :AND or :OR
1240
1254
  def add_filter(clause, cond, invert=false, combine=:AND, &block)
1241
1255
  if cond == EMPTY_ARRAY && !block
1242
1256
  raise Error, "must provide an argument to a filtering method if not passing a block"
@@ -1256,8 +1270,7 @@ module Sequel
1256
1270
  cond = nil
1257
1271
  end
1258
1272
 
1259
- cond = filter_expr(cond, &block)
1260
- cond = SQL::BooleanExpression.invert(cond) if invert
1273
+ cond = _invert_filter(filter_expr(cond, &block), invert)
1261
1274
  cond = SQL::BooleanExpression.new(combine, @opts[clause], cond) if @opts[clause]
1262
1275
 
1263
1276
  if cond.nil?
@@ -492,11 +492,24 @@ module Sequel
492
492
  end
493
493
 
494
494
  sql << '('
495
+ if filter = opts[:filter]
496
+ filter = filter_expr(filter, &opts[:filter_block])
497
+ end
495
498
  if opts[:*]
496
- sql << '*'
499
+ if filter && !supports_filtered_aggregates?
500
+ literal_append(sql, Sequel.case({filter=>1}, nil))
501
+ filter = nil
502
+ else
503
+ sql << '*'
504
+ end
497
505
  else
498
506
  sql << "DISTINCT " if opts[:distinct]
499
- expression_list_append(sql, f.args)
507
+ if filter && !supports_filtered_aggregates?
508
+ expression_list_append(sql, f.args.map{|arg| Sequel.case({filter=>arg}, nil)})
509
+ filter = nil
510
+ else
511
+ expression_list_append(sql, f.args)
512
+ end
500
513
  if order = opts[:order]
501
514
  sql << " ORDER BY "
502
515
  expression_list_append(sql, order)
@@ -510,9 +523,9 @@ module Sequel
510
523
  sql << ')'
511
524
  end
512
525
 
513
- if filter = opts[:filter]
526
+ if filter
514
527
  sql << " FILTER (WHERE "
515
- literal_append(sql, filter_expr(filter, &opts[:filter_block]))
528
+ literal_append(sql, filter)
516
529
  sql << ')'
517
530
  end
518
531
 
@@ -0,0 +1,45 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The any_not_empty extension changes the behavior of Dataset#any?
4
+ # if called without a block. By default, this method uses the
5
+ # standard Enumerable behavior of enumerating results and seeing
6
+ # if any result is not false or nil. With this extension, it
7
+ # just checks whether the dataset is empty. This approach can
8
+ # be much faster if the dataset is currently large.
9
+ #
10
+ # DB[:table].any?
11
+ # # SELECT * FROM table
12
+ #
13
+ # DB[:table].extension(:any_not_empty).any?
14
+ # # SELECT 1 as one FROM table LIMIT 1
15
+ #
16
+ # You can load this extension into specific datasets:
17
+ #
18
+ # ds = DB[:table]
19
+ # ds = ds.extension(:any_not_empty)
20
+ #
21
+ # Or you can load it into all of a database's datasets, which
22
+ # is probably the desired behavior if you are using this extension:
23
+ #
24
+ # DB.extension(:any_not_empty)
25
+ #
26
+ # Note that this can result in any? returning a different result if
27
+ # the dataset has a row_proc that can return false or nil.
28
+ #
29
+ # Related module: Sequel::AnyNotEmpty
30
+
31
+ #
32
+ module Sequel
33
+ module AnyNotEmpty
34
+ # If a block is not given, return whether the dataset is not empty.
35
+ def any?
36
+ if block_given?
37
+ super
38
+ else
39
+ !empty?
40
+ end
41
+ end
42
+ end
43
+
44
+ Dataset.register_extension(:any_not_empty, AnyNotEmpty)
45
+ end
@@ -0,0 +1,68 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The exclude_or_null extension adds Dataset#exclude_or_null and
4
+ # Dataset#exclude_or_null_having. These methods are similar to
5
+ # Dataset#exclude and Dataset#exclude_having, except that they
6
+ # will also exclude rows where the condition IS NULL.
7
+ #
8
+ # DB[:table].exclude_or_null(foo: 1)
9
+ # # SELECT * FROM table WHERE NOT coalesce((foo = 1), false)
10
+ #
11
+ # DB[:table].exclude_or_null{foo(bar) =~ 1}
12
+ # # SELECT * FROM table HAVING NOT coalesce((foo(bar) = 1), false))
13
+ #
14
+ # You can load this extension into specific datasets:
15
+ #
16
+ # ds = DB[:table]
17
+ # ds = ds.extension(:exclude_or_null)
18
+ #
19
+ # Or you can load it into all of a database's datasets, which
20
+ # is probably the desired behavior if you are using this extension:
21
+ #
22
+ # DB.extension(:exclude_or_null)
23
+ #
24
+ # Note, this extension works correctly on PostgreSQL, SQLite, MySQL,
25
+ # H2, and HSQLDB. However, it does not work correctly on Microsoft SQL Server,
26
+ # Oracle, DB2, SQLAnywhere, or Derby.
27
+ #
28
+ # Related module: Sequel::ExcludeOrNull
29
+
30
+ #
31
+ module Sequel
32
+ module ExcludeOrNull
33
+ # Performs the inverse of Dataset#where, but also excludes rows where the given
34
+ # condition IS NULL.
35
+ #
36
+ # DB[:items].exclude_or_null(category: 'software')
37
+ # # SELECT * FROM items WHERE NOT coalesce((category = 'software'), false)
38
+ #
39
+ # DB[:items].exclude_or_null(category: 'software', id: 3)
40
+ # # SELECT * FROM items WHERE NOT coalesce(((category = 'software') AND (id = 3)), false)
41
+ def exclude_or_null(*cond, &block)
42
+ add_filter(:where, cond, :or_null, &block)
43
+ end
44
+
45
+ # The same as exclude_or_null, but affecting the HAVING clause instead of the
46
+ # WHERE clause.
47
+ #
48
+ # DB[:items].select_group(:name).exclude_or_null_having{count(name) < 2}
49
+ # # SELECT name FROM items GROUP BY name HAVING NOT coalesce((count(name) < 2), true)
50
+ def exclude_or_null_having(*cond, &block)
51
+ add_filter(:having, cond, :or_null, &block)
52
+ end
53
+
54
+ private
55
+
56
+ # Recognize :or_null value for invert, returning an expression for
57
+ # the invert of the condition or the condition being null.
58
+ def _invert_filter(cond, invert)
59
+ if invert == :or_null
60
+ ~SQL::Function.new(:coalesce, cond, SQL::Constants::SQLFALSE)
61
+ else
62
+ super
63
+ end
64
+ end
65
+ end
66
+
67
+ Dataset.register_extension(:exclude_or_null, ExcludeOrNull)
68
+ end
@@ -51,8 +51,8 @@
51
51
  # ia.length(2) # array_length(int_array_column, 2)
52
52
  # ia.lower # array_lower(int_array_column, 1)
53
53
  # ia.lower(2) # array_lower(int_array_column, 2)
54
- # ia.join # array_to_string(int_array_column, '', NULL)
55
- # ia.join(':') # array_to_string(int_array_column, ':', NULL)
54
+ # ia.join # array_to_string(int_array_column, '')
55
+ # ia.join(':') # array_to_string(int_array_column, ':')
56
56
  # ia.join(':', ' ') # array_to_string(int_array_column, ':', ' ')
57
57
  # ia.unnest # unnest(int_array_column)
58
58
  # ia.unnest(:b) # unnest(int_array_column, b)
@@ -217,12 +217,16 @@ module Sequel
217
217
 
218
218
  # Call the array_to_string method:
219
219
  #
220
- # array_op.join # array_to_string(array, '', NULL)
221
- # array_op.to_string # array_to_string(array, '', NULL)
222
- # array_op.join(":") # array_to_string(array, ':', NULL)
220
+ # array_op.join # array_to_string(array, '')
221
+ # array_op.to_string # array_to_string(array, '')
222
+ # array_op.join(":") # array_to_string(array, ':')
223
223
  # array_op.join(":", "*") # array_to_string(array, ':', '*')
224
224
  def to_string(joiner="", null=nil)
225
- function(:array_to_string, joiner, null)
225
+ if null.nil?
226
+ function(:array_to_string, joiner)
227
+ else
228
+ function(:array_to_string, joiner, null)
229
+ end
226
230
  end
227
231
  alias join to_string
228
232
 
@@ -133,8 +133,11 @@ module Sequel
133
133
  # the pg_type table to get names and array oids for
134
134
  # enums.
135
135
  def parse_enum_labels
136
+ order = [:enumtypid]
137
+ order << :enumsortorder if server_version >= 90100
138
+
136
139
  enum_labels = metadata_dataset.from(:pg_enum).
137
- order(:enumtypid, :enumsortorder).
140
+ order(*order).
138
141
  select_hash_groups(Sequel.cast(:enumtypid, Integer).as(:v), :enumlabel).freeze
139
142
  enum_labels.each_value(&:freeze)
140
143
 
@@ -12,7 +12,7 @@
12
12
  #
13
13
  # Note that wrapping JSON primitives changes the behavior for
14
14
  # JSON false and null values. Because only +false+ and +nil+
15
- # in Ruby are considered falesy, wrapping these objects results
15
+ # in Ruby are considered falsey, wrapping these objects results
16
16
  # in unexpected behavior if you use the values directly in
17
17
  # conditionals:
18
18
  #
@@ -73,6 +73,23 @@
73
73
  # j.pretty # jsonb_pretty(jsonb_column)
74
74
  # j.set(%w'0 a', :h) # jsonb_set(jsonb_column, ARRAY['0','a'], h, true)
75
75
  #
76
+ # On PostgreSQL 12+ SQL/JSON functions and operators are supported:
77
+ #
78
+ # j.path_exists('$.foo') # (jsonb_column @? '$.foo')
79
+ # j.path_match('$.foo') # (jsonb_column @@ '$.foo')
80
+ #
81
+ # j.path_exists!('$.foo') # jsonb_path_exists(jsonb_column, '$.foo')
82
+ # j.path_match!('$.foo') # jsonb_path_match(jsonb_column, '$.foo')
83
+ # j.path_query('$.foo') # jsonb_path_query(jsonb_column, '$.foo')
84
+ # j.path_query_array('$.foo') # jsonb_path_query_array(jsonb_column, '$.foo')
85
+ # j.path_query_first('$.foo') # jsonb_path_query_first(jsonb_column, '$.foo')
86
+ #
87
+ # For the PostgreSQL 12+ SQL/JSON functions, one argument is required (+path+) and
88
+ # two more arguments are optional (+vars+ and +silent+). +path+ specifies the JSON path.
89
+ # +vars+ specifies a hash or a string in JSON format of named variables to be
90
+ # substituted in +path+. +silent+ specifies whether errors are suppressed. By default,
91
+ # errors are not suppressed.
92
+ #
76
93
  # If you are also using the pg_json extension, you should load it before
77
94
  # loading this extension. Doing so will allow you to use the #op method on
78
95
  # JSONHash, JSONHarray, JSONBHash, and JSONBArray, allowing you to perform json/jsonb operations
@@ -292,6 +309,8 @@ module Sequel
292
309
  CONTAINED_BY = ["(".freeze, " <@ ".freeze, ")".freeze].freeze
293
310
  DELETE_PATH = ["(".freeze, " #- ".freeze, ")".freeze].freeze
294
311
  HAS_KEY = ["(".freeze, " ? ".freeze, ")".freeze].freeze
312
+ PATH_EXISTS = ["(".freeze, " @? ".freeze, ")".freeze].freeze
313
+ PATH_MATCH = ["(".freeze, " @@ ".freeze, ")".freeze].freeze
295
314
 
296
315
  # jsonb expression for deletion of the given argument from the
297
316
  # current jsonb.
@@ -362,6 +381,95 @@ module Sequel
362
381
  self.class.new(function(:insert, wrap_input_array(path), wrap_input_jsonb(other), insert_after))
363
382
  end
364
383
 
384
+ # Returns whether the JSON path returns any item for the json object.
385
+ #
386
+ # json_op.path_exists("$.foo") # (json @? '$.foo')
387
+ def path_exists(path)
388
+ bool_op(PATH_EXISTS, path)
389
+ end
390
+
391
+ # Returns whether the JSON path returns any item for the json object.
392
+ #
393
+ # json_op.path_exists!("$.foo")
394
+ # # jsonb_path_exists(json, '$.foo')
395
+ #
396
+ # json_op.path_exists!("$.foo ? ($ > $x)", x: 2)
397
+ # # jsonb_path_exists(json, '$.foo ? ($ > $x)', '{"x":2}')
398
+ #
399
+ # json_op.path_exists!("$.foo ? ($ > $x)", {x: 2}, true)
400
+ # # jsonb_path_exists(json, '$.foo ? ($ > $x)', '{"x":2}', true)
401
+ def path_exists!(path, vars=nil, silent=nil)
402
+ Sequel::SQL::BooleanExpression.new(:NOOP, _path_function(:jsonb_path_exists, path, vars, silent))
403
+ end
404
+
405
+ # Returns the first item of the result of JSON path predicate check for the json object.
406
+ # Returns nil if the first item is not true or false.
407
+ #
408
+ # json_op.path_match("$.foo") # (json @@ '$.foo')
409
+ def path_match(path)
410
+ bool_op(PATH_MATCH, path)
411
+ end
412
+
413
+ # Returns the first item of the result of JSON path predicate check for the json object.
414
+ # Returns nil if the first item is not true or false and silent is true.
415
+ #
416
+ # json_op.path_match!("$.foo")
417
+ # # jsonb_path_match(json, '$.foo')
418
+ #
419
+ # json_op.path_match!("$.foo ? ($ > $x)", x: 2)
420
+ # # jsonb_path_match(json, '$.foo ? ($ > $x)', '{"x":2}')
421
+ #
422
+ # json_op.path_match!("$.foo ? ($ > $x)", {x: 2}, true)
423
+ # # jsonb_path_match(json, '$.foo ? ($ > $x)', '{"x":2}', true)
424
+ def path_match!(path, vars=nil, silent=nil)
425
+ Sequel::SQL::BooleanExpression.new(:NOOP, _path_function(:jsonb_path_match, path, vars, silent))
426
+ end
427
+
428
+ # Returns a set of all jsonb values specified by the JSON path
429
+ # for the json object.
430
+ #
431
+ # json_op.path_query("$.foo")
432
+ # # jsonb_path_query(json, '$.foo')
433
+ #
434
+ # json_op.path_query("$.foo ? ($ > $x)", x: 2)
435
+ # # jsonb_path_query(json, '$.foo ? ($ > $x)', '{"x":2}')
436
+ #
437
+ # json_op.path_query("$.foo ? ($ > $x)", {x: 2}, true)
438
+ # # jsonb_path_query(json, '$.foo ? ($ > $x)', '{"x":2}', true)
439
+ def path_query(path, vars=nil, silent=nil)
440
+ _path_function(:jsonb_path_query, path, vars, silent)
441
+ end
442
+
443
+ # Returns a jsonb array of all values specified by the JSON path
444
+ # for the json object.
445
+ #
446
+ # json_op.path_query_array("$.foo")
447
+ # # jsonb_path_query_array(json, '$.foo')
448
+ #
449
+ # json_op.path_query_array("$.foo ? ($ > $x)", x: 2)
450
+ # # jsonb_path_query_array(json, '$.foo ? ($ > $x)', '{"x":2}')
451
+ #
452
+ # json_op.path_query_array("$.foo ? ($ > $x)", {x: 2}, true)
453
+ # # jsonb_path_query_array(json, '$.foo ? ($ > $x)', '{"x":2}', true)
454
+ def path_query_array(path, vars=nil, silent=nil)
455
+ JSONBOp.new(_path_function(:jsonb_path_query_array, path, vars, silent))
456
+ end
457
+
458
+ # Returns the first item of the result specified by the JSON path
459
+ # for the json object.
460
+ #
461
+ # json_op.path_query_first("$.foo")
462
+ # # jsonb_path_query_first(json, '$.foo')
463
+ #
464
+ # json_op.path_query_first("$.foo ? ($ > $x)", x: 2)
465
+ # # jsonb_path_query_first(json, '$.foo ? ($ > $x)', '{"x":2}')
466
+ #
467
+ # json_op.path_query_first("$.foo ? ($ > $x)", {x: 2}, true)
468
+ # # jsonb_path_query_first(json, '$.foo ? ($ > $x)', '{"x":2}', true)
469
+ def path_query_first(path, vars=nil, silent=nil)
470
+ JSONBOp.new(_path_function(:jsonb_path_query_first, path, vars, silent))
471
+ end
472
+
365
473
  # Return the receiver, since it is already a JSONBOp.
366
474
  def pg_jsonb
367
475
  self
@@ -386,6 +494,22 @@ module Sequel
386
494
 
387
495
  private
388
496
 
497
+ # Internals of the jsonb SQL/JSON path functions.
498
+ def _path_function(func, path, vars, silent)
499
+ args = []
500
+ if vars
501
+ if vars.is_a?(Hash)
502
+ vars = vars.to_json
503
+ end
504
+ args << vars
505
+
506
+ unless silent.nil?
507
+ args << silent
508
+ end
509
+ end
510
+ SQL::Function.new(func, self, path, *args)
511
+ end
512
+
389
513
  # Return a placeholder literal with the given str and args, wrapped
390
514
  # in a boolean expression, used by operators that return booleans.
391
515
  def bool_op(str, other)