sequel 5.20.0 → 5.21.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.
- checksums.yaml +4 -4
- data/CHANGELOG +24 -0
- data/doc/opening_databases.rdoc +2 -1
- data/doc/release_notes/5.21.0.txt +87 -0
- data/doc/sharding.rdoc +2 -0
- data/lib/sequel/adapters/ado.rb +27 -19
- data/lib/sequel/adapters/jdbc/mysql.rb +2 -2
- data/lib/sequel/adapters/shared/sqlite.rb +11 -1
- data/lib/sequel/adapters/sqlite.rb +1 -1
- data/lib/sequel/adapters/utils/mysql_mysql2.rb +2 -0
- data/lib/sequel/extensions/named_timezones.rb +2 -0
- data/lib/sequel/extensions/pg_json.rb +316 -123
- data/lib/sequel/extensions/server_block.rb +15 -4
- data/lib/sequel/plugins/rcte_tree.rb +6 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +24 -0
- data/spec/adapters/mysql_spec.rb +0 -5
- data/spec/adapters/postgres_spec.rb +180 -1
- data/spec/extensions/pg_json_spec.rb +206 -29
- data/spec/extensions/rcte_tree_spec.rb +6 -0
- data/spec/extensions/server_block_spec.rb +38 -0
- data/spec/integration/dataset_test.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33b35675e5f68471b8216acbbe10085992616ae93b6f47191c44bb3860b38285
|
4
|
+
data.tar.gz: e1be4ce80c4810a9022505f7e5d0becaa11912f6e91344b860195e6b11438d27
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)
|
data/doc/opening_databases.rdoc
CHANGED
@@ -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
|
data/doc/sharding.rdoc
CHANGED
@@ -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
|
data/lib/sequel/adapters/ado.rb
CHANGED
@@ -47,34 +47,40 @@ module Sequel
|
|
47
47
|
#AdVarWChar = 202
|
48
48
|
#AdWChar = 130
|
49
49
|
|
50
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
64
|
+
binary = Object.new
|
65
|
+
def binary.call(v)
|
61
66
|
Sequel.blob(v.pack('c*'))
|
62
67
|
end
|
63
68
|
|
64
|
-
|
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
|
-
[
|
71
|
-
[
|
72
|
-
[
|
73
|
-
[
|
74
|
-
].each do |
|
75
|
-
|
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] =
|
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
|
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
|
-
|
257
|
-
|
258
|
-
|
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
|
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
|
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
|
@@ -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.
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# strings, true
|
8
|
-
#
|
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
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
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
|
24
|
-
# use
|
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
|
-
#
|
27
|
-
#
|
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
|
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
|
34
|
-
#
|
46
|
+
# array.pg_json # json type
|
47
|
+
# array.pg_jsonb # jsonb type
|
35
48
|
#
|
36
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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[]
|
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::
|
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
|
-
#
|
69
|
-
|
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
|
-
|
81
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
97
|
-
|
98
|
-
include Sequel::SQL::AliasMethods
|
99
|
-
include Sequel::SQL::CastMethods
|
138
|
+
json_class = Class.new(base_class) do
|
139
|
+
include JSONObject
|
100
140
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
108
|
-
|
109
|
-
end
|
147
|
+
jsonb_class = Class.new(base_class) do
|
148
|
+
include JSONBObject
|
110
149
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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,
|
132
|
-
add_conversion_proc(3802,
|
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] = [
|
138
|
-
@schema_type_classes[:jsonb] = [
|
226
|
+
@schema_type_classes[:json] = [JSONObject]
|
227
|
+
@schema_type_classes[:jsonb] = [JSONBObject]
|
139
228
|
end
|
140
229
|
end
|
141
230
|
|
142
|
-
|
143
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
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
|
404
|
+
# Convert the value given to a JSON wrapper object.
|
242
405
|
def typecast_value_json(value)
|
243
406
|
case value
|
244
|
-
when
|
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
|
-
|
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
|
425
|
+
# Convert the value given to a JSONB wrapper object.
|
262
426
|
def typecast_value_jsonb(value)
|
263
427
|
case value
|
264
|
-
when
|
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
|
-
|
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::
|
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::
|
294
|
-
|
295
|
-
|
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::
|
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::
|
312
|
-
|
313
|
-
|
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)
|