sequel 5.70.0 → 5.80.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +134 -0
  3. data/README.rdoc +7 -5
  4. data/doc/dataset_basics.rdoc +1 -1
  5. data/doc/mass_assignment.rdoc +1 -1
  6. data/doc/migration.rdoc +15 -0
  7. data/doc/opening_databases.rdoc +6 -2
  8. data/doc/querying.rdoc +6 -1
  9. data/doc/release_notes/5.71.0.txt +21 -0
  10. data/doc/release_notes/5.72.0.txt +33 -0
  11. data/doc/release_notes/5.73.0.txt +66 -0
  12. data/doc/release_notes/5.74.0.txt +45 -0
  13. data/doc/release_notes/5.75.0.txt +35 -0
  14. data/doc/release_notes/5.76.0.txt +86 -0
  15. data/doc/release_notes/5.77.0.txt +63 -0
  16. data/doc/release_notes/5.78.0.txt +67 -0
  17. data/doc/release_notes/5.79.0.txt +28 -0
  18. data/doc/release_notes/5.80.0.txt +40 -0
  19. data/doc/schema_modification.rdoc +2 -2
  20. data/doc/testing.rdoc +4 -2
  21. data/lib/sequel/adapters/ibmdb.rb +1 -1
  22. data/lib/sequel/adapters/jdbc/h2.rb +3 -0
  23. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -0
  24. data/lib/sequel/adapters/jdbc/postgresql.rb +3 -0
  25. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
  26. data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
  27. data/lib/sequel/adapters/jdbc.rb +10 -6
  28. data/lib/sequel/adapters/mysql2.rb +2 -2
  29. data/lib/sequel/adapters/odbc/mssql.rb +1 -1
  30. data/lib/sequel/adapters/postgres.rb +6 -5
  31. data/lib/sequel/adapters/shared/db2.rb +12 -0
  32. data/lib/sequel/adapters/shared/mssql.rb +30 -2
  33. data/lib/sequel/adapters/shared/mysql.rb +68 -3
  34. data/lib/sequel/adapters/shared/oracle.rb +4 -6
  35. data/lib/sequel/adapters/shared/postgres.rb +116 -6
  36. data/lib/sequel/adapters/shared/sqlanywhere.rb +10 -4
  37. data/lib/sequel/adapters/shared/sqlite.rb +20 -3
  38. data/lib/sequel/adapters/sqlite.rb +42 -3
  39. data/lib/sequel/connection_pool.rb +4 -2
  40. data/lib/sequel/database/misc.rb +3 -2
  41. data/lib/sequel/database/schema_methods.rb +11 -4
  42. data/lib/sequel/database/transactions.rb +6 -0
  43. data/lib/sequel/dataset/actions.rb +8 -6
  44. data/lib/sequel/dataset/dataset_module.rb +1 -1
  45. data/lib/sequel/dataset/features.rb +10 -1
  46. data/lib/sequel/dataset/graph.rb +1 -0
  47. data/lib/sequel/dataset/query.rb +58 -9
  48. data/lib/sequel/dataset/sql.rb +47 -34
  49. data/lib/sequel/exceptions.rb +5 -0
  50. data/lib/sequel/extensions/any_not_empty.rb +2 -2
  51. data/lib/sequel/extensions/async_thread_pool.rb +7 -0
  52. data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
  53. data/lib/sequel/extensions/caller_logging.rb +2 -1
  54. data/lib/sequel/extensions/duplicate_columns_handler.rb +10 -9
  55. data/lib/sequel/extensions/index_caching.rb +5 -1
  56. data/lib/sequel/extensions/migration.rb +64 -14
  57. data/lib/sequel/extensions/named_timezones.rb +1 -1
  58. data/lib/sequel/extensions/pg_array.rb +10 -0
  59. data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
  60. data/lib/sequel/extensions/pg_extended_date_support.rb +4 -4
  61. data/lib/sequel/extensions/pg_json_ops.rb +52 -0
  62. data/lib/sequel/extensions/pg_range.rb +2 -2
  63. data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
  64. data/lib/sequel/extensions/provenance.rb +108 -0
  65. data/lib/sequel/extensions/round_timestamps.rb +1 -1
  66. data/lib/sequel/extensions/schema_caching.rb +1 -1
  67. data/lib/sequel/extensions/sqlite_json_ops.rb +76 -18
  68. data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
  69. data/lib/sequel/model/associations.rb +9 -2
  70. data/lib/sequel/model/base.rb +26 -13
  71. data/lib/sequel/model/exceptions.rb +15 -3
  72. data/lib/sequel/plugins/column_encryption.rb +27 -6
  73. data/lib/sequel/plugins/defaults_setter.rb +16 -0
  74. data/lib/sequel/plugins/list.rb +5 -2
  75. data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
  76. data/lib/sequel/plugins/optimistic_locking.rb +9 -42
  77. data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
  78. data/lib/sequel/plugins/paged_operations.rb +181 -0
  79. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +5 -1
  80. data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
  81. data/lib/sequel/plugins/rcte_tree.rb +7 -4
  82. data/lib/sequel/plugins/static_cache_cache.rb +5 -1
  83. data/lib/sequel/plugins/validation_helpers.rb +1 -1
  84. data/lib/sequel/version.rb +1 -1
  85. metadata +44 -3
