associate_jsonb 0.0.4 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -31
  3. data/Rakefile +1 -3
  4. data/lib/associate_jsonb.rb +133 -5
  5. data/lib/associate_jsonb/arel_extensions/table.rb +1 -1
  6. data/lib/associate_jsonb/arel_extensions/visitors/postgresql.rb +113 -0
  7. data/lib/associate_jsonb/associations/association_scope.rb +2 -4
  8. data/lib/associate_jsonb/associations/belongs_to_association.rb +8 -8
  9. data/lib/associate_jsonb/associations/builder/belongs_to.rb +2 -2
  10. data/lib/associate_jsonb/attribute_methods.rb +19 -0
  11. data/lib/associate_jsonb/attribute_methods/read.rb +15 -0
  12. data/lib/associate_jsonb/connection_adapters/schema_creation.rb +168 -0
  13. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_foreign_key_function.rb +9 -0
  14. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_nested_set_function.rb +9 -0
  15. data/lib/associate_jsonb/connection_adapters/schema_definitions/alter_table.rb +40 -0
  16. data/lib/associate_jsonb/connection_adapters/schema_definitions/constraint_definition.rb +60 -0
  17. data/lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb +102 -0
  18. data/lib/associate_jsonb/connection_adapters/schema_definitions/table.rb +12 -0
  19. data/lib/associate_jsonb/connection_adapters/schema_definitions/table_definition.rb +25 -0
  20. data/lib/associate_jsonb/connection_adapters/schema_statements.rb +116 -0
  21. data/lib/associate_jsonb/persistence.rb +14 -0
  22. data/lib/associate_jsonb/predicate_builder.rb +15 -0
  23. data/lib/associate_jsonb/reflection.rb +2 -2
  24. data/lib/associate_jsonb/relation/where_clause.rb +3 -0
  25. data/lib/associate_jsonb/version.rb +1 -1
  26. data/lib/associate_jsonb/with_store_attribute.rb +31 -23
  27. metadata +29 -12
  28. 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: 5cb22feba951a6241ccada849b6d17ae06a4a84c797a49ae139844917d50474c
4
- data.tar.gz: 910b39ddc0f89d8525439d9d4e74244a62bbf3f4d07cb333306879312588dfda
3
+ metadata.gz: 847b6c8d5410dca5511a9ab2537ea3d2d9ba5c40e681ad42a63f0e752604d1ff
4
+ data.tar.gz: 935adff8c21a94b4d7593a236c9e56eec29c31340a897c82851b3662b0da880e
5
5
  SHA512:
6
- metadata.gz: 5382c87315fbbbc8bc52ebb5efb605e60373c7f24e9d681a008f5d5c53dc26649df89f7233ca1b3b6962fdd16a891c9072e55577a3c40e655faa54f3ec0dc375
7
- data.tar.gz: 3570c685cde53a273f70f5d434d1daacbc32b260b0b0500a3e6ef801b18a50fdd007a1970ede0d692a5eee89a87eba46b73ced15019514cfafece65b01c588a8
6
+ metadata.gz: c614936df446af3b0bd1301037954214e973cc4522637c3578f47f775065eb9085f08b668b11bf619ba3ca7968b14c078e192579be56eee489d5bf0780097552
7
+ data.tar.gz: 5243289a848da4cc15d03204a6d5bc6a502d810e6f839fb2b26aabc264bea63553b040a8508cf6f6d0722d82be5ea905c9bb75caa5e9ac1038b2e9d85e535122
data/README.md CHANGED
@@ -2,17 +2,98 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/associate_jsonb.svg)](https://badge.fury.io/rb/associate_jsonb)
4
4
 
5
- Basic ActiveRecord Associations using PostgreSQL JSONB columns, with built-in accessors and column indexes
5
+ #### Easy PostgreSQL JSONB extensions
6
+ **including:**
6
7
 
7
- <!-- This gem was created as a solution to this [task](http://cultofmartians.com/tasks/active-record-jsonb-associations.html) from [EvilMartians](http://evilmartians.com).
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 (>= 9.6)
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
- ### One-to-one and One-to-many associations
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
- Foreign keys for association on one model have to be unique, even if they use different store column.
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
- You can also use `add_references` in your migration to add JSONB column and index for it (if `index: true` option is set):
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
- add_reference :profiles, :users, store: :extra, index: true
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
- ### Many-to-many associations
145
+ #### enabling/adding attribute types
47
146
 
48
- 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
147
+ first, create the sql function
49
148
 
50
- #### Performance
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
- Compared to regular associations, fetching models associated via JSONB column has no drops in performance.
160
+ then in an initializer, enable key based updates:
53
161
 
54
- Getting the count of connected records is ~35% faster with associations via JSONB (tested on associations with up to 10 000 connections).
162
+ ```ruby
163
+ # config/initializers/associate_jsonb.rb
164
+ AssociateJsonb.enable_jsonb_set
165
+ ```
55
166
 
56
- Adding new connections is slightly faster with JSONB, for scopes up to 500 records connected to another record (total count of records in the table does not matter that much. If you have more then ~500 records connected to one record on average, and you want to add new records to the scope, JSONB associations will be slower then traditional:
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
- <img src="https://github.com/lebedev-yury/associate_jsonb/blob/master/doc/images/adding-associations.png?raw=true | width=500" alt="JSONB HAMTB is slower on adding associations" width="600">
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
- On the other hand, unassociating models from a big amount of associated models if faster with JSONB HABTM as the associations count grows:
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
- <img src="https://github.com/lebedev-yury/associate_jsonb/blob/master/doc/images/deleting-associations.png?raw=true | width=500" alt="JSONB HAMTB is faster on removing associations" width="600">
173
+ #### disabling/removing attribute types
63
174
 
64
- ## Installation
175
+ - by default `jsonb_nested_set` updates are disabled.
65
176
 
66
- Add this line to your application's Gemfile:
177
+ - if you've enabled them and need to disable, use: `AssociateJsonb.disable_jsonb_set`
67
178
 
68
- ```ruby
69
- gem 'associate_jsonb'
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
- And then execute:
182
+ ### Automatically delete nil value hash keys
73
183
 
74
- ```bash
75
- $ bundle install
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, just run:
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::Associations'
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')
@@ -12,19 +12,121 @@ require "mutex_m"
12
12
 
13
13
  require "zeitwerk"
14
14
  loader = Zeitwerk::Loader.for_gem
15
- loader.inflector.inflect "supported_rails_version" => "SUPPORTED_RAILS_VERSION"
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
- # ActiveRecord::Associations::Preloader::HasMany.prepend(
85
- # AssociateJsonb::Associations::Preloader::HasMany
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::DashArrow.
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