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 +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)
|