@@ -0,0 +1,110 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The pg_auto_parameterize_in_array extension builds on the pg_auto_parameterize
4
+ # extension, adding support for handling additional types when converting from
5
+ # IN to = ANY and NOT IN to != ALL:
6
+ #
7
+ # DB[:table].where(column: [1.0, 2.0, ...])
8
+ # # Without extension: column IN ($1::numeric, $2:numeric, ...) # bound variables: 1.0, 2.0, ...
9
+ # # With extension: column = ANY($1::numeric[]) # bound variables: [1.0, 2.0, ...]
10
+ #
11
+ # This prevents the use of an unbounded number of bound variables based on the
12
+ # size of the array, as well as using different SQL for different array sizes.
13
+ #
14
+ # The following types are supported when doing the conversions, with the database
15
+ # type used:
16
+ #
17
+ # Float :: if any are infinite or NaN, double precision, otherwise numeric
18
+ # BigDecimal :: numeric
19
+ # Date :: date
20
+ # Time :: timestamp (or timestamptz if pg_timestamptz extension is used)
21
+ # DateTime :: timestamp (or timestamptz if pg_timestamptz extension is used)
22
+ # Sequel::SQLTime :: time
23
+ # Sequel::SQL::Blob :: bytea
24
+ #
25
+ # String values are also supported using the +text+ type, but only if the
26
+ # +:treat_string_list_as_text_array+ Database option is used. This is because
27
+ # treating strings as text can break programs, since the type for
28
+ # literal strings in PostgreSQL is +unknown+, not +text+.
29
+ #
30
+ # The conversion is only done for single dimensional arrays that have more
31
+ # than two elements, where all elements are of the same class (other than
32
+ # nil values).
33
+ #
34
+ # Related module: Sequel::Postgres::AutoParameterizeInArray
35
+
36
+ module Sequel
37
+ module Postgres
38
+ # Enable automatically parameterizing queries.
39
+ module AutoParameterizeInArray
40
+ # Transform column IN (...) expressions into column = ANY($)
41
+ # and column NOT IN (...) expressions into column != ALL($)
42
+ # using an array bound variable for the ANY/ALL argument,
43
+ # if all values inside the predicate are of the same type and
44
+ # the type is handled by the extension.
45
+ # This is the same optimization PostgreSQL performs internally,
46
+ # but this reduces the number of bound variables.
47
+ def complex_expression_sql_append(sql, op, args)
48
+ case op
49
+ when :IN, :"NOT IN"
50
+ l, r = args
51
+ if auto_param?(sql) && (type = _bound_variable_type_for_array(r))
52
+ if op == :IN
53
+ op = :"="
54
+ func = :ANY
55
+ else
56
+ op = :!=
57
+ func = :ALL
58
+ end
59
+ args = [l, Sequel.function(func, Sequel.pg_array(r, type))]
60
+ end
61
+ end
62
+
63
+ super
64
+ end
65
+
66
+ private
67
+
68
+ # The bound variable type string to use for the bound variable array.
69
+ # Returns nil if a bound variable should not be used for the array.
70
+ def _bound_variable_type_for_array(r)
71
+ return unless Array === r && r.size > 1
72
+ classes = r.map(&:class)
73
+ classes.uniq!
74
+ classes.delete(NilClass)
75
+ return unless classes.size == 1
76
+
77
+ klass = classes[0]
78
+ if klass == Integer
79
+ # This branch is not taken on Ruby <2.4, because of the Fixnum/Bignum split.
80
+ # However, that causes no problems as pg_auto_parameterize handles integer
81
+ # arrays natively (though the SQL used is different)
82
+ "int8"
83
+ elsif klass == String
84
+ "text" if db.typecast_value(:boolean, db.opts[:treat_string_list_as_text_array])
85
+ elsif klass == BigDecimal
86
+ "numeric"
87
+ elsif klass == Date
88
+ "date"
89
+ elsif klass == Time
90
+ @db.cast_type_literal(Time)
91
+ elsif klass == Float
92
+ # PostgreSQL treats literal floats as numeric, not double precision
93
+ # But older versions of PostgreSQL don't handle Infinity/NaN in numeric
94
+ r.all?{|v| v.nil? || v.finite?} ? "numeric" : "double precision"
95
+ elsif klass == Sequel::SQLTime
96
+ "time"
97
+ elsif klass == DateTime
98
+ @db.cast_type_literal(DateTime)
99
+ elsif klass == Sequel::SQL::Blob
100
+ "bytea"
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ Database.register_extension(:pg_auto_parameterize_in_array) do |db|
107
+ db.extension(:pg_array, :pg_auto_parameterize)
108
+ db.extend_datasets(Postgres::AutoParameterizeInArray)
109
+ end
110
+ end
@@ -53,8 +53,8 @@ module Sequel
53
53
  # on jdbc.
