sequel 5.20.0 → 5.21.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e092b5439a78eec1bb50a0bbc8a62c1d84ee5b27b9c5f034c250b632d03e17af
4
- data.tar.gz: 231708d52405407b11113c55d5fdcd95e7d6155b0a8283f736876569ced6e4b1
3
+ metadata.gz: 33b35675e5f68471b8216acbbe10085992616ae93b6f47191c44bb3860b38285
4
+ data.tar.gz: e1be4ce80c4810a9022505f7e5d0becaa11912f6e91344b860195e6b11438d27
5
5
  SHA512:
6
- metadata.gz: 4c2d7e19056e53f2d780f57f5da3c4675531f9678c6a54e922e0d59895bd2e485f12ee0b2644ed801c832a53653f75531d849cb7d60dcd9fb6a4edbfa8cb4c0b
7
- data.tar.gz: 701b041185cc57c1966ea3b4be040e26a9e0aabfd6df45f0192f5b0b51133bfb94629f0634fa6edfc513a1c9193d455cb6222b388683d638e474b344e663ca50
6
+ metadata.gz: fedb31816a3c56136b007317f135072c318e71938c3375feff8c0ab2ec7ddc5744457008266434dff285095b09bfdf78ab33a24b0b4ac255119f9e80ae6758f1
7
+ data.tar.gz: eea67a798eedf2cb8b10bbf20eaf3b0deb7e9eabd1c9b8f238754ffbbd4afea394caa8b4574db7bda6373db987a8a088f83732ea353591eeca37ee4583d57730
data/CHANGELOG CHANGED
@@ -1,3 +1,27 @@
1
+ === 5.21.0 (2019-06-01)
2
+
3
+ * Recognize additional DatabaseLockTimeout errors in mysql and mysql2 adapters (jeremyevans)
4
+
5
+ * Disallow eager_graph of ancestors and descendants associations when using the rcte_tree plugin (jeremyevans)
6
+
7
+ * Make jdbc/mysql adapter work when using JRuby with Java 11 (jeremyevans)
8
+
9
+ * Support window function options :window, :exclude, and :frame :type=>:groups, :start, and :end on SQLite 3.28.0+ (jeremyevans)
10
+
11
+ * Make the server_block extension respect the :servers_hash Database option (jeremyevans)
12
+
13
+ * Typecast string input for json/jsonb types as JSON strings instead of parsing as JSON in the pg_json extension when Database#typecast_json_strings is set to true (jeremyevans)
14
+
15
+ * Wrap JSON primitives (string, number, true, false, nil) in the pg_json extension when Database#wrap_json_primitives is set to true (jeremyevans)
16
+
17
+ * Convert the Database :timeout option to an integer in the sqlite adapter (jeremyevans) (#1620)
18
+
19
+ * Improve performance in ado adapter using more efficient inner loop (jeremyevans)
20
+
21
+ * Improve performance in ado adapter using faster callables for type conversion (jeremyevans)
22
+
23
+ * Fix handling of decimal values in the ado adapter when using locales where the decimal separator is , and not . (jeremyevans) (#1619)
24
+
1
25
  === 5.20.0 (2019-05-01)
2
26
 
3
27
  * Fix reversing of alter_table add_foreign_key when :type option is used (jeremyevans) (#1615)
@@ -264,7 +264,8 @@ The following additional options are supported:
264
264
  or an array of symbols or strings (e.g. <tt>:sql_mode=>[:no_zero_date, :pipes_as_concat]</tt>).
265
265
  :timeout :: Sets the wait_timeout for the connection, defaults to 1 month.
266
266
  :read_timeout :: Set the timeout in seconds for reading back results to a query.
267
- :connect_timeout :: Set the timeout in seconds before a connection attempt is abandoned.
267
+ :connect_timeout :: Set the timeout in seconds before a connection attempt is abandoned
268
+ (may not be supported when using MariaDB 10.2+ client libraries).
268
269
 
269
270
  The :sslkey, :sslcert, :sslca, :sslcapath, and :sslca options (in that order) are passed to Mysql#ssl_set method
270
271
  if either the :sslca or :sslkey option is given.
@@ -0,0 +1,87 @@
1
+ = New Features
2
+
3
+ * The pg_json extension now adds a Database#wrap_json_primitives
4
+ accessor. When set to true, JSON primitive values (string, number,
5
+ true, false, and null) will be wrapped by delegate Ruby objects
6
+ instead of using Ruby primitives. This allows the values to round
7
+ trip, so the following code will work even for primitive values in
8
+ json_column:
9
+
10
+ DB.extension :pg_json
11
+ DB.wrap_json_primitives = true
12
+ value = DB[:table].get(:json_column)
13
+ DB[:other_table].insert(json_column: value)
14
+
15
+ This should be enabled with care, especially in cases where false
16
+ and null JSON values are used, as the behavior will change if
17
+ the objects are used in a boolean context in Ruby, as only false
18
+ and nil in Ruby are treated as false:
19
+
20
+ # assume JSON false or null value
21
+ value = DB[:table].get(:json_column)
22
+
23
+ if value
24
+ # executed if wrap_json_primitives is true
25
+ else
26
+ # executed by default
27
+ end
28
+
29
+ When typecasting input in model objects to a JSON type, string
30
+ input will still be parsed as JSON. However, you can set the
31
+ Database#typecast_json_strings accessor to true, and then string
32
+ input will be considered as a JSON string instead of parsing the
33
+ string as JSON.
34
+
35
+ To prevent backwards compatibility issues, Sequel.pg_json/pg_jsonb
36
+ behavior has not changed. To support wrapping Ruby primitives in
37
+ the delegate objects, new Sequel.pg_json_wrap/pg_jsonb_wrap methods
38
+ have been added. These methods only handle the Ruby primitives,
39
+ they cannot be used if the existing object is already a delegate
40
+ object.
41
+
42
+ As model objects always consider a nil value as SQL NULL and do
43
+ not typecast it, if you want to explicitly set a JSON null value,
44
+ you need to wrap it explicitly:
45
+
46
+ model_object.json_column = Sequel.pg_json_wrap(nil)
47
+
48
+ = Other Improvements
49
+
50
+ * Sequel now supports window function options :window, :exclude, and
51
+ :frame :type=>:groups, :start, and :end on SQLite 3.28.0+.
52
+
53
+ * The server_block extension now respects the :servers_hash Database
54
+ option. This makes it more similar to Sequel's default behavior.
55
+ However, that means by default, the server_block extension will
56
+ default to handling unknown shards as the default shard, instead
57
+ of raising an error for them.
58
+
59
+ * The rcte_tree plugin now disallows eager graphing of the ancestors
60
+ and descendants associations. Previously, eager graphing of these
61
+ associations generated incorrect results. It is not possible to
62
+ eager graph these extensions, but normal eager loading does work.
63
+
64
+ * The ado adapter's performance has been improved by using faster
65
+ callables for type conversion and a more efficient inner loop.
66
+
67
+ * The sqlite adapter now converts a :timeout option given as a string
68
+ to an integer. This allows you to use the option inside of a
69
+ connection string.
70
+
71
+ * The mysql and mysql2 adapters now recognize an additional
72
+ DatabaseLockTimeout error.
73
+
74
+ * The jdbc/mysql adapter now works correctly when using JRuby with
75
+ Java 11.
76
+
77
+ * The ado adapter now handles numeric values when using locales that
78
+ use comma instead of period as the decimal separator.
79
+
80
+ = Backwards Compatibility
81
+
82
+ * In the pg_json extension, the following singleton methods of
83
+ Sequel::Postgres::JSONDatabaseMethods are now deprecated:
84
+
85
+ * parse_json
86
+ * db_parse_json
87
+ * db_parse_jsonb
@@ -140,6 +140,8 @@ the shard to use. This is fairly easy using a Sequel::Model:
140
140
 
141
141
  Rainbow.plaintext_for_hash("e580726d31f6e1ad216ffd87279e536d1f74e606")
142
142
 
143
+ === :servers_hash Option
144
+
143
145
  The connection pool can be further controlled to change how it handles attempts
144
146
  to access shards that haven't been configured. The default is
145
147
  to assume the :default shard. However, you can specify a
@@ -47,34 +47,40 @@ module Sequel
47
47
  #AdVarWChar = 202
48
48
  #AdWChar = 130
49
49
 
50
- cp = Object.new
51
-
52
- def cp.bigint(v)
50
+ bigint = Object.new
51
+ def bigint.call(v)
53
52
  v.to_i
54
53
  end
55
54
 
56
- def cp.numeric(v)
57
- BigDecimal(v)
55
+ numeric = Object.new
56
+ def numeric.call(v)
57
+ if v.include?(',')
58
+ BigDecimal(v.tr(',', '.'))
59
+ else
60
+ BigDecimal(v)
61
+ end
58
62
  end
59
63
 
60
- def cp.binary(v)
64
+ binary = Object.new
65
+ def binary.call(v)
61
66
  Sequel.blob(v.pack('c*'))
62
67
  end
63
68
 
64
- def cp.date(v)
69
+ date = Object.new
70
+ def date.call(v)
65
71
  Date.new(v.year, v.month, v.day)
66
72
  end
67
73
 
68
74
  CONVERSION_PROCS = {}
69
75
  [
70
- [:bigint, AdBigInt],
71
- [:numeric, AdNumeric, AdVarNumeric],
72
- [:date, AdDBDate],
73
- [:binary, AdBinary, AdVarBinary, AdLongVarBinary]
74
- ].each do |meth, *types|
75
- method = cp.method(meth)
76
+ [bigint, AdBigInt],
77
+ [numeric, AdNumeric, AdVarNumeric],
78
+ [date, AdDBDate],
79
+ [binary, AdBinary, AdVarBinary, AdLongVarBinary]
80
+ ].each do |callable, *types|
81
+ callable.freeze
76
82
  types.each do |i|
77
- CONVERSION_PROCS[i] = method
83
+ CONVERSION_PROCS[i] = callable
78
84
  end
79
85
  end
80
86
  CONVERSION_PROCS.freeze
@@ -227,7 +233,6 @@ module Sequel
227
233
  cols = []
228
234
  conversion_procs = db.conversion_procs
229
235
 
230
- i = -1
231
236
  ts_cp = nil
232
237
  recordset.Fields.each do |field|
233
238
  type = field.Type
@@ -244,18 +249,21 @@ module Sequel
244
249
  else
245
250
  conversion_procs[type]
246
251
  end
247
- cols << [output_identifier(field.Name), cp, i+=1]
252
+ cols << [output_identifier(field.Name), cp]
248
253
  end
249
254
 
250
255
  self.columns = cols.map(&:first)
251
256
  return if recordset.EOF
257
+ max = cols.length
252
258
 
253
259
  recordset.GetRows.transpose.each do |field_values|
254
260
  h = {}
255
261
 
256
- cols.each do |name, cp, index|
257
- h[name] = if (v = field_values[index]) && cp
258
- cp[v]
262
+ i = -1
263
+ while (i += 1) < max
264
+ name, cp = cols[i]
265
+ h[name] = if (v = field_values[i]) && cp
266
+ cp.call(v)
259
267
  else
260
268
  v
261
269
  end
@@ -54,12 +54,12 @@ module Sequel
54
54
  # MySQL 5.1.12 JDBC adapter requires generated keys
55
55
  # and previous versions don't mind.
56
56
  def execute_statement_insert(stmt, sql)
57
- stmt.executeUpdate(sql, JavaSQL::Statement.RETURN_GENERATED_KEYS)
57
+ stmt.executeUpdate(sql, JavaSQL::Statement::RETURN_GENERATED_KEYS)
58
58
  end
59
59
 
60
60
  # Return generated keys for insert statements.
61
61
  def prepare_jdbc_statement(conn, sql, opts)
62
- opts[:type] == :insert ? conn.prepareStatement(sql, JavaSQL::Statement.RETURN_GENERATED_KEYS) : super
62
+ opts[:type] == :insert ? conn.prepareStatement(sql, JavaSQL::Statement::RETURN_GENERATED_KEYS) : super
63
63
  end
64
64
 
65
65
  # Convert tinyint(1) type to boolean
@@ -513,7 +513,7 @@ module Sequel
513
513
 
514
514
  Dataset.def_sql_method(self, :delete, [['if db.sqlite_version >= 30803', %w'with delete from where'], ["else", %w'delete from where']])
515
515
  Dataset.def_sql_method(self, :insert, [['if db.sqlite_version >= 30803', %w'with insert conflict into columns values on_conflict'], ["else", %w'insert conflict into columns values']])
516
- Dataset.def_sql_method(self, :select, [['if opts[:values]', %w'with values compounds'], ['else', %w'with select distinct columns from join where group having compounds order limit lock']])
516
+ Dataset.def_sql_method(self, :select, [['if opts[:values]', %w'with values compounds'], ['else', %w'with select distinct columns from join where group having window compounds order limit lock']])
517
517
  Dataset.def_sql_method(self, :update, [['if db.sqlite_version >= 30803', %w'with update table set where'], ["else", %w'update table set where']])
518
518
 
519
519
  def cast_sql_append(sql, expr, type)
@@ -732,6 +732,11 @@ module Sequel
732
732
  def supports_where_true?
733
733
  false
734
734
  end
735
+
736
+ # SQLite 3.28+ supports the WINDOW clause.
737
+ def supports_window_clause?
738
+ db.sqlite_version >= 32800
739
+ end
735
740
 
736
741
  # SQLite 3.25+ supports window functions. However, support is only enabled
737
742
  # on SQLite 3.26.0+ because internal Sequel usage of window functions
@@ -741,6 +746,11 @@ module Sequel
741
746
  db.sqlite_version >= 32600
742
747
  end
743
748
 
749
+ # SQLite 3.28.0+ supports all window frame options that Sequel supports
750
+ def supports_window_function_frame_option?(option)
751
+ db.sqlite_version >= 32800 ? true : super
752
+ end
753
+
744
754
  private
745
755
 
746
756
  # SQLite uses string literals instead of identifiers in AS clauses.
@@ -112,7 +112,7 @@ module Sequel
112
112
  sqlite3_opts = {}
113
113
  sqlite3_opts[:readonly] = typecast_value_boolean(opts[:readonly]) if opts.has_key?(:readonly)
114
114
  db = ::SQLite3::Database.new(opts[:database].to_s, sqlite3_opts)
115
- db.busy_timeout(opts.fetch(:timeout, 5000))
115
+ db.busy_timeout(typecast_value_integer(opts.fetch(:timeout, 5000)))
116
116
 
117
117
  if USE_EXTENDED_RESULT_CODES
118
118
  db.extended_result_codes = true
@@ -59,6 +59,8 @@ module Sequel
59
59
  ForeignKeyConstraintViolation
60
60
  when 4025
61
61
  CheckConstraintViolation
62
+ when 1205
63
+ DatabaseLockTimeout
62
64
  else
63
65
  super
64
66
  end
@@ -66,6 +66,7 @@ module Sequel
66
66
  # Handle both TZInfo 1 and TZInfo 2
67
67
  if defined?(TZInfo::VERSION) && TZInfo::VERSION > '2'
68
68
  # :nodoc:
69
+ # :nocov:
69
70
  def convert_input_datetime_other(v, input_timezone)
70
71
  local_offset = Rational(input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset, 86400)
71
72
  (v - local_offset).new_offset(local_offset)
@@ -78,6 +79,7 @@ module Sequel
78
79
  DateTime.jd(v.jd, v.hour, v.minute, v.second + v.sec_fraction, v.offset, v.start)
79
80
  end
80
81
  # :nodoc:
82
+ # :nocov:
81
83
  else
82
84
  # Assume the given DateTime has a correct time but a wrong timezone. It is
83
85
  # currently in UTC timezone, but it should be converted to the input_timezone.
@@ -1,44 +1,99 @@
1
1
  # frozen-string-literal: true
2
2
  #
3
3
  # The pg_json extension adds support for Sequel to handle
4
- # PostgreSQL's json and jsonb types. It is slightly more strict than the
5
- # PostgreSQL json types in that the object returned should be an
6
- # array or object (PostgreSQL's json type considers plain numbers
7
- # strings, true, false, and null as valid). Sequel will work with
8
- # PostgreSQL json values that are not arrays or objects, but support
9
- # is fairly limited and the values do not roundtrip.
4
+ # PostgreSQL's json and jsonb types. By default, it wraps
5
+ # JSON arrays and JSON objects with ruby array-like and
6
+ # hash-like objects. If you would like to wrap JSON primitives
7
+ # (numbers, strings, +null+, +true+, and +false+), you need to
8
+ # use the +wrap_json_primitives+ setter:
10
9
  #
11
- # This extension integrates with Sequel's native postgres and jdbc/postgresql adapters, so
12
- # that when json fields are retrieved, they are parsed and returned
13
- # as instances of Sequel::Postgres::JSONArray or
14
- # Sequel::Postgres::JSONHash (or JSONBArray or JSONBHash for jsonb
15
- # columns). JSONArray and JSONHash are
16
- # DelegateClasses of Array and Hash, so they mostly act the same, but
17
- # not completely (json_array.is_a?(Array) is false). If you want
18
- # the actual array for a JSONArray, call JSONArray#to_a. If you want
19
- # the actual hash for a JSONHash, call JSONHash#to_hash.
20
- # This is done so that Sequel does not treat JSONArray and JSONHash
21
- # like Array and Hash by default, which would cause issues.
10
+ # DB.extension :pg_json
11
+ # DB.wrap_json_primitives = true
12
+ #
13
+ # Note that wrapping JSON primitives changes the behavior for
14
+ # JSON false and null values. Because only +false+ and +nil+
15
+ # in Ruby are considered falesy, wrapping these objects results
16
+ # in unexpected behavior if you use the values directly in
17
+ # conditionals:
18
+ #
19
+ # if DB[:table].get(:json_column)
20
+ # # called if the value of json_column is null/false
21
+ # # if you are wrapping primitives
22
+ # end
22
23
  #
23
- # To turn an existing Array or Hash into a JSONArray or JSONHash,
24
- # use Sequel.pg_json:
24
+ # To extract the Ruby primitive object from the wrapper object,
25
+ # you can use +__getobj__+ (this comes from Ruby's delegate library).
25
26
  #
26
- # Sequel.pg_json(array) # or Sequel.pg_jsonb(array) for jsonb type
27
- # Sequel.pg_json(hash) # or Sequel.pg_jsonb(hash) for jsonb type
27
+ # To wrap an existing Ruby array, hash, string, integer, float,
28
+ # +nil+, +true+, or +false+, use +Sequel.pg_json_wrap+ or +Sequel.pg_jsonb_wrap+:
29
+ #
30
+ # Sequel.pg_json_wrap(object) # json type
31
+ # Sequel.pg_jsonb_wrap(object) # jsonb type
32
+ #
33
+ # So if you want to insert an array or hash into an json database column:
34
+ #
35
+ # DB[:table].insert(column: Sequel.pg_json_wrap([1, 2, 3]))
36
+ # DB[:table].insert(column: Sequel.pg_json_wrap({'a'=>1, 'b'=>2}))
37
+ #
38
+ # Note that the +pg_json_wrap+ and +pg_jsonb_wrap+ methods only handle Ruby primitives,
39
+ # they do not handle already wrapped objects.
28
40
  #
29
41
  # If you have loaded the {core_extensions extension}[rdoc-ref:doc/core_extensions.rdoc],
30
42
  # or you have loaded the core_refinements extension
31
- # and have activated refinements for the file, you can also use Array#pg_json and Hash#pg_json:
43
+ # and have activated refinements for the file, you can also use the
44
+ # +pg_json+ and +pg_jsonb+ methods directly on Array or Hash:
32
45
  #
33
- # array.pg_json # or array.pg_jsonb for jsonb type
34
- # hash.pg_json # or hash.pg_jsonb for jsonb type
46
+ # array.pg_json # json type
47
+ # array.pg_jsonb # jsonb type
35
48
  #
36
- # So if you want to insert an array or hash into an json database column:
49
+ # hash.pg_json # json type
50
+ # hash.pg_jsonb # jsonb type
51
+ #
52
+ # Model classes that use json or jsonb columns will have typecasting automatically
53
+ # setup, so you can assign Ruby primitives to model columns and have the wrapped
54
+ # objects automatically created. However, for backwards compatibility, passing
55
+ # a string object will parse the string as JSON, not create a JSON string object.
56
+ #
57
+ # obj = Model.new
58
+ # obj.json_column = {'a'=>'b'}
59
+ # obj.json_column.class
60
+ # # => Sequel::Postgres::JSONHash
61
+ # obj.json_column['a']
62
+ # # => 'b'
63
+ #
64
+ # obj.json_column = '{"a": "b"}'
65
+ # obj.json_column.class
66
+ # # => Sequel::Postgres::JSONHash
67
+ # obj.json_column['a']
68
+ # # => 'b'
37
69
  #
38
- # DB[:table].insert(column: Sequel.pg_json([1, 2, 3]))
39
- # DB[:table].insert(column: Sequel.pg_json({'a'=>1, 'b'=>2}))
70
+ # You can change the handling of string typecasting by using +typecast_json_strings+:
40
71
  #
41
- # To use this extension, please load it into the Database instance:
72
+ # DB.typecast_json_strings = true
73
+ # obj.json_column = '{"a": "b"}'
74
+ # obj.json_column.class
75
+ # # => Sequel::Postgres::JSONString
76
+ # obj.json_column
77
+ # # => '{"a": "b"}'
78
+ #
79
+ # Note that +nil+ values are never automatically wrapped:
80
+ #
81
+ # obj.json_column = nil
82
+ # obj.json_column.class
83
+ # # => NilClass
84
+ # obj.json_column
85
+ # # => nil
86
+ #
87
+ # If you want to set a JSON null value when using a model, you must wrap it
88
+ # explicitly:
89
+ #
90
+ # obj.json_column = Sequel.pg_json_wrap(nil)
91
+ # obj.json_column.class
92
+ # # => Sequel::Postgres::JSONNull
93
+ # obj.json_column
94
+ # # => nil
95
+ #
96
+ # To use this extension, load it into the Database instance:
42
97
  #
43
98
  # DB.extension :pg_json
44
99
  #
@@ -46,7 +101,7 @@
46
101
  # for details on using json columns in CREATE/ALTER TABLE statements.
47
102
  #
48
103
  # This extension integrates with the pg_array extension. If you plan
49
- # to use the json[] type, load the pg_array extension before the
104
+ # to use the json[] or jsonb[] types, load the pg_array extension before the
50
105
  # pg_json extension:
51
106
  #
52
107
  # DB.extension :pg_array, :pg_json
@@ -54,114 +109,148 @@
54
109
  # Note that when accessing json hashes, you should always use strings for keys.
55
110
  # Attempting to use other values (such as symbols) will not work correctly.
56
111
  #
57
- # This extension requires both the json and delegate libraries.
112
+ # This extension requires both the json and delegate libraries. However, you
113
+ # can override +Sequel.parse_json+, +Sequel.object_to_json+, and
114
+ # +Sequel.json_parser_error_class+ to use an alternative JSON implementation.
58
115
  #
59
- # Related modules: Sequel::Postgres::JSONArrayBase, Sequel::Postgres::JSONArray,
60
- # Sequel::Postgres::JSONArray, Sequel::Postgres::JSONBArray, Sequel::Postgres::JSONHashBase,
61
- # Sequel::Postgres::JSONHash, Sequel::Postgres::JSONBHash, Sequel::Postgres::JSONDatabaseMethods
116
+ # Related modules: Sequel::Postgres::JSONDatabaseMethods
62
117
 
63
118
  require 'delegate'
64
119
  require 'json'
65
120
 
66
121
  module Sequel
67
122
  module Postgres
68
- # Class representing PostgreSQL JSON/JSONB column array values.
69
- class JSONArrayBase < DelegateClass(Array)
70
- include Sequel::SQL::AliasMethods
71
- include Sequel::SQL::CastMethods
72
-
73
- # Convert the array to a json string and append a
74
- # literalized version of the string to the sql.
75
- def sql_literal_append(ds, sql)
76
- ds.literal_append(sql, Sequel.object_to_json(self))
77
- end
123
+ # A module included in all of the JSON wrapper classes.
124
+ module JSONObject
78
125
  end
79
126
 
80
- class JSONArray < JSONArrayBase
81
- # Cast as json
82
- def sql_literal_append(ds, sql)
83
- super
84
- sql << '::json'
85
- end
127
+ # A module included in all of the JSONB wrapper classes.
128
+ module JSONBObject
86
129
  end
87
130
 
88
- class JSONBArray < JSONArrayBase
89
- # Cast as jsonb
90
- def sql_literal_append(ds, sql)
91
- super
92
- sql << '::jsonb'
131
+ create_delegate_class = lambda do |name, delegate_class|
132
+ base_class = DelegateClass(delegate_class)
133
+ base_class.class_eval do
134
+ include Sequel::SQL::AliasMethods
135
+ include Sequel::SQL::CastMethods
93
136
  end
94
- end
95
137
 
96
- # Class representing PostgreSQL JSON/JSONB column hash/object values.
97
- class JSONHashBase < DelegateClass(Hash)
98
- include Sequel::SQL::AliasMethods
99
- include Sequel::SQL::CastMethods
138
+ json_class = Class.new(base_class) do
139
+ include JSONObject
100
140
 
101
- # Convert the hash to a json string and append a
102
- # literalized version of the string to the sql.
103
- def sql_literal_append(ds, sql)
104
- ds.literal_append(sql, Sequel.object_to_json(self))
141
+ def sql_literal_append(ds, sql)
142
+ ds.literal_append(sql, Sequel.object_to_json(self))
143
+ sql << '::json'
144
+ end
105
145
  end
106
146
 
107
- # Return the object being delegated to.
108
- alias to_hash __getobj__
109
- end
147
+ jsonb_class = Class.new(base_class) do
148
+ include JSONBObject
110
149
 
111
- class JSONHash < JSONHashBase
112
- # Cast as json
113
- def sql_literal_append(ds, sql)
114
- super
115
- sql << '::json'
150
+ def sql_literal_append(ds, sql)
151
+ ds.literal_append(sql, Sequel.object_to_json(self))
152
+ sql << '::jsonb'
153
+ end
116
154
  end
155
+
156
+ const_set(:"JSON#{name}Base", base_class)
157
+ const_set(:"JSON#{name}", json_class)
158
+ const_set(:"JSONB#{name}", jsonb_class)
117
159
  end
118
160
 
119
- class JSONBHash < JSONHashBase
120
- # Cast as jsonb
121
- def sql_literal_append(ds, sql)
122
- super
123
- sql << '::jsonb'
124
- end
161
+ create_delegate_class.call(:Array, Array)
162
+ create_delegate_class.call(:Hash, Hash)
163
+ create_delegate_class.call(:String, String)
164
+ create_delegate_class.call(:Integer, Integer)
165
+ create_delegate_class.call(:Float, Float)
166
+ create_delegate_class.call(:Null, NilClass)
167
+ create_delegate_class.call(:True, TrueClass)
168
+ create_delegate_class.call(:False, FalseClass)
169
+
170
+ JSON_WRAPPER_MAPPING = {
171
+ ::Array => JSONArray,
172
+ ::Hash => JSONHash,
173
+ }.freeze
174
+
175
+ JSONB_WRAPPER_MAPPING = {
176
+ ::Array => JSONBArray,
177
+ ::Hash => JSONBHash,
178
+ }.freeze
179
+
180
+ JSON_PRIMITIVE_WRAPPER_MAPPING = {
181
+ ::String => JSONString,
182
+ ::Integer => JSONInteger,
183
+ ::Float => JSONFloat,
184
+ ::NilClass => JSONNull,
185
+ ::TrueClass => JSONTrue,
186
+ ::FalseClass => JSONFalse,
187
+ }
188
+
189
+ JSONB_PRIMITIVE_WRAPPER_MAPPING = {
190
+ ::String => JSONBString,
191
+ ::Integer => JSONBInteger,
192
+ ::Float => JSONBFloat,
193
+ ::NilClass => JSONBNull,
194
+ ::TrueClass => JSONBTrue,
195
+ ::FalseClass => JSONBFalse,
196
+ }
197
+
198
+ if RUBY_VERSION < '2.4'
199
+ # :nocov:
200
+ JSON_PRIMITIVE_WRAPPER_MAPPING[Fixnum] = JSONInteger
201
+ JSON_PRIMITIVE_WRAPPER_MAPPING[Bignum] = JSONInteger
202
+ JSONB_PRIMITIVE_WRAPPER_MAPPING[Fixnum] = JSONBInteger
203
+ JSONB_PRIMITIVE_WRAPPER_MAPPING[Bignum] = JSONBInteger
204
+ # :nocov:
125
205
  end
126
206
 
207
+ JSON_PRIMITIVE_WRAPPER_MAPPING.freeze
208
+ JSONB_PRIMITIVE_WRAPPER_MAPPING.freeze
209
+
210
+ JSON_COMBINED_WRAPPER_MAPPING =JSON_WRAPPER_MAPPING.merge(JSON_PRIMITIVE_WRAPPER_MAPPING).freeze
211
+ JSON_WRAP_CLASSES = JSON_COMBINED_WRAPPER_MAPPING.keys.freeze
212
+
213
+ JSONB_COMBINED_WRAPPER_MAPPING =JSONB_WRAPPER_MAPPING.merge(JSONB_PRIMITIVE_WRAPPER_MAPPING).freeze
214
+ JSONB_WRAP_CLASSES = JSONB_COMBINED_WRAPPER_MAPPING.keys.freeze
215
+
127
216
  # Methods enabling Database object integration with the json type.
128
217
  module JSONDatabaseMethods
129
218
  def self.extended(db)
130
219
  db.instance_exec do
131
- add_conversion_proc(114, JSONDatabaseMethods.method(:db_parse_json))
132
- add_conversion_proc(3802, JSONDatabaseMethods.method(:db_parse_jsonb))
220
+ add_conversion_proc(114, method(:_db_parse_json))
221
+ add_conversion_proc(3802, method(:_db_parse_jsonb))
133
222
  if respond_to?(:register_array_type)
134
223
  register_array_type('json', :oid=>199, :scalar_oid=>114)
135
224
  register_array_type('jsonb', :oid=>3807, :scalar_oid=>3802)
136
225
  end
137
- @schema_type_classes[:json] = [JSONHash, JSONArray]
138
- @schema_type_classes[:jsonb] = [JSONBHash, JSONBArray]
226
+ @schema_type_classes[:json] = [JSONObject]
227
+ @schema_type_classes[:jsonb] = [JSONBObject]
139
228
  end
140
229
  end
141
230
 
142
- # Parse JSON data coming from the database. Since PostgreSQL allows
143
- # non JSON data in JSON fields (such as plain numbers and strings),
144
- # we don't want to raise an exception for that.
231
+
232
+ # Deprecated
145
233
  def self.db_parse_json(s)
234
+ # SEQUEL6: Remove
146
235
  parse_json(s)
147
236
  rescue Sequel::InvalidValue
148
237
  raise unless s.is_a?(String)
149
238
  parse_json("[#{s}]").first
150
239
  end
151
240
 
152
- # Same as db_parse_json, but consider the input as jsonb.
241
+ # Deprecated
153
242
  def self.db_parse_jsonb(s)
243
+ # SEQUEL6: Remove
154
244
  parse_json(s, true)
155
245
  rescue Sequel::InvalidValue
156
246
  raise unless s.is_a?(String)
157
247
  parse_json("[#{s}]").first
158
248
  end
159
249
 
160
- # Parse the given string as json, returning either a JSONArray
161
- # or JSONHash instance (or JSONBArray or JSONBHash instance if jsonb
162
- # argument is true), or a String, Numeric, true, false, or nil
163
- # if the json library used supports that.
250
+ # Deprecated
164
251
  def self.parse_json(s, jsonb=false)
252
+ # SEQUEL6: Remove
253
+ Sequel::Deprecation.deprecate("Sequel::Postgres::JSONDatabaseMethods.{parse_json,db_parse_json,db_parse_jsonb} are deprecated and will be removed in Sequel 6.")
165
254
  begin
166
255
  value = Sequel.parse_json(s)
167
256
  rescue Sequel.json_parser_error_class => e
@@ -180,10 +269,22 @@ module Sequel
180
269
  end
181
270
  end
182
271
 
272
+ # Whether to wrap JSON primitives instead of using Ruby objects.
273
+ # Wrapping the primitives allows the primitive values to roundtrip,
274
+ # but it can cause problems, especially as false/null JSON values
275
+ # will be treated as truthy in Ruby due to the wrapping. False by
276
+ # default.
277
+ attr_accessor :wrap_json_primitives
278
+
279
+ # Whether to typecast strings for json/jsonb types as JSON
280
+ # strings, instead of trying to parse the string as JSON.
281
+ # False by default.
282
+ attr_accessor :typecast_json_strings
283
+
183
284
  # Handle json and jsonb types in bound variables
184
285
  def bound_variable_arg(arg, conn)
185
286
  case arg
186
- when JSONArrayBase, JSONHashBase
287
+ when JSONObject, JSONBObject
187
288
  Sequel.object_to_json(arg)
188
289
  else
189
290
  super
@@ -192,10 +293,72 @@ module Sequel
192
293
 
193
294
  private
194
295
 
296
+ # Parse JSON data coming from the database. Since PostgreSQL allows
297
+ # non JSON data in JSON fields (such as plain numbers and strings),
298
+ # we don't want to raise an exception for that.
299
+ def _db_parse_json(s)
300
+ _wrap_json(_parse_json(s))
301
+ rescue Sequel::InvalidValue
302
+ raise unless s.is_a?(String)
303
+ _wrap_json(_parse_json("[#{s}]").first)
304
+ end
305
+
306
+ # Same as _db_parse_json, but consider the input as jsonb.
307
+ def _db_parse_jsonb(s)
308
+ _wrap_jsonb(_parse_json(s))
309
+ rescue Sequel::InvalidValue
310
+ raise unless s.is_a?(String)
311
+ _wrap_jsonb(_parse_json("[#{s}]").first)
312
+ end
313
+
314
+ # Parse the given string as json, returning either a JSONArray
315
+ # or JSONHash instance (or JSONBArray or JSONBHash instance if jsonb
316
+ # argument is true), or a String, Numeric, true, false, or nil
317
+ # if the json library used supports that.
318
+ def _parse_json(s)
319
+ begin
320
+ Sequel.parse_json(s)
321
+ rescue Sequel.json_parser_error_class => e
322
+ raise Sequel.convert_exception_class(e, Sequel::InvalidValue)
323
+ end
324
+ end
325
+
326
+ # Wrap the parsed JSON value in the appropriate JSON wrapper class.
327
+ # Only wrap primitive values if wrap_json_primitives is set.
328
+ def _wrap_json(value)
329
+ if klass = JSON_WRAPPER_MAPPING[value.class]
330
+ klass.new(value)
331
+ elsif klass = JSON_PRIMITIVE_WRAPPER_MAPPING[value.class]
332
+ if wrap_json_primitives
333
+ klass.new(value)
334
+ else
335
+ value
336
+ end
337
+ else
338
+ raise Sequel::InvalidValue, "unhandled json value: #{value.inspect}"
339
+ end
340
+ end
341
+
342
+ # Wrap the parsed JSON value in the appropriate JSONB wrapper class.
343
+ # Only wrap primitive values if wrap_json_primitives is set.
344
+ def _wrap_jsonb(value)
345
+ if klass = JSONB_WRAPPER_MAPPING[value.class]
346
+ klass.new(value)
347
+ elsif klass = JSONB_PRIMITIVE_WRAPPER_MAPPING[value.class]
348
+ if wrap_json_primitives
349
+ klass.new(value)
350
+ else
351
+ value
352
+ end
353
+ else
354
+ raise Sequel::InvalidValue, "unhandled jsonb value: #{value.inspect}"
355
+ end
356
+ end
357
+
195
358
  # Handle json[] and jsonb[] types in bound variables.
196
359
  def bound_variable_array(a)
197
360
  case a
198
- when JSONHashBase, JSONArrayBase
361
+ when JSONObject, JSONBObject
199
362
  "\"#{Sequel.object_to_json(a).gsub('"', '\\"')}\""
200
363
  else
201
364
  super
@@ -238,41 +401,43 @@ module Sequel
238
401
  end
239
402
  end
240
403
 
241
- # Convert the value given to a JSONArray or JSONHash
404
+ # Convert the value given to a JSON wrapper object.
242
405
  def typecast_value_json(value)
243
406
  case value
244
- when JSONArray, JSONHash
407
+ when JSONObject
245
408
  value
246
- when Array
247
- JSONArray.new(value)
248
- when Hash
249
- JSONHash.new(value)
250
- when JSONBArray
251
- JSONArray.new(value.to_a)
252
- when JSONBHash
253
- JSONHash.new(value.to_hash)
254
409
  when String
255
- JSONDatabaseMethods.parse_json(value)
410
+ if typecast_json_strings
411
+ JSONString.new(value)
412
+ else
413
+ _wrap_json(_parse_json(value))
414
+ end
415
+ when *JSON_WRAP_CLASSES
416
+ JSON_COMBINED_WRAPPER_MAPPING[value.class].new(value)
417
+ when JSONBObject
418
+ value = value.__getobj__
419
+ JSON_COMBINED_WRAPPER_MAPPING[value.class].new(value)
256
420
  else
257
421
  raise Sequel::InvalidValue, "invalid value for json: #{value.inspect}"
258
422
  end
259
423
  end
260
424
 
261
- # Convert the value given to a JSONBArray or JSONBHash
425
+ # Convert the value given to a JSONB wrapper object.
262
426
  def typecast_value_jsonb(value)
263
427
  case value
264
- when JSONBArray, JSONBHash
428
+ when JSONBObject
265
429
  value
266
- when Array
267
- JSONBArray.new(value)
268
- when Hash
269
- JSONBHash.new(value)
270
- when JSONArray
271
- JSONBArray.new(value.to_a)
272
- when JSONHash
273
- JSONBHash.new(value.to_hash)
274
430
  when String
275
- JSONDatabaseMethods.parse_json(value, true)
431
+ if typecast_json_strings
432
+ JSONBString.new(value)
433
+ else
434
+ _wrap_jsonb(_parse_json(value))
435
+ end
436
+ when *JSONB_WRAP_CLASSES
437
+ JSONB_COMBINED_WRAPPER_MAPPING[value.class].new(value)
438
+ when JSONObject
439
+ value = value.__getobj__
440
+ JSONB_COMBINED_WRAPPER_MAPPING[value.class].new(value)
276
441
  else
277
442
  raise Sequel::InvalidValue, "invalid value for jsonb: #{value.inspect}"
278
443
  end
@@ -282,40 +447,68 @@ module Sequel
282
447
 
283
448
  module SQL::Builders
284
449
  # Wrap the array or hash in a Postgres::JSONArray or Postgres::JSONHash.
450
+ # Also handles Postgres::JSONObject and JSONBObjects.
451
+ # For other objects, calls +Sequel.pg_json_op+ (which is defined
452
+ # by the pg_json_ops extension).
285
453
  def pg_json(v)
286
454
  case v
287
- when Postgres::JSONArray, Postgres::JSONHash
455
+ when Postgres::JSONObject
288
456
  v
289
457
  when Array
290
458
  Postgres::JSONArray.new(v)
291
459
  when Hash
292
460
  Postgres::JSONHash.new(v)
293
- when Postgres::JSONBArray
294
- Postgres::JSONArray.new(v.to_a)
295
- when Postgres::JSONBHash
296
- Postgres::JSONHash.new(v.to_hash)
461
+ when Postgres::JSONBObject
462
+ v = v.__getobj__
463
+ Postgres::JSON_COMBINED_WRAPPER_MAPPING[v.class].new(v)
297
464
  else
298
465
  Sequel.pg_json_op(v)
299
466
  end
300
467
  end
301
468
 
469
+ # Wraps Ruby array, hash, string, integer, float, true, false, and nil
470
+ # values with the appropriate JSON wrapper. Raises an exception for
471
+ # other types.
472
+ def pg_json_wrap(v)
473
+ case v
474
+ when *Postgres::JSON_WRAP_CLASSES
475
+ Postgres::JSON_COMBINED_WRAPPER_MAPPING[v.class].new(v)
476
+ else
477
+ raise Error, "invalid value passed to Sequel.pg_json_wrap: #{v.inspect}"
478
+ end
479
+ end
480
+
302
481
  # Wrap the array or hash in a Postgres::JSONBArray or Postgres::JSONBHash.
482
+ # Also handles Postgres::JSONObject and JSONBObjects.
483
+ # For other objects, calls +Sequel.pg_json_op+ (which is defined
484
+ # by the pg_json_ops extension).
303
485
  def pg_jsonb(v)
304
486
  case v
305
- when Postgres::JSONBArray, Postgres::JSONBHash
487
+ when Postgres::JSONBObject
306
488
  v
307
489
  when Array
308
490
  Postgres::JSONBArray.new(v)
309
491
  when Hash
310
492
  Postgres::JSONBHash.new(v)
311
- when Postgres::JSONArray
312
- Postgres::JSONBArray.new(v.to_a)
313
- when Postgres::JSONHash
314
- Postgres::JSONBHash.new(v.to_hash)
493
+ when Postgres::JSONObject
494
+ v = v.__getobj__
495
+ Postgres::JSONB_COMBINED_WRAPPER_MAPPING[v.class].new(v)
315
496
  else
316
497
  Sequel.pg_jsonb_op(v)
317
498
  end
318
499
  end
500
+
501
+ # Wraps Ruby array, hash, string, integer, float, true, false, and nil
502
+ # values with the appropriate JSONB wrapper. Raises an exception for
503
+ # other types.
504
+ def pg_jsonb_wrap(v)
505
+ case v
506
+ when *Postgres::JSONB_WRAP_CLASSES
507
+ Postgres::JSONB_COMBINED_WRAPPER_MAPPING[v.class].new(v)
508
+ else
509
+ raise Error, "invalid value passed to Sequel.pg_jsonb_wrap: #{v.inspect}"
510
+ end
511
+ end
319
512
  end
320
513
 
321
514
  Database.register_extension(:pg_json, Postgres::JSONDatabaseMethods)