associate_jsonb 0.0.4 → 0.0.10
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/README.md +145 -31
- data/Rakefile +1 -3
- data/lib/associate_jsonb.rb +133 -5
- data/lib/associate_jsonb/arel_extensions/table.rb +1 -1
- data/lib/associate_jsonb/arel_extensions/visitors/postgresql.rb +113 -0
- data/lib/associate_jsonb/associations/association_scope.rb +2 -4
- data/lib/associate_jsonb/associations/belongs_to_association.rb +8 -8
- data/lib/associate_jsonb/associations/builder/belongs_to.rb +2 -2
- data/lib/associate_jsonb/attribute_methods.rb +19 -0
- data/lib/associate_jsonb/attribute_methods/read.rb +15 -0
- data/lib/associate_jsonb/connection_adapters/schema_creation.rb +168 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_foreign_key_function.rb +9 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_nested_set_function.rb +9 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/alter_table.rb +40 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/constraint_definition.rb +60 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb +102 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/table.rb +12 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/table_definition.rb +25 -0
- data/lib/associate_jsonb/connection_adapters/schema_statements.rb +116 -0
- data/lib/associate_jsonb/persistence.rb +14 -0
- data/lib/associate_jsonb/predicate_builder.rb +15 -0
- data/lib/associate_jsonb/reflection.rb +2 -2
- data/lib/associate_jsonb/relation/where_clause.rb +3 -0
- data/lib/associate_jsonb/version.rb +1 -1
- data/lib/associate_jsonb/with_store_attribute.rb +31 -23
- metadata +29 -12
- data/lib/associate_jsonb/connection_adapters/reference_definition.rb +0 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 847b6c8d5410dca5511a9ab2537ea3d2d9ba5c40e681ad42a63f0e752604d1ff
|
4
|
+
data.tar.gz: 935adff8c21a94b4d7593a236c9e56eec29c31340a897c82851b3662b0da880e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c614936df446af3b0bd1301037954214e973cc4522637c3578f47f775065eb9085f08b668b11bf619ba3ca7968b14c078e192579be56eee489d5bf0780097552
|
7
|
+
data.tar.gz: 5243289a848da4cc15d03204a6d5bc6a502d810e6f839fb2b26aabc264bea63553b040a8508cf6f6d0722d82be5ea905c9bb75caa5e9ac1038b2e9d85e535122
|
data/README.md
CHANGED
@@ -2,17 +2,98 @@
|
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/associate_jsonb)
|
4
4
|
|
5
|
-
|
5
|
+
#### Easy PostgreSQL JSONB extensions
|
6
|
+
**including:**
|
6
7
|
|
7
|
-
|
8
|
+
- Basic ActiveRecord Associations using PostgreSQL JSONB columns, with built-in accessors and column indexes
|
9
|
+
- Thread-Safe JSONB updates (well, as safe as they can be) using a custom nested version of `jsonb_set` (`jsonb_nested_set`)
|
8
10
|
|
9
11
|
**Requirements:**
|
10
12
|
|
11
|
-
- PostgreSQL (>=
|
13
|
+
- PostgreSQL (>= 12)
|
14
|
+
- Rails 6.0.3.2
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'associate_jsonb'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
$ bundle install
|
28
|
+
```
|
12
29
|
|
13
30
|
## Usage
|
14
31
|
|
15
|
-
###
|
32
|
+
### Jsonb Associations
|
33
|
+
|
34
|
+
#### One-to-One and One-to-Many associations
|
35
|
+
|
36
|
+
To set up your jsonb column, you can use the built in `add_reference`/`table.references` function. This will only add a new store column if it doesn't already exist
|
37
|
+
|
38
|
+
```bash
|
39
|
+
rails g migration add_foreign_key_store_to_my_table
|
40
|
+
```
|
41
|
+
```ruby
|
42
|
+
class AddForeignKeyStoreToMyTable < ActiveRecord::Migration[6.0]
|
43
|
+
def change
|
44
|
+
add_reference :my_table, :user, store: :extra # => store created
|
45
|
+
add_reference :my_table, :label, store: :extra, null: false # => store already exists, NOT NULL check constraint added to `store->'label_id'`
|
46
|
+
# NOTE: you can also use a `change_table(:my_table) block`
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
and
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class CreateMyTable < ActiveRecord::Migration[6.0]
|
55
|
+
def change
|
56
|
+
create_table(:my_table) do |t|
|
57
|
+
t.references :user, store: :extra
|
58
|
+
t.references :label, store: :extra, null: false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
If you add the `jsonb_foreign_key` function to your database, you can also create a foreign_key **check** constraint by using the same built-in `:foreign_key` option used in normal reference definitions.
|
65
|
+
|
66
|
+
**NOTE**: true foreign key references are not possible with jsonb attributes. This will instead create a CHECK constraint that looks for the referenced column using an `EXISTS` statement
|
67
|
+
|
68
|
+
```bash
|
69
|
+
rails g migration add_jsonb_foreign_key_function
|
70
|
+
```
|
71
|
+
```ruby
|
72
|
+
class AddJsonbForeignKeyFunction < ActiveRecord::Migration[6.0]
|
73
|
+
def up
|
74
|
+
add_jsonb_foreign_key_function
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
```ruby
|
79
|
+
class CreateMyTable < ActiveRecord::Migration[6.0]
|
80
|
+
def change
|
81
|
+
create_table(:my_table) do |t|
|
82
|
+
t.references :user, store: :extra, foreign_key: true, null: false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
```
|
87
|
+
```ruby
|
88
|
+
class CreateMyTable < ActiveRecord::Migration[6.0]
|
89
|
+
def change
|
90
|
+
create_table(:my_table) do |t|
|
91
|
+
t.references :person, store: :extra, foreign_key: { to_table: :users }, null: false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
16
97
|
|
17
98
|
You can store all foreign keys of your model in one JSONB column, without having to create multiple columns:
|
18
99
|
|
@@ -35,49 +116,87 @@ class User < ActiveRecord::Base
|
|
35
116
|
end
|
36
117
|
```
|
37
118
|
|
38
|
-
|
119
|
+
### Many-to-Many associations
|
120
|
+
|
121
|
+
Due to the ease of getting out-of-sync, and the complexity needed to build it, HABTM relation functionality has not been implemented through JSONB
|
122
|
+
|
123
|
+
### jsonb_set based hash updates
|
39
124
|
|
40
|
-
|
125
|
+
When enabled, *only* keys present in the updated hash and with values changed in memory will be updated.
|
126
|
+
To completely delete a `key/value` pair from an enabled attribute, set the key's value to `nil`.
|
127
|
+
|
128
|
+
e.g.
|
41
129
|
|
42
130
|
```ruby
|
43
|
-
|
131
|
+
# given: instance#data == { "key_1"=>1,
|
132
|
+
# "key_2"=>2,
|
133
|
+
# "key_3"=> { "key_4"=>7,
|
134
|
+
# "key_5"=>8,
|
135
|
+
# "key_6"=>9 } }
|
136
|
+
|
137
|
+
instance.update({ key_1: "asdf", a: 1, key_2: nil, key_3: { key_5: nil }})
|
138
|
+
|
139
|
+
# instance#data => { "key_1"=>"asdf",
|
140
|
+
# "a"=>"asdf",
|
141
|
+
# "key_3"=> { "key_4"=>7,
|
142
|
+
# "key_6"=>9 } }
|
44
143
|
```
|
45
144
|
|
46
|
-
|
145
|
+
#### enabling/adding attribute types
|
47
146
|
|
48
|
-
|
147
|
+
first, create the sql function
|
49
148
|
|
50
|
-
|
149
|
+
```bash
|
150
|
+
rails g migration add_jsonb_nested_set_function
|
151
|
+
```
|
152
|
+
```ruby
|
153
|
+
class AddJsonbNestedSetFunction < ActiveRecord::Migration[6.0]
|
154
|
+
def up
|
155
|
+
add_jsonb_nested_set_function
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
51
159
|
|
52
|
-
|
160
|
+
then in an initializer, enable key based updates:
|
53
161
|
|
54
|
-
|
162
|
+
```ruby
|
163
|
+
# config/initializers/associate_jsonb.rb
|
164
|
+
AssociateJsonb.enable_jsonb_set
|
165
|
+
```
|
55
166
|
|
56
|
-
|
167
|
+
- Key based updates rely on inheritance for allowed attribute types. Any attributes that respond true to `attr_type.is_a?(GivenClass)` for any enabled type classes will use `jsonb_nested_set`
|
57
168
|
|
58
|
-
|
169
|
+
- To add classes to the enabled list, pass them as arguments to `AssociateJsonb.add_hash_type(*klasses)`. Any arguments passed to `AssociateJsonb.enable_jsonb_set` are forwarded to `AssociateJsonb.add_hash_type`
|
59
170
|
|
60
|
-
|
171
|
+
- By default, calling `AssociateJsonb.enable_jsonb_set(*klasses)` without arguments, and no classes previously added, adds `ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb` to the allowed classes list
|
61
172
|
|
62
|
-
|
173
|
+
#### disabling/removing attribute types
|
63
174
|
|
64
|
-
|
175
|
+
- by default `jsonb_nested_set` updates are disabled.
|
65
176
|
|
66
|
-
|
177
|
+
- if you've enabled them and need to disable, use: `AssociateJsonb.disable_jsonb_set`
|
67
178
|
|
68
|
-
|
69
|
-
|
70
|
-
```
|
179
|
+
- To remove a class from the allowed list while leaving nested set updates enabled, use `AssociateJsonb.remove_hash_type(*klasses)`.
|
180
|
+
Any arguments passed to `AssociateJsonb.disable_jsonb_set` are forwarded to `AssociateJsonb.remove_hash_type`
|
71
181
|
|
72
|
-
|
182
|
+
### Automatically delete nil value hash keys
|
73
183
|
|
74
|
-
|
75
|
-
|
184
|
+
When jsonb_set updates are disabled, jsonb columns are replaced with the current document (i.e. default rails behavior)
|
185
|
+
|
186
|
+
You are also given the option to automatically clear nil/null values from the hash automatically when jsonb_set is disabled
|
187
|
+
|
188
|
+
in an initializer:
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
# config/initializers/associate_jsonb.rb
|
192
|
+
AssociateJsonb.jsonb_delete_nil = true
|
76
193
|
```
|
77
194
|
|
195
|
+
Rules for classes to which this applies are the same as for `jsonb_nested_set`; add and remove classes through `AssociateJsonb.(add|remove)_hash_type(*klasses)`
|
196
|
+
|
78
197
|
## Developing
|
79
198
|
|
80
|
-
To setup development environment,
|
199
|
+
To setup development environment, run:
|
81
200
|
|
82
201
|
```bash
|
83
202
|
$ bin/setup
|
@@ -89,11 +208,6 @@ To run specs:
|
|
89
208
|
$ bundle exec rspec
|
90
209
|
```
|
91
210
|
|
92
|
-
To run benchmarks (that will take a while):
|
93
|
-
|
94
|
-
```bash
|
95
|
-
$ bundle exec rake benchmarks:habtm
|
96
|
-
``` -->
|
97
|
-
|
98
211
|
## License
|
212
|
+
|
99
213
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -4,13 +4,11 @@ rescue LoadError
|
|
4
4
|
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
5
|
end
|
6
6
|
|
7
|
-
Rake.add_rakelib 'benchmarks'
|
8
|
-
|
9
7
|
require 'rdoc/task'
|
10
8
|
|
11
9
|
RDoc::Task.new(:rdoc) do |rdoc|
|
12
10
|
rdoc.rdoc_dir = 'rdoc'
|
13
|
-
rdoc.title = 'AssociateJsonb
|
11
|
+
rdoc.title = 'AssociateJsonb'
|
14
12
|
rdoc.options << '--line-numbers'
|
15
13
|
rdoc.rdoc_files.include('README.md')
|
16
14
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
data/lib/associate_jsonb.rb
CHANGED
@@ -12,19 +12,121 @@ require "mutex_m"
|
|
12
12
|
|
13
13
|
require "zeitwerk"
|
14
14
|
loader = Zeitwerk::Loader.for_gem
|
15
|
-
loader.inflector.inflect
|
15
|
+
loader.inflector.inflect(
|
16
|
+
"postgresql" => "PostgreSQL",
|
17
|
+
"supported_rails_version" => "SUPPORTED_RAILS_VERSION"
|
18
|
+
)
|
19
|
+
loader.collapse("#{__dir__}/associate_jsonb/connection_adapters/schema_definitions")
|
16
20
|
loader.setup # ready!
|
17
21
|
|
18
22
|
module AssociateJsonb
|
23
|
+
mattr_accessor :jsonb_hash_types, default: []
|
24
|
+
mattr_accessor :jsonb_set_added, default: [] # :nodoc:
|
25
|
+
mattr_accessor :jsonb_set_removed, default: [] # :nodoc:
|
26
|
+
mattr_accessor :jsonb_set_enabled, default: false
|
27
|
+
mattr_accessor :jsonb_delete_nil, default: false
|
28
|
+
private_class_method :jsonb_hash_types=
|
29
|
+
private_class_method :jsonb_set_enabled=
|
30
|
+
|
31
|
+
def self.jsonb_oid_class # :nodoc:
|
32
|
+
:default
|
33
|
+
end
|
34
|
+
private_class_method :jsonb_oid_class
|
35
|
+
|
36
|
+
##
|
37
|
+
# Enables the use of `jsonb_nested_set` for hash updates
|
38
|
+
#
|
39
|
+
# if passed a class, or a list of classes, those classes will be added todo
|
40
|
+
# the enabled classes. if no argument is given, and the enabled class list is
|
41
|
+
# empty, `ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb` is added
|
42
|
+
# to the list of enabled classes
|
43
|
+
def self.enable_jsonb_set(klass = nil, *classes)
|
44
|
+
if klass.nil?
|
45
|
+
add_hash_type jsonb_oid_class if jsonb_hash_types.empty?
|
46
|
+
else
|
47
|
+
add_hash_type(*Array(klass), *classes)
|
48
|
+
end
|
49
|
+
self.jsonb_set_enabled = true
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Disables the use of `jsonb_nested_set` for hash updates
|
54
|
+
#
|
55
|
+
# if passed a class, or a list of classes, those classes will be removed from
|
56
|
+
# the list of enabled classes
|
57
|
+
def self.disable_jsonb_set(klass = nil, *classes)
|
58
|
+
remove_hash_type(*Array(klass), *classes) unless klass.nil?
|
59
|
+
self.jsonb_set_enabled = false
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Add class(es) to the list of classes that are able to be upated when
|
64
|
+
# `jsonb_set_enabled` is true
|
65
|
+
def self.add_hash_type(*classes)
|
66
|
+
self.jsonb_set_added |= classes.flatten
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Remove class(es) from the list of classes that are able to be upated when
|
71
|
+
# `jsonb_set_enabled` is true
|
72
|
+
def self.remove_hash_type(*classes)
|
73
|
+
self.jsonb_set_removed |= classes.flatten
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Returns true if `jsonb_set_enabled` is true and the value is an enabled hash
|
78
|
+
# type
|
79
|
+
def self.merge_hash?(value)
|
80
|
+
!!jsonb_set_enabled && is_hash?(value)
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Returns true if the given value is a descendant of any of the classes
|
85
|
+
# in `jsonb_hash_types`
|
86
|
+
def self.is_hash?(value)
|
87
|
+
!!value && self.jsonb_hash_types.any? { |type| value.is_a?(type) }
|
88
|
+
end
|
19
89
|
end
|
20
90
|
|
21
|
-
|
22
91
|
# rubocop:disable Metrics/BlockLength
|
23
92
|
ActiveSupport.on_load :active_record do
|
24
93
|
loader.eager_load
|
94
|
+
AssociateJsonb.module_eval do
|
95
|
+
redefine_method = proc {|name, hide = false, &block|
|
96
|
+
method(name).owner.remove_method name
|
97
|
+
if block
|
98
|
+
define_singleton_method(name, &block)
|
99
|
+
private_class_method name if hide
|
100
|
+
end
|
101
|
+
}
|
102
|
+
|
103
|
+
redefine_method.call(:jsonb_oid_class, true) do
|
104
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
|
105
|
+
end
|
106
|
+
|
107
|
+
redefine_method.call(:add_hash_type) do |*classes|
|
108
|
+
self.jsonb_hash_types |= classes.flatten
|
109
|
+
end
|
110
|
+
|
111
|
+
redefine_method.call(:remove_hash_type) do |*classes|
|
112
|
+
self.jsonb_hash_types -= classes.flatten
|
113
|
+
end
|
114
|
+
|
115
|
+
self.add_hash_type jsonb_set_added.map {|k| (k == :default) ? self.jsonb_oid_class : k}
|
116
|
+
self.remove_hash_type jsonb_set_removed
|
117
|
+
|
118
|
+
redefine_method.call(:jsonb_set_added)
|
119
|
+
redefine_method.call(:jsonb_set_added=)
|
120
|
+
redefine_method.call(:jsonb_set_removed)
|
121
|
+
redefine_method.call(:jsonb_set_removed=)
|
122
|
+
redefine_method = nil
|
123
|
+
end
|
124
|
+
|
25
125
|
|
26
126
|
ActiveRecord::Base.include AssociateJsonb::WithStoreAttribute
|
27
127
|
ActiveRecord::Base.include AssociateJsonb::Associations
|
128
|
+
ActiveRecord::Base.include AssociateJsonb::AttributeMethods
|
129
|
+
ActiveRecord::Base.include AssociateJsonb::Persistence
|
28
130
|
|
29
131
|
Arel::Nodes.include AssociateJsonb::ArelNodes
|
30
132
|
|
@@ -40,6 +142,10 @@ ActiveSupport.on_load :active_record do
|
|
40
142
|
AssociateJsonb::ArelExtensions::Table
|
41
143
|
)
|
42
144
|
|
145
|
+
Arel::Visitors::PostgreSQL.prepend(
|
146
|
+
AssociateJsonb::ArelExtensions::Visitors::PostgreSQL
|
147
|
+
)
|
148
|
+
|
43
149
|
Arel::Visitors::Visitor.singleton_class.prepend(
|
44
150
|
AssociateJsonb::ArelExtensions::Visitors::Visitor
|
45
151
|
)
|
@@ -81,11 +187,33 @@ ActiveSupport.on_load :active_record do
|
|
81
187
|
AssociateJsonb::Associations::Preloader::Association
|
82
188
|
)
|
83
189
|
|
84
|
-
|
85
|
-
|
86
|
-
|
190
|
+
%i[
|
191
|
+
AlterTable
|
192
|
+
ConstraintDefinition
|
193
|
+
ReferenceDefinition
|
194
|
+
SchemaCreation
|
195
|
+
Table
|
196
|
+
TableDefinition
|
197
|
+
].each do |m|
|
198
|
+
includable = AssociateJsonb::ConnectionAdapters.const_get(m)
|
199
|
+
including =
|
200
|
+
begin
|
201
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL.const_get(m)
|
202
|
+
rescue NameError
|
203
|
+
ActiveRecord::ConnectionAdapters.const_get(m)
|
204
|
+
end
|
205
|
+
including.prepend includable
|
206
|
+
rescue NameError
|
207
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL.const_set(m, includable)
|
208
|
+
end
|
209
|
+
|
210
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(
|
211
|
+
AssociateJsonb::ConnectionAdapters::SchemaStatements
|
212
|
+
)
|
213
|
+
|
87
214
|
|
88
215
|
ActiveRecord::Reflection::AbstractReflection.prepend AssociateJsonb::Reflection
|
216
|
+
ActiveRecord::PredicateBuilder.prepend AssociateJsonb::PredicateBuilder
|
89
217
|
ActiveRecord::Relation::WhereClause.prepend AssociateJsonb::Relation::WhereClause
|
90
218
|
|
91
219
|
ActiveRecord::ConnectionAdapters::ReferenceDefinition.prepend(
|
@@ -23,7 +23,7 @@ module AssociateJsonb
|
|
23
23
|
def [](name)
|
24
24
|
return super unless store_col = store_tracker&.get(name)
|
25
25
|
|
26
|
-
attr = ::Arel::Nodes::Jsonb::
|
26
|
+
attr = ::Arel::Nodes::Jsonb::DashDoubleArrow.
|
27
27
|
new(self, self[store_col[:store]], store_col[:key])
|
28
28
|
|
29
29
|
if cast_as = (store_col[:cast] && store_col[:cast][:sql_type])
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module AssociateJsonb
|
2
|
+
module ArelExtensions
|
3
|
+
module Visitors
|
4
|
+
module PostgreSQL
|
5
|
+
private
|
6
|
+
def collect_hash_changes(original, updated, nesting = nil)
|
7
|
+
keys = original.keys.map(&:to_s)
|
8
|
+
updated_keys = updated.keys.map(&:to_s)
|
9
|
+
keys |= updated_keys
|
10
|
+
original = original.with_indifferent_access
|
11
|
+
updated = updated.with_indifferent_access
|
12
|
+
added = []
|
13
|
+
deleted = []
|
14
|
+
finished = {}
|
15
|
+
keys.each do |k|
|
16
|
+
if updated[k].is_a?(Hash)
|
17
|
+
finished[k], a, d = collect_hash_changes(original[k].is_a?(Hash) ? original[k] : {}, updated[k], nesting ? "#{nesting},#{k}" : k)
|
18
|
+
a = [[(nesting ? "{#{nesting},#{k}}" : "{#{k}}"), {}]] if original[k].nil? && a.blank?
|
19
|
+
added |= a
|
20
|
+
deleted |= d
|
21
|
+
elsif updated[k].nil?
|
22
|
+
deleted << (nesting ? "{#{nesting},#{k}}" : "{#{k}}") if updated_keys.include?(k)
|
23
|
+
elsif original[k] != updated[k]
|
24
|
+
finished[k] = updated[k]
|
25
|
+
added << [(nesting ? "{#{nesting},#{k}}" : "{#{k}}"), updated[k]]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
[ finished, added, deleted ]
|
29
|
+
end
|
30
|
+
|
31
|
+
def is_hash?(type)
|
32
|
+
AssociateJsonb.is_hash? type
|
33
|
+
end
|
34
|
+
|
35
|
+
def is_update?(collector)
|
36
|
+
collector &&
|
37
|
+
Array(collector.value).any? {|v| v.is_a?(String) && (v =~ /UPDATE/) }
|
38
|
+
rescue
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_insert?(collector)
|
43
|
+
collector &&
|
44
|
+
Array(collector.value).any? {|v| v.is_a?(String) && (v =~ /INSERT INTO/) }
|
45
|
+
rescue
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def visit_BindHashChanges(t, collector)
|
50
|
+
changes, additions, deletions =
|
51
|
+
collect_hash_changes(
|
52
|
+
t.original_value.presence || {},
|
53
|
+
t.value.presence || {}
|
54
|
+
)
|
55
|
+
|
56
|
+
base_json = +"COALESCE(#{quote_column_name(t.name)}, '{}'::jsonb)"
|
57
|
+
json = base_json
|
58
|
+
|
59
|
+
deletions.each do |del|
|
60
|
+
json = +"(#{json} #- '#{del}')"
|
61
|
+
end
|
62
|
+
|
63
|
+
coalesced_paths = []
|
64
|
+
additions.sort.each do |add, value|
|
65
|
+
collector.add_bind(t.with_value_from_user(value)) do |i|
|
66
|
+
json = +"jsonb_nested_set(#{json},'#{add}', COALESCE($#{i}, '{}'::jsonb))"
|
67
|
+
''
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
collector << json
|
72
|
+
end
|
73
|
+
|
74
|
+
def visit_Arel_Nodes_BindParam(o, collector)
|
75
|
+
catch(:nodes_bound) do
|
76
|
+
if AssociateJsonb.jsonb_set_enabled
|
77
|
+
catch(:not_hashable) do
|
78
|
+
if is_hash?(o.value.type)
|
79
|
+
if is_update?(collector)
|
80
|
+
visit_BindHashChanges(o.value, collector)
|
81
|
+
|
82
|
+
throw :nodes_bound, collector
|
83
|
+
elsif is_insert?(collector)
|
84
|
+
value = o.value
|
85
|
+
|
86
|
+
value, _, _ =
|
87
|
+
collect_hash_changes(
|
88
|
+
{},
|
89
|
+
value.value.presence || {}
|
90
|
+
)
|
91
|
+
throw :nodes_bound, collector.add_bind(o.value.with_cast_value(value)) { |i| "$#{i}"}
|
92
|
+
else
|
93
|
+
throw :not_hashable
|
94
|
+
end
|
95
|
+
else
|
96
|
+
throw :not_hashable
|
97
|
+
end
|
98
|
+
end
|
99
|
+
elsif AssociateJsonb.jsonb_delete_nil && is_hash?(o.value.type)
|
100
|
+
value, _, _ =
|
101
|
+
collect_hash_changes(
|
102
|
+
{},
|
103
|
+
o.value.value.presence || {}
|
104
|
+
)
|
105
|
+
throw :nodes_bound, collector.add_bind(o.value.with_cast_value(value)) { |i| "$#{i}" }
|
106
|
+
end
|
107
|
+
throw :nodes_bound, collector.add_bind(o.value) { |i| "$#{i}" }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|