54
54
  def bound_variable_arg(arg, conn)
55
55
  case arg
56
- when Date, Time
57
- literal(arg)
56
+ when Time, Date
57
+ @default_dataset.literal_date_or_time(arg)
58
58
  else
59
59
  super
60
60
  end
@@ -203,7 +203,7 @@ module Sequel
203
203
  date <<= ((date.year) * 24 - 12)
204
204
  date = db.from_application_timestamp(date)
205
205
  minutes = (date.offset * 1440).to_i
206
- date.strftime("'%Y-%m-%d %H:%M:%S.%N#{format_timestamp_offset(*minutes.divmod(60))} BC'")
206
+ date.strftime("'%Y-%m-%d %H:%M:%S.%6N#{sprintf("%+03i%02i", *minutes.divmod(60))} BC'")
207
207
  else
208
208
  super
209
209
  end
@@ -247,7 +247,7 @@ module Sequel
247
247
  def literal_time(time)
248
248
  if time < TIME_YEAR_1
249
249
  time = db.from_application_timestamp(time)
250
- time.strftime("'#{sprintf('%04i', time.year.abs+1)}-%m-%d %H:%M:%S.%N#{format_timestamp_offset(*(time.utc_offset/RATIONAL_60).divmod(60))} BC'")
250
+ time.strftime("'#{sprintf('%04i', time.year.abs+1)}-%m-%d %H:%M:%S.%6N#{sprintf("%+03i%02i", *(time.utc_offset/RATIONAL_60).divmod(60))} BC'")
251
251
  else
252
252
  super
253
253
  end
@@ -123,6 +123,15 @@
123
123
  # c = Sequel.pg_jsonb_op(:c)
124
124
  # DB[:t].update(c['key1'] => 1.to_json, c['key2'] => "a".to_json)
125
125
  #
126
+ # On PostgreSQL 16+, the <tt>IS [NOT] JSON</tt> operator is supported:
127
+ #
128
+ # j.is_json # j IS JSON
129
+ # j.is_json(type: :object) # j IS JSON OBJECT
130
+ # j.is_json(type: :object, unique: true) # j IS JSON OBJECT WITH UNIQUE
131
+ # j.is_not_json # j IS NOT JSON
132
+ # j.is_not_json(type: :array) # j IS NOT JSON ARRAY
133
+ # j.is_not_json(unique: true) # j IS NOT JSON WITH UNIQUE
134
+ #
126
135
  # If you are also using the pg_json extension, you should load it before
127
136
  # loading this extension. Doing so will allow you to use the #op method on
128
137
  # JSONHash, JSONHarray, JSONBHash, and JSONBArray, allowing you to perform json/jsonb operations
@@ -151,6 +160,18 @@ module Sequel
151
160
  GET_PATH = ["(".freeze, " #> ".freeze, ")".freeze].freeze
152
161
  GET_PATH_TEXT = ["(".freeze, " #>> ".freeze, ")".freeze].freeze
153
162
 
163
+ IS_JSON = ["(".freeze, " IS JSON".freeze, "".freeze, ")".freeze].freeze
164
+ IS_NOT_JSON = ["(".freeze, " IS NOT JSON".freeze, "".freeze, ")".freeze].freeze
165
+ EMPTY_STRING = Sequel::LiteralString.new('').freeze
166
+ WITH_UNIQUE = Sequel::LiteralString.new(' WITH UNIQUE').freeze
167
+ IS_JSON_MAP = {
168
+ nil => EMPTY_STRING,
169
+ :value => Sequel::LiteralString.new(' VALUE').freeze,
170
+ :scalar => Sequel::LiteralString.new(' SCALAR').freeze,
171
+ :object => Sequel::LiteralString.new(' OBJECT').freeze,
172
+ :array => Sequel::LiteralString.new(' ARRAY').freeze
173
+ }.freeze
174
+
154
175
  # Get JSON array element or object field as json. If an array is given,
155
176
  # gets the object at the specified path.
156
177
  #
@@ -233,6 +254,30 @@ module Sequel
233
254
  end
234
255
  end
235
256
 
257
+ # Return whether the json object can be parsed as JSON.
258
+ #
259
+ # Options:
260
+ # :type :: Check whether the json object can be parsed as a specific type
261
+ # of JSON (:value, :scalar, :object, :array).
262
+ # :unique :: Check JSON objects for unique keys.
263
+ #
264
+ # json_op.is_json # json IS JSON
265
+ # json_op.is_json(type: :object) # json IS JSON OBJECT
266
+ # json_op.is_json(unique: true) # json IS JSON WITH UNIQUE
267
+ def is_json(opts=OPTS)
268
+ _is_json(IS_JSON, opts)
269
+ end
270
+
271
+ # Return whether the json object cannot be parsed as JSON. The opposite
272
+ # of #is_json. See #is_json for options.
273
+ #
274
+ # json_op.is_not_json # json IS NOT JSON
275
+ # json_op.is_not_json(type: :object) # json IS NOT JSON OBJECT
276
+ # json_op.is_not_json(unique: true) # json IS NOT JSON WITH UNIQUE
277
+ def is_not_json(opts=OPTS)
278
+ _is_json(IS_NOT_JSON, opts)
279
+ end
280
+
236
281
  # Returns a set of keys AS text in the json object.
237
282
  #
238
283
  # json_op.keys # json_object_keys(json)
@@ -286,6 +331,13 @@ module Sequel
286
331
 
287
332
  private
288
333
 
334
+ # Internals of IS [NOT] JSON support
335
+ def _is_json(lit_array, opts)
336
+ raise Error, "invalid is_json :type option: #{opts[:type].inspect}" unless type = IS_JSON_MAP[opts[:type]]
337
+ unique = opts[:unique] ? WITH_UNIQUE : EMPTY_STRING
338
+ Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(lit_array, [self, type, unique]))
339
+ end
340
+
289
341
  # Return a placeholder literal with the given str and args, wrapped
290
342
  # in an JSONOp or JSONBOp, used by operators that return json or jsonb.
291
343
  def json_op(str, args)
@@ -494,8 +494,8 @@ module Sequel
494
494
  case k
495
495
  when nil
496
496
  ''
497
- when Date, Time
498
- ds.literal(k)[1...-1]
497
+ when Time, Date
498
+ ds.literal_date_or_time(k, true)
499
499
  when Integer, Float
500
500
  k.to_s
501
501
  when BigDecimal
@@ -1,20 +1,35 @@
1
1
  # frozen-string-literal: true
2
2
  #
3
3
  # The pg_timestamptz extension changes the default timestamp
4
- # type for the database to be +timestamptz+ (+timestamp with time zone+)
5
- # instead of +timestamp+ (+timestamp without time zone+). This is
4
+ # type for the database to be +timestamptz+ (<tt>timestamp with time zone</tt>)
5
+ # instead of +timestamp+ (<tt>timestamp without time zone</tt>). This is
6
6
  # recommended if you are dealing with multiple timezones in your application.
7
+ #
8
+ # If you are using the auto_cast_date_and_time extension, the pg_timestamptz
9
+ # extension will automatically cast Time and DateTime values to
10
+ # <tt>TIMESTAMP WITH TIME ZONE</tt> instead of +TIMESTAMP+.
7
11
  #
8
12
  # To load the extension into the database:
9
13
  #
10
14
  # DB.extension :pg_timestamptz
11
15
  #
12
- # Related module: Sequel::Postgres::Timestamptz
16
+ # To load the extension into individual datasets:
17
+ #
18
+ # ds = ds.extension(:pg_timestamptz)
19
+ #
20
+ # Note that the loading into individual datasets only affects the integration
21
+ # with the auto_cast_date_and_time extension.
22
+ #
23
+ # Related modules: Sequel::Postgres::Timestamptz, Sequel::Postgres::TimestamptzDatasetMethods
13
24
 
14
25
  #
15
26
  module Sequel
16
27
  module Postgres
17
28
  module Timestamptz
29
+ def self.extended(db)
30
+ db.extend_datasets(TimestamptzDatasetMethods)
31
+ end
32
+
18
33
  private
19
34
 
20
35
  # Use timestamptz by default for generic timestamp value.
@@ -22,7 +37,16 @@ module Sequel
22
37
  :timestamptz
23
38
  end
24
39
  end
40
+
41
+ module TimestamptzDatasetMethods
42
+ private
43
+
44
+ def literal_datetime_timestamp_cast
45
+ 'TIMESTAMP WITH TIME ZONE '
46
+ end
47
+ end
25
48
  end
26
49
 
50
+ Dataset.register_extension(:pg_timestamptz, Postgres::TimestamptzDatasetMethods)
27
51
  Database.register_extension(:pg_timestamptz, Postgres::Timestamptz)
28
52
  end
@@ -0,0 +1,108 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The provenance dataset extension tracks the locations of all
4
+ # dataset clones that resulted in the current dataset, and includes
5
+ # the information as a comment in the dataset's SQL. This makes it
6
+ # possible to see how a query was built, which can aid debugging.
7
+ # Example:
8
+ #
9
+ # DB[:table].
10
+ # select(:a).
11
+ # where{b > 10}.
12
+ # order(:c).
13
+ # limit(10)
14
+ # # SQL:
15
+ # # SELECT a FROM table WHERE (b > 10) ORDER BY c LIMIT 10 --
16
+ # # -- Dataset Provenance
17
+ # # -- Keys:[:from] Source:(eval at bin/sequel:257):2:in `<main>'
18
+ # # -- Keys:[:select] Source:(eval at bin/sequel:257):3:in `<main>'
19
+ # # -- Keys:[:where] Source:(eval at bin/sequel:257):4:in `<main>'
20
+ # # -- Keys:[:order] Source:(eval at bin/sequel:257):5:in `<main>'
21
+ # # -- Keys:[:limit] Source:(eval at bin/sequel:257):6:in `<main>'
22
+ #
23
+ # With the above example, the source is fairly obvious and not helpful,
24
+ # but in real applications, where datasets can be built from multiple
25
+ # files, seeing where each dataset clone was made can be helpful.
26
+ #
27
+ # The Source listed will skip locations in the Ruby standard library
28
+ # as well as Sequel itself. Other locations can be skipped by
29
+ # providing a Database :provenance_caller_ignore Regexp option:
30
+ #
31
+ # DB.opts[:provenance_caller_ignore] = /\/gems\/library_name-/
32
+ #
33
+ # Related module: Sequel::Dataset::Provenance
34
+
35
+ #
36
+ module Sequel
37
+ class Dataset
38
+ module Provenance
39
+ SEQUEL_LIB_PATH = (File.expand_path('../../..', __FILE__) + '/').freeze
40
+ RUBY_STDLIB = RbConfig::CONFIG["rubylibdir"]
41
+
42
+ if TRUE_FREEZE
43
+ # Include provenance information when cloning datasets.
44
+ def clone(opts = nil || (return self))
45
+ super(provenance_opts(opts))
46
+ end
47
+ else
48
+ # :nocov:
49
+ def clone(opts = OPTS) # :nodoc:
50
+ super(provenance_opts(opts))
51
+ end
52
+ # :nocov:
53
+ end
54
+
55
+ %w'select insert update delete'.each do |type|
56
+ # Include the provenance information as a comment when preparing dataset SQL
57
+ define_method(:"#{type}_sql") do |*a|
58
+ sql = super(*a)
59
+
60
+ if provenance = @opts[:provenance]
61
+ comment = provenance.map do |hash|
62
+ " -- Keys:#{hash[:keys].inspect} Source:#{hash[:source]}".to_s.gsub(/\s+/, ' ')
63
+ end
64
+ comment << ""
65
+ comment.unshift " -- Dataset Provenance"
66
+ comment.unshift " -- "
67
+ comment = comment.join("\n")
68
+
69
+ if sql.frozen?
70
+ sql += comment
71
+ sql.freeze
72
+ elsif @opts[:append_sql] || @opts[:placeholder_literalizer]
73
+ sql << comment
74
+ else
75
+ sql += comment
76
+ end
77
+ end
78
+
79
+ sql
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # Return a copy of opts with provenance information added.
86
+ def provenance_opts(opts)
87
+ provenance = {source: provenance_source, keys: opts.keys.freeze}.freeze
88
+ opts = opts.dup
89
+ opts[:provenance] = ((@opts[:provenance] || EMPTY_ARRAY).dup << provenance).freeze
90
+ opts
91
+ end
92
+
93
+ # Return the caller line for the provenance change. This skips
94
+ # Sequel itself and the standard library. Additional locations
95
+ # can be skipped using the :provenance_caller_ignore Dataset option.
96
+ def provenance_source
97
+ ignore = db.opts[:provenance_caller_ignore]
98
+ caller.find do |line|
99
+ !(line.start_with?(SEQUEL_LIB_PATH) ||
100
+ line.start_with?(RUBY_STDLIB) ||
101
+ (ignore && line =~ ignore))
102
+ end
103
+ end
104
+ end
105
+
106
+ register_extension(:provenance, Provenance)
107
+ end
108
+ end
@@ -35,7 +35,7 @@ module Sequel
35
35
 
36
36
  # Round Sequel::SQLTime values before literalizing
37
37
  def literal_sqltime(v)
38
- super(v.round(timestamp_precision))
38
+ super(v.round(sqltime_precision))
39
39
  end
40
40
 
41
41
  # Round Time values before literalizing
@@ -52,7 +52,7 @@ module Sequel
52
52
  # Dump the cached schema to the filename given in Marshal format.
53
53
  def dump_schema_cache(file)
54
54
  sch = {}
55
- @schemas.each do |k,v|
55
+ @schemas.sort.each do |k,v|
56
56
  sch[k] = v.map do |c, h|
57
57
  h = Hash[h]
58
58
  h.delete(:callable_default)
@@ -2,27 +2,34 @@
2
2
  #
3
3
  # The sqlite_json_ops extension adds support to Sequel's DSL to make
4
4
  # it easier to call SQLite JSON functions and operators (added
5
- # first in SQLite 3.38.0).
5
+ # first in SQLite 3.38.0). It also supports the SQLite JSONB functions
6
+ # added in SQLite 3.45.0.
6
7
  #
7
8
  # To load the extension:
8
9
  #
9
10
  # Sequel.extension :sqlite_json_ops
10
11
  #
11
- # This extension works by calling methods on Sequel::SQLite::JSONOp objects,
12
- # which you can create via Sequel.sqlite_json_op:
12
+ # This extension works by calling methods on Sequel::SQLite::JSONOp and
13
+ # Sequel::SQLite::JSONBOp objects, which you can create using
14
+ # Sequel.sqlite_json_op and Sequel.sqlite_jsonb_op:
13
15
  #
14
16
  # j = Sequel.sqlite_json_op(:json_column)
17
+ # jb = Sequel.sqlite_jsonb_op(:jsonb_column)
15
18
  #
16
- # Also, on most Sequel expression objects, you can call the sqlite_json_op method
17
- # to create a Sequel::SQLite::JSONOp object:
19
+ # Also, on most Sequel expression objects, you can call the sqlite_json_op or
20
+ # sqlite_jsonb_op method to create a Sequel::SQLite::JSONOp or
21
+ # Sequel::SQLite::JSONBOp object:
18
22
  #
19
23
  # j = Sequel[:json_column].sqlite_json_op
24
+ # jb = Sequel[:jsonb_column].sqlite_jsonb_op
20
25
  #
21
26
  # If you have loaded the {core_extensions extension}[rdoc-ref:doc/core_extensions.rdoc],
22
27
  # or you have loaded the core_refinements extension
23
28
  # and have activated refinements for the file, you can also use Symbol#sqlite_json_op:
29
+ # or Symbol#sqlite_jsonb_op:
24
30
  #
25
31
  # j = :json_column.sqlite_json_op
32
+ # jb = :json_column.sqlite_jsonb_op
26
33
  #
27
34
  # The following methods are available for Sequel::SQLite::JSONOp instances:
28
35
  #
@@ -30,11 +37,13 @@
30
37
  # j.get(1) # (json_column ->> 1)
31
38
  # j.get_text(1) # (json_column -> 1)
32
39
  # j.extract('$.a') # json_extract(json_column, '$.a')
40
+ # jb.extract('$.a') # jsonb_extract(jsonb_column, '$.a')
33
41
  #
34
42
  # j.array_length # json_array_length(json_column)
35
43
  # j.type # json_type(json_column)
36
44
  # j.valid # json_valid(json_column)
37
- # j.json # json(json_column)
45
+ # jb.json # json(jsonb_column)
46
+ # j.jsonb # jsonb(json_column)
38
47
  #
39
48
  # j.insert('$.a', 1) # json_insert(json_column, '$.a', 1)
40
49
  # j.set('$.a', 1) # json_set(json_column, '$.a', 1)
@@ -42,22 +51,30 @@
42
51
  # j.remove('$.a') # json_remove(json_column, '$.a')
43
52
  # j.patch('{"a":2}') # json_patch(json_column, '{"a":2}')
44
53
  #
54
+ # jb.insert('$.a', 1) # jsonb_insert(jsonb_column, '$.a', 1)
55
+ # jb.set('$.a', 1) # jsonb_set(jsonb_column, '$.a', 1)
56
+ # jb.replace('$.a', 1) # jsonb_replace(jsonb_column, '$.a', 1)
57
+ # jb.remove('$.a') # jsonb_remove(jsonb_column, '$.a')
58
+ # jb.patch('{"a":2}') # jsonb_patch(jsonb_column, '{"a":2}')
59
+ #
45
60
  # j.each # json_each(json_column)
46
61
  # j.tree # json_tree(json_column)
47
62
  #
48
- # Related modules: Sequel::SQLite::JSONOp
63
+ # Related modules: Sequel::SQLite::JSONBaseOp, Sequel::SQLite::JSONOp,
64
+ # Sequel::SQLite::JSONBOp
49
65
 
50
66
  #
51
67
  module Sequel
52
68
  module SQLite
53
- # The JSONOp class is a simple container for a single object that
54
- # defines methods that yield Sequel expression objects representing
55
- # SQLite json operators and functions.
69
+ # JSONBaseOp is an abstract base wrapper class for a object that
70
+ # defines methods that return Sequel expression objects representing
71
+ # SQLite json operators and functions. It is subclassed by both
72
+ # JSONOp and JSONBOp for json and jsonb specific behavior.
56
73
  #
57
74
  # In the method documentation examples, assume that:
58
75
  #
59
76
  # json_op = Sequel.sqlite_json_op(:json)
60
- class JSONOp < Sequel::SQL::Wrapper
77
+ class JSONBaseOp < Sequel::SQL::Wrapper
61
78
  GET = ["(".freeze, " ->> ".freeze, ")".freeze].freeze
62
79
  private_constant :GET
63
80
 
@@ -82,7 +99,7 @@ module Sequel
82
99
  # json_op.array_length # json_array_length(json)
83
100
  # json_op.array_length('$[1]') # json_array_length(json, '$[1]')
84
101
  def array_length(*args)
85
- Sequel::SQL::NumericExpression.new(:NOOP, function(:array_length, *args))
102
+ Sequel::SQL::NumericExpression.new(:NOOP, SQL::Function.new(:json_array_length, self, *args))
86
103
  end
87
104
 
88
105
  # Returns an expression for a set of information extracted from the top-level
@@ -92,7 +109,7 @@ module Sequel
92
109
  # json_op.each # json_each(json)
93
110
  # json_op.each('$.a') # json_each(json, '$.a')
94
111
  def each(*args)
95
- function(:each, *args)
112
+ SQL::Function.new(:json_each, self, *args)
96
113
  end
97
114
 
98
115
  # Returns an expression for the JSON array element or object field at the specified
@@ -129,10 +146,17 @@ module Sequel
129
146
  #
130
147
  # json_op.json # json(json)
131
148
  def json
132
- self.class.new(SQL::Function.new(:json, self))
149
+ JSONOp.new(SQL::Function.new(:json, self))
133
150
  end
134
151
  alias minify json
135
152
 
153
+ # Returns the JSONB format of the JSON.
154
+ #
155
+ # json_op.jsonb # jsonb(json)
156
+ def jsonb
157
+ JSONBOp.new(SQL::Function.new(:jsonb, self))
158
+ end
159
+
136
160
  # Returns an expression for updating the JSON object using the RFC 7396 MergePatch algorithm
137
161
  #
138
162
  # json_op.patch('{"a": 1, "b": null}') # json_patch(json, '{"a": 1, "b": null}')
@@ -172,7 +196,7 @@ module Sequel
172
196
  # json_op.tree # json_tree(json)
173
197
  # json_op.tree('$.a') # json_tree(json, '$.a')
174
198
  def tree(*args)
175
- function(:tree, *args)
199
+ SQL::Function.new(:json_tree, self, *args)
176
200
  end
177
201
 
178
202
  # Returns an expression for the type of the JSON value or the JSON value at the given path.
@@ -180,13 +204,13 @@ module Sequel
180
204
  # json_op.type # json_type(json)
181
205
  # json_op.type('$[1]') # json_type(json, '$[1]')
182
206
  def type(*args)
183
- Sequel::SQL::StringExpression.new(:NOOP, function(:type, *args))
207
+ Sequel::SQL::StringExpression.new(:NOOP, SQL::Function.new(:json_type, self, *args))
184
208
  end
185
209
  alias typeof type
186
210
 
187
211
  # Returns a boolean expression for whether the JSON is valid or not.
188
212
  def valid
189
- Sequel::SQL::BooleanExpression.new(:NOOP, function(:valid))
213
+ Sequel::SQL::BooleanExpression.new(:NOOP, SQL::Function.new(:json_valid, self))
190
214
  end
191
215
 
192
216
  private
@@ -198,7 +222,7 @@ module Sequel
198
222
 
199
223
  # Internals of the methods that return functions prefixed with +json_+.
200
224
  def function(name, *args)
201
- SQL::Function.new("json_#{name}", self, *args)
225
+ SQL::Function.new("#{function_prefix}_#{name}", self, *args)
202
226
  end
203
227
 
204
228
  # Internals of the methods that return functions prefixed with +json_+, that
@@ -208,12 +232,36 @@ module Sequel
208
232
  end
209
233
  end
210
234
 
235
+ # JSONOp is used for SQLite json-specific functions and operators.
236
+ class JSONOp < JSONBaseOp
237
+ private
238
+
239
+ def function_prefix
240
+ "json"
241
+ end
242
+ end
243
+
244
+ # JSONOp is used for SQLite jsonb-specific functions and operators.
245
+ class JSONBOp < JSONBaseOp
246
+ private
247
+
248
+ def function_prefix
249
+ "jsonb"
250
+ end
251
+ end
252
+
211
253
  module JSONOpMethods
212
254
  # Wrap the receiver in an JSONOp so you can easily use the SQLite
213
255
  # json functions and operators with it.
214
256
  def sqlite_json_op
215
257
  JSONOp.new(self)
216
258
  end
259
+
260
+ # Wrap the receiver in an JSONBOp so you can easily use the SQLite
261
+ # jsonb functions and operators with it.
262
+ def sqlite_jsonb_op
263
+ JSONBOp.new(self)
264
+ end
217
265
  end
218
266
  end
219
267
 
@@ -227,6 +275,16 @@ module Sequel
227
275
  SQLite::JSONOp.new(v)
228
276
  end
229
277
  end
278
+
279
+ # Return the object wrapped in an SQLite::JSONBOp.
280
+ def sqlite_jsonb_op(v)
281
+ case v
282
+ when SQLite::JSONBOp
283
+ v
284
+ else
285
+ SQLite::JSONBOp.new(v)
286
+ end
287
+ end
230
288
  end
231
289
 
232
290
  class SQL::GenericExpression