associate_jsonb 0.0.7 → 6.1.4.1.1
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 +123 -89
- data/Rakefile +1 -3
- data/lib/associate_jsonb/arel_nodes/sql_casted_equality.rb +0 -22
- data/lib/associate_jsonb/associations/alias_tracker.rb +2 -2
- data/lib/associate_jsonb/associations/association.rb +3 -17
- data/lib/associate_jsonb/associations/association_scope.rb +9 -12
- data/lib/associate_jsonb/associations/builder/belongs_to.rb +26 -3
- data/lib/associate_jsonb/associations/foreign_association.rb +23 -0
- data/lib/associate_jsonb/associations/join_dependency.rb +22 -9
- data/lib/associate_jsonb/associations/preloader/association.rb +24 -0
- data/lib/associate_jsonb/attribute_methods.rb +2 -2
- data/lib/associate_jsonb/connection_adapters/schema_creation.rb +10 -4
- data/lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb +15 -1
- data/lib/associate_jsonb/connection_adapters/schema_statements.rb +15 -10
- data/lib/associate_jsonb/reflection.rb +4 -4
- data/lib/associate_jsonb/relation/where_clause.rb +5 -27
- data/lib/associate_jsonb/supported_rails_version.rb +1 -1
- data/lib/associate_jsonb/version.rb +1 -1
- data/lib/associate_jsonb/with_store_attribute.rb +6 -6
- data/lib/associate_jsonb.rb +70 -27
- metadata +10 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f26470e30b277fe8ef0189ca4bef7eacf8cf7d4fa22008a879f5c1b187f9c476
|
4
|
+
data.tar.gz: 28ca2bd98358795e4e9d6a59e14738536e7a2696e0b4eacb11612e5bc33840a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6475d5e18d9c67d1b4b13a4770640ba79031357317dd185d77b8c959b8fdf1f59b52d6ee12fe1bed2e18652ed36747ce22c33af390bd63a3dbb87249c81479d2
|
7
|
+
data.tar.gz: 300b6fb1c2385bab73823765ee42d27e2b0a047efcf10b58727ad5e2973672910ca07a57ae76b64ff4ab32f006bf934d6376ddd975b9380600404d6264860955
|
data/README.md
CHANGED
@@ -2,97 +2,98 @@
|
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/associate_jsonb)
|
4
4
|
|
5
|
-
#### PostgreSQL JSONB extensions
|
6
|
-
|
7
|
-
|
5
|
+
#### Easy PostgreSQL JSONB extensions
|
6
|
+
**including:**
|
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
13
|
- PostgreSQL (>= 12)
|
12
14
|
- Rails 6.0.3.2
|
13
15
|
|
14
|
-
##
|
16
|
+
## Installation
|
15
17
|
|
16
|
-
|
18
|
+
Add this line to your application's Gemfile:
|
17
19
|
|
20
|
+
```ruby
|
21
|
+
gem 'associate_jsonb'
|
22
|
+
```
|
18
23
|
|
19
|
-
|
24
|
+
And then execute:
|
20
25
|
|
21
|
-
|
22
|
-
|
26
|
+
```bash
|
27
|
+
$ bundle install
|
28
|
+
```
|
23
29
|
|
24
|
-
|
25
|
-
```ruby
|
26
|
-
# given: instance#data == { "key_1"=>1,
|
27
|
-
# "key_2"=>2,
|
28
|
-
# "key_3"=> { "key_4"=>7,
|
29
|
-
# "key_5"=>8,
|
30
|
-
# "key_6"=>9 } }
|
30
|
+
## Usage
|
31
31
|
|
32
|
-
|
33
|
-
# instance#data => { "key_1"=>"asdf",
|
34
|
-
# "a"=>"asdf",
|
35
|
-
# "key_3"=> { "key_4"=>7,
|
36
|
-
# "key_6"=>9 } }
|
32
|
+
### Jsonb Associations
|
37
33
|
|
38
|
-
|
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
|
39
37
|
|
40
|
-
#### enabling/adding attribute types
|
41
|
-
first, create the sql function
|
42
38
|
```bash
|
43
|
-
rails g migration
|
39
|
+
rails g migration add_foreign_key_store_to_my_table
|
44
40
|
```
|
45
41
|
```ruby
|
46
|
-
class
|
47
|
-
def
|
48
|
-
|
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`
|
49
47
|
end
|
50
48
|
end
|
51
49
|
```
|
52
50
|
|
53
|
-
|
51
|
+
and
|
52
|
+
|
54
53
|
```ruby
|
55
|
-
|
56
|
-
|
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
|
57
62
|
```
|
58
63
|
|
59
|
-
|
60
|
-
|
61
|
-
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`
|
62
|
-
|
63
|
-
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
|
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.
|
64
65
|
|
65
|
-
|
66
|
-
by default `jsonb_nested_set` updates are disabled.
|
67
|
-
|
68
|
-
if you've enabled them and need to disable, use: `AssociateJsonb.disable_jsonb_set`
|
69
|
-
|
70
|
-
To remove a class from the allowed list while leaving nested set updates enabled, use `AssociateJsonb.remove_hash_type(*klasses)`.
|
71
|
-
Any arguments passed to `AssociateJsonb.disable_jsonb_set` are forwarded to `AssociateJsonb.remove_hash_type`
|
72
|
-
|
73
|
-
### Automatically delete nil value hash keys
|
74
|
-
|
75
|
-
When jsonb_set updates are disabled, jsonb columns are replaced with the current document (i.e. default rails behavior)
|
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
|
76
67
|
|
77
|
-
|
78
|
-
|
79
|
-
|
68
|
+
```bash
|
69
|
+
rails g migration add_jsonb_foreign_key_function
|
70
|
+
```
|
80
71
|
```ruby
|
81
|
-
|
82
|
-
|
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
|
83
95
|
```
|
84
96
|
|
85
|
-
Rules for classes to with this applies are the same as for `jsonb_nested_set`; add and remove classes through `AssociateJsonb.(add|remove)_hash_type(*klasses)`
|
86
|
-
|
87
|
-
<!-- This gem was created as a solution to this [task](http://cultofmartians.com/tasks/active-record-jsonb-associations.html) from [EvilMartians](http://evilmartians.com).
|
88
|
-
|
89
|
-
**Requirements:**
|
90
|
-
|
91
|
-
- PostgreSQL (>= 9.6)
|
92
|
-
|
93
|
-
## Usage
|
94
|
-
|
95
|
-
### One-to-one and One-to-many associations
|
96
97
|
|
97
98
|
You can store all foreign keys of your model in one JSONB column, without having to create multiple columns:
|
98
99
|
|
@@ -115,49 +116,87 @@ class User < ActiveRecord::Base
|
|
115
116
|
end
|
116
117
|
```
|
117
118
|
|
118
|
-
|
119
|
+
### Many-to-Many associations
|
119
120
|
|
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
|
124
|
+
|
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.
|
121
129
|
|
122
130
|
```ruby
|
123
|
-
|
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 } }
|
124
143
|
```
|
125
144
|
|
126
|
-
|
145
|
+
#### enabling/adding attribute types
|
127
146
|
|
128
|
-
|
147
|
+
first, create the sql function
|
148
|
+
|
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
|
+
```
|
129
159
|
|
130
|
-
|
160
|
+
then in an initializer, enable key based updates:
|
131
161
|
|
132
|
-
|
162
|
+
```ruby
|
163
|
+
# config/initializers/associate_jsonb.rb
|
164
|
+
AssociateJsonb.enable_jsonb_set
|
165
|
+
```
|
133
166
|
|
134
|
-
|
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`
|
135
168
|
|
136
|
-
|
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`
|
137
170
|
|
138
|
-
|
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
|
139
172
|
|
140
|
-
|
173
|
+
#### disabling/removing attribute types
|
141
174
|
|
142
|
-
|
175
|
+
- by default `jsonb_nested_set` updates are disabled.
|
143
176
|
|
144
|
-
|
177
|
+
- if you've enabled them and need to disable, use: `AssociateJsonb.disable_jsonb_set`
|
145
178
|
|
146
|
-
|
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`
|
147
181
|
|
148
|
-
|
149
|
-
gem 'associate_jsonb'
|
150
|
-
```
|
182
|
+
### Automatically delete nil value hash keys
|
151
183
|
|
152
|
-
|
184
|
+
When jsonb_set updates are disabled, jsonb columns are replaced with the current document (i.e. default rails behavior)
|
153
185
|
|
154
|
-
|
155
|
-
|
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
|
156
193
|
```
|
157
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
|
+
|
158
197
|
## Developing
|
159
198
|
|
160
|
-
To setup development environment,
|
199
|
+
To setup development environment, run:
|
161
200
|
|
162
201
|
```bash
|
163
202
|
$ bin/setup
|
@@ -169,11 +208,6 @@ To run specs:
|
|
169
208
|
$ bundle exec rspec
|
170
209
|
```
|
171
210
|
|
172
|
-
To run benchmarks (that will take a while):
|
173
|
-
|
174
|
-
```bash
|
175
|
-
$ bundle exec rake benchmarks:habtm
|
176
|
-
``` -->
|
177
|
-
|
178
211
|
## License
|
212
|
+
|
179
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')
|
@@ -1,28 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# module AssociateJsonb
|
5
|
-
# module ArelNodes
|
6
|
-
# class SqlCastedEquality < ::Arel::Nodes::Equality
|
7
|
-
# attr_reader :original_left
|
8
|
-
# def initialize(left, cast_as, right)
|
9
|
-
# @original_left = left
|
10
|
-
# super(
|
11
|
-
# ::Arel::Nodes::NamedFunction.new(
|
12
|
-
# "CAST",
|
13
|
-
# [ left.as(cast_as) ]
|
14
|
-
# ),
|
15
|
-
# right
|
16
|
-
# )
|
17
|
-
# end
|
18
|
-
# end
|
19
|
-
# end
|
20
|
-
# end
|
21
|
-
|
22
|
-
|
23
|
-
# encoding: utf-8
|
24
|
-
# frozen_string_literal: true
|
25
|
-
|
26
4
|
module AssociateJsonb
|
27
5
|
module ArelNodes
|
28
6
|
class SqlCastedEquality < AssociateJsonb::ArelNodes::SqlCastedBinary
|
@@ -5,8 +5,8 @@ require "active_support/core_ext/string/conversions"
|
|
5
5
|
module AssociateJsonb
|
6
6
|
module Associations
|
7
7
|
module AliasTracker # :nodoc:
|
8
|
-
def aliased_table_for(
|
9
|
-
super(
|
8
|
+
def aliased_table_for(arel_table, table_name = nil, store_tracker: nil)
|
9
|
+
super(arel_table, table_name).with_store_tracker(store_tracker)
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -12,28 +12,14 @@ module AssociateJsonb
|
|
12
12
|
assigned_keys = record.changed_attribute_names_to_save
|
13
13
|
assigned_keys += except_from_scope_attributes.keys.map(&:to_s)
|
14
14
|
attributes = scope_for_create.except!(*(assigned_keys - skip_assign))
|
15
|
+
|
15
16
|
if attributes.key?(reflection.foreign_store_key.to_s)
|
16
|
-
|
17
|
-
attributes[reflection.foreign_key.to_s] = v
|
17
|
+
attributes[reflection.foreign_key.to_s] = attributes.delete(reflection.foreign_store_key.to_s)
|
18
18
|
end
|
19
|
+
|
19
20
|
record.send(:_assign_attributes, attributes) if attributes.any?
|
20
21
|
set_inverse_instance(record)
|
21
22
|
end
|
22
|
-
|
23
|
-
private
|
24
|
-
def creation_attributes
|
25
|
-
return super if reflection.belongs_to?
|
26
|
-
return super unless reflection.foreign_store?
|
27
|
-
|
28
|
-
attributes = {}
|
29
|
-
|
30
|
-
jsonb_store = reflection.foreign_store_attr
|
31
|
-
attributes[jsonb_store] ||= {}
|
32
|
-
attributes[jsonb_store][reflection.foreign_store_key] =
|
33
|
-
owner[reflection.active_record_primary_key]
|
34
|
-
|
35
|
-
attributes
|
36
|
-
end
|
37
23
|
end
|
38
24
|
end
|
39
25
|
end
|
@@ -4,17 +4,13 @@
|
|
4
4
|
module AssociateJsonb
|
5
5
|
module Associations
|
6
6
|
module AssociationScope #:nodoc:
|
7
|
-
|
8
7
|
def get_chain(reflection, association, tracker)
|
9
8
|
name = reflection.name
|
10
9
|
chain = [ActiveRecord::Reflection::RuntimeReflection.new(reflection, association)]
|
11
10
|
reflection.chain.drop(1).each do |refl|
|
12
|
-
aliased_table = tracker.aliased_table_for(
|
13
|
-
refl.
|
14
|
-
|
15
|
-
refl.klass.type_caster,
|
16
|
-
refl.klass.store_column_attribute_tracker
|
17
|
-
)
|
11
|
+
aliased_table = tracker.aliased_table_for(refl.klass.arel_table, store_tracker: refl.klass.store_column_attribute_tracker) do
|
12
|
+
refl.alias_candidate(name)
|
13
|
+
end
|
18
14
|
chain << ActiveRecord::Associations::AssociationScope::ReflectionProxy.new(refl, aliased_table)
|
19
15
|
end
|
20
16
|
chain
|
@@ -25,18 +21,19 @@ module AssociateJsonb
|
|
25
21
|
reflection = owner_reflection.instance_variable_get(:@reflection)
|
26
22
|
return super unless reflection&.foreign_store?
|
27
23
|
|
24
|
+
primary_key = reflection.join_primary_key
|
25
|
+
foreign_key = reflection.join_foreign_key
|
26
|
+
store_key = reflection.foreign_store_key || primary_key
|
28
27
|
|
29
|
-
join_keys = owner_reflection.join_keys
|
30
28
|
table = owner_reflection.aliased_table
|
31
|
-
|
32
|
-
value = transform_value(owner[join_keys.foreign_key])
|
29
|
+
value = transform_value(owner[foreign_key])
|
33
30
|
|
34
31
|
apply_jsonb_equality(
|
35
32
|
scope,
|
36
33
|
table,
|
37
34
|
reflection.foreign_store_attr,
|
38
|
-
|
39
|
-
|
35
|
+
store_key.to_s,
|
36
|
+
owner_reflection.join_foreign_key,
|
40
37
|
value,
|
41
38
|
reflection.active_record
|
42
39
|
)
|
@@ -5,6 +5,29 @@ module AssociateJsonb
|
|
5
5
|
module Associations
|
6
6
|
module Builder
|
7
7
|
module BelongsTo #:nodoc:
|
8
|
+
# Maybe convert to builder callbacks here in next iteration
|
9
|
+
# def define_callbacks(model, reflection)
|
10
|
+
# super
|
11
|
+
# add_after_jsonb_initialize_callbacks(model, reflection) if reflection.jsonb_store?
|
12
|
+
# add_after_foreign_store_initialize_callbacks(model, reflection) if reflection.foreign_store?
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def add_after_jsonb_initialize_callbacks(model, reflection)
|
16
|
+
# model.after_initialize lambda {|record|
|
17
|
+
# association = association(reflection.name)
|
18
|
+
# p model, reflection.join_primary_key, reflection.join_foreign_key, reflection.options
|
19
|
+
# p record.attributes
|
20
|
+
# # record.attributes._write_attribute()
|
21
|
+
# }
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def add_after_foreign_store_initialize_callbacks(model, reflection)
|
25
|
+
# model.after_initialize lambda {|record|
|
26
|
+
# association = association(reflection.name)
|
27
|
+
# # record.attributes._write_attribute()
|
28
|
+
# }
|
29
|
+
# end
|
30
|
+
|
8
31
|
def valid_options(options)
|
9
32
|
super + %i[ store store_key ]
|
10
33
|
end
|
@@ -18,7 +41,7 @@ module AssociateJsonb
|
|
18
41
|
end
|
19
42
|
|
20
43
|
def add_association_accessor_methods(mixin, reflection)
|
21
|
-
foreign_key = reflection.
|
44
|
+
foreign_key = reflection.join_foreign_key.to_s
|
22
45
|
key = (reflection.jsonb_store_key || foreign_key).to_s
|
23
46
|
store = reflection.jsonb_store_attr
|
24
47
|
|
@@ -35,8 +58,8 @@ module AssociateJsonb
|
|
35
58
|
foreign_type = :integer
|
36
59
|
sql_type = "numeric"
|
37
60
|
begin
|
38
|
-
primary_key = reflection.
|
39
|
-
primary_column = reflection.klass.
|
61
|
+
primary_key = reflection.join_primary_key.to_s
|
62
|
+
primary_column = reflection.klass.columns_hash[primary_key]
|
40
63
|
|
41
64
|
if primary_column
|
42
65
|
foreign_type = primary_column.type
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module Associations
|
6
|
+
module ForeignAssociation # :nodoc:
|
7
|
+
private
|
8
|
+
# Sets the owner attributes on the given record
|
9
|
+
def set_owner_attributes(record)
|
10
|
+
return if options[:through]
|
11
|
+
return super unless reflection.foreign_store?
|
12
|
+
|
13
|
+
jsonb_store = reflection.foreign_store_attr.to_s
|
14
|
+
value = record._read_attribute(jsonb_store).presence || {}
|
15
|
+
fk_value = owner._read_attribute(reflection.join_foreign_key)
|
16
|
+
value[reflection.foreign_store_key] = fk_value
|
17
|
+
|
18
|
+
record._write_attribute(reflection.join_primary_key, fk_value)
|
19
|
+
record._write_attribute(jsonb_store, value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -6,15 +6,28 @@ module AssociateJsonb
|
|
6
6
|
module Associations
|
7
7
|
module JoinDependency # :nodoc:
|
8
8
|
private
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
)
|
17
|
-
|
9
|
+
def make_constraints(parent, child, join_type)
|
10
|
+
foreign_table = parent.table
|
11
|
+
foreign_klass = parent.base_klass
|
12
|
+
child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) do |reflection|
|
13
|
+
table, terminated = @joined_tables[reflection]
|
14
|
+
root = reflection == child.reflection
|
15
|
+
|
16
|
+
if table && (!root || !terminated)
|
17
|
+
@joined_tables[reflection] = [table, root] if root
|
18
|
+
next table, true
|
19
|
+
end
|
20
|
+
|
21
|
+
table_name = @references[reflection.name.to_sym]&.to_s
|
22
|
+
|
23
|
+
table = alias_tracker.aliased_table_for(reflection.klass.arel_table, table_name, store_tracker: reflection.klass.store_column_attribute_tracker) do
|
24
|
+
name = reflection.alias_candidate(parent.table_name)
|
25
|
+
root ? name : "#{name}_join"
|
26
|
+
end
|
27
|
+
|
28
|
+
@joined_tables[reflection] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
|
29
|
+
table
|
30
|
+
end.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
|
18
31
|
end
|
19
32
|
end
|
20
33
|
end
|
@@ -18,6 +18,30 @@ module AssociateJsonb
|
|
18
18
|
).intersects_with(ids)
|
19
19
|
)
|
20
20
|
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def load_records
|
24
|
+
return super unless reflection.foreign_store?
|
25
|
+
# owners can be duplicated when a relation has a collection association join
|
26
|
+
# #compare_by_identity makes such owners different hash keys
|
27
|
+
@records_by_owner = {}.compare_by_identity
|
28
|
+
raw_records = owner_keys.empty? ? [] : records_for(owner_keys)
|
29
|
+
|
30
|
+
@preloaded_records = raw_records.select do |record|
|
31
|
+
assignments = false
|
32
|
+
|
33
|
+
owners_by_key[convert_key(record[association_key_name])].each do |owner|
|
34
|
+
entries = (@records_by_owner[owner] ||= [])
|
35
|
+
|
36
|
+
if reflection.collection? || entries.empty?
|
37
|
+
entries << record
|
38
|
+
assignments = true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
assignments
|
43
|
+
end
|
44
|
+
end
|
21
45
|
end
|
22
46
|
end
|
23
47
|
end
|
@@ -11,8 +11,8 @@ module AssociateJsonb
|
|
11
11
|
|
12
12
|
private
|
13
13
|
def attributes_with_info(attribute_names)
|
14
|
-
attribute_names.
|
15
|
-
|
14
|
+
attribute_names.index_with do |name|
|
15
|
+
_fetch_attribute(name)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -30,7 +30,7 @@ module AssociateJsonb
|
|
30
30
|
end
|
31
31
|
|
32
32
|
create_sql << "(#{statements.join(', ')})" if statements.present?
|
33
|
-
add_table_options!(create_sql,
|
33
|
+
add_table_options!(create_sql, o)
|
34
34
|
create_sql << " AS #{to_sql(o.as)}" if o.as
|
35
35
|
create_sql
|
36
36
|
end
|
@@ -89,11 +89,16 @@ module AssociateJsonb
|
|
89
89
|
DECLARE
|
90
90
|
does_exist BOOLEAN;
|
91
91
|
BEGIN
|
92
|
-
IF store
|
92
|
+
IF store->>key IS NULL
|
93
93
|
THEN
|
94
94
|
return nullable;
|
95
95
|
END IF;
|
96
96
|
|
97
|
+
IF store->>key = ''
|
98
|
+
THEN
|
99
|
+
return FALSE;
|
100
|
+
END IF;
|
101
|
+
|
97
102
|
EXECUTE FORMAT('SELECT EXISTS (SELECT 1 FROM %1$I WHERE %1$I.%2$I = CAST($1 AS ' || type || '))', table_name, foreign_key)
|
98
103
|
INTO does_exist
|
99
104
|
USING store->>key;
|
@@ -151,8 +156,9 @@ module AssociateJsonb
|
|
151
156
|
def add_column_options!(sql, opts)
|
152
157
|
super
|
153
158
|
|
154
|
-
if opts[:constraint]
|
155
|
-
|
159
|
+
if constraint_opts = opts[:constraint]
|
160
|
+
constraint_opts = { value: constraint_opts } unless constraint_opts.is_a?(Hash)
|
161
|
+
sql << " #{accept(ConstraintDefinition.new(**constraint_opts))}"
|
156
162
|
end
|
157
163
|
|
158
164
|
sql
|
@@ -60,11 +60,25 @@ module AssociateJsonb
|
|
60
60
|
deferrable: true
|
61
61
|
)
|
62
62
|
end
|
63
|
+
elsif !nullable
|
64
|
+
columns.each do |col_name, *|
|
65
|
+
value = <<-SQL.squish
|
66
|
+
#{store}->>'#{col_name}' IS NOT NULL
|
67
|
+
AND
|
68
|
+
#{store}->>'#{col_name}' <> ''
|
69
|
+
SQL
|
70
|
+
table.constraint(
|
71
|
+
name: "#{table.name}_#{col_name}_not_null",
|
72
|
+
value: value,
|
73
|
+
not_valid: false,
|
74
|
+
deferrable: true
|
75
|
+
)
|
76
|
+
end
|
63
77
|
end
|
64
78
|
|
65
79
|
return unless index
|
66
80
|
|
67
|
-
columns.each do |col_name, type,
|
81
|
+
columns.each do |col_name, type, *|
|
68
82
|
type = :text if type == :string
|
69
83
|
table.index(
|
70
84
|
"CAST (\"#{store}\"->>'#{col_name}' AS #{type || :bigint})",
|
@@ -12,38 +12,43 @@ module AssociateJsonb
|
|
12
12
|
execute schema_creation.accept(AddJsonbForeignKeyFunction.new)
|
13
13
|
end
|
14
14
|
|
15
|
-
def create_table(table_name, **options)
|
16
|
-
td = create_table_definition(table_name, **options)
|
15
|
+
def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
|
16
|
+
td = create_table_definition(table_name, **extract_table_options!(options))
|
17
17
|
|
18
|
-
if
|
19
|
-
pk =
|
20
|
-
|
18
|
+
if id && !td.as
|
19
|
+
pk = primary_key || ActiveRecord::Base.get_primary_key(table_name.to_s.singularize)
|
20
|
+
|
21
|
+
if id.is_a?(Hash)
|
22
|
+
options.merge!(id.except(:type))
|
23
|
+
id = id.fetch(:type, :primary_key)
|
21
24
|
end
|
22
25
|
|
23
26
|
if pk.is_a?(Array)
|
24
27
|
td.primary_keys pk
|
25
28
|
else
|
26
|
-
td.primary_key pk,
|
29
|
+
td.primary_key pk, id, **options
|
27
30
|
end
|
28
31
|
end
|
29
32
|
|
30
33
|
yield td if block_given?
|
31
34
|
|
32
|
-
if
|
33
|
-
drop_table(table_name,
|
35
|
+
if force
|
36
|
+
drop_table(table_name, force: force, if_exists: true)
|
37
|
+
else
|
38
|
+
schema_cache.clear_data_source_cache!(table_name.to_s)
|
34
39
|
end
|
35
40
|
|
36
41
|
result = execute schema_creation.accept td
|
37
42
|
|
38
43
|
td.indexes.each do |column_name, index_options|
|
39
|
-
add_index(table_name, column_name, index_options)
|
44
|
+
add_index(table_name, column_name, **index_options, if_not_exists: td.if_not_exists)
|
40
45
|
end
|
41
46
|
|
42
47
|
td.constraints.each do |ct|
|
43
48
|
add_constraint(table_name, **ct)
|
44
49
|
end
|
45
50
|
|
46
|
-
if table_comment =
|
51
|
+
if table_comment = td.comment.presence
|
47
52
|
change_table_comment(table_name, table_comment)
|
48
53
|
end
|
49
54
|
|
@@ -18,7 +18,7 @@ module AssociateJsonb
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def jsonb_store_key
|
21
|
-
options[:store_key].presence ||
|
21
|
+
options[:store_key].presence || join_foreign_key
|
22
22
|
end
|
23
23
|
|
24
24
|
def foreign_store?
|
@@ -36,7 +36,7 @@ module AssociateJsonb
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def foreign_store_key
|
39
|
-
options[:foreign_store_key].presence ||
|
39
|
+
options[:foreign_store_key].presence || join_primary_key
|
40
40
|
end
|
41
41
|
|
42
42
|
def join_scope(table, foreign_table, foreign_klass)
|
@@ -52,8 +52,8 @@ module AssociateJsonb
|
|
52
52
|
|
53
53
|
scope_chain_items.inject(klass_scope, &:merge!)
|
54
54
|
|
55
|
-
key =
|
56
|
-
foreign_key =
|
55
|
+
key = join_primary_key
|
56
|
+
foreign_key = join_foreign_key
|
57
57
|
|
58
58
|
if foreign_store?
|
59
59
|
klass_scope.where!(
|
@@ -4,36 +4,14 @@
|
|
4
4
|
module AssociateJsonb
|
5
5
|
module Relation
|
6
6
|
module WhereClause
|
7
|
-
def to_h(table_name = nil)
|
8
|
-
equalities
|
9
|
-
|
10
|
-
equalities = equalities.select do |node|
|
11
|
-
node.original_left.relation.name == table_name
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
equalities.map { |node|
|
7
|
+
def to_h(table_name = nil, equality_only: false)
|
8
|
+
equalities(predicates, equality_only).each_with_object({}) do |node, hash|
|
9
|
+
next if table_name&.!= node.original_left.relation.name
|
16
10
|
name = node.original_left.name.to_s
|
17
11
|
value = extract_node_value(node.right)
|
18
|
-
[name
|
19
|
-
}.to_h
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
def equalities(predicates)
|
24
|
-
equalities = []
|
25
|
-
|
26
|
-
predicates.each do |node|
|
27
|
-
case node
|
28
|
-
when Arel::Nodes::Equality, Arel::Nodes::SqlCastedEquality
|
29
|
-
equalities << node
|
30
|
-
when Arel::Nodes::And
|
31
|
-
equalities.concat equalities(node.children)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
equalities
|
12
|
+
hash[name] = value
|
36
13
|
end
|
14
|
+
end
|
37
15
|
end
|
38
16
|
end
|
39
17
|
end
|
@@ -99,15 +99,15 @@ module AssociateJsonb
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def store_column_attribute(store, attr, cast_type = ActiveRecord::Type::Value.new, sql_type: nil, key: nil, **attribute_opts)
|
102
|
-
store = store.
|
103
|
-
attr = attr.
|
102
|
+
store = store.to_s
|
103
|
+
attr = attr.to_s
|
104
104
|
key ||= attr
|
105
105
|
key = key.to_s
|
106
106
|
array = attribute_opts[:array]
|
107
107
|
attribute attr, cast_type, **attribute_opts
|
108
108
|
|
109
109
|
instance_eval <<~CODE, __FILE__, __LINE__ + 1
|
110
|
-
add_store_column_attribute_name(
|
110
|
+
add_store_column_attribute_name(attr, store, key, { sql_type: sql_type, type: cast_type, opts: attribute_opts })
|
111
111
|
CODE
|
112
112
|
|
113
113
|
include WithStoreAttribute::InstanceMethodsOnActivation.new(self, store, attr, key, array)
|
@@ -120,9 +120,9 @@ module AssociateJsonb
|
|
120
120
|
|
121
121
|
array_or_attr = ->(value) {
|
122
122
|
is_array \
|
123
|
-
|
124
|
-
|
125
|
-
|
123
|
+
? %Q(Array(#{value})) \
|
124
|
+
: %Q(#{value})
|
125
|
+
}
|
126
126
|
|
127
127
|
on_store_change = "_write_attribute(:#{attribute}, #{array_or_attr.call %Q(#{store}["#{key}"])})"
|
128
128
|
on_attr_change = "super(#{array_or_attr.call %Q(given)})"
|
data/lib/associate_jsonb.rb
CHANGED
@@ -20,64 +20,106 @@ loader.collapse("#{__dir__}/associate_jsonb/connection_adapters/schema_definitio
|
|
20
20
|
loader.setup # ready!
|
21
21
|
|
22
22
|
module AssociateJsonb
|
23
|
-
mattr_accessor :jsonb_hash_types,
|
24
|
-
mattr_accessor :
|
23
|
+
mattr_accessor :jsonb_hash_types, default: []
|
24
|
+
mattr_accessor :jsonb_set_added, default: [] # :nodoc:
|
25
|
+
mattr_accessor :jsonb_set_removed, default: [] # :nodoc:
|
25
26
|
mattr_accessor :jsonb_set_enabled, default: false
|
26
|
-
mattr_accessor :jsonb_delete_nil,
|
27
|
+
mattr_accessor :jsonb_delete_nil, default: false
|
27
28
|
private_class_method :jsonb_hash_types=
|
28
29
|
private_class_method :jsonb_set_enabled=
|
29
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 to
|
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
|
30
43
|
def self.enable_jsonb_set(klass = nil, *classes)
|
31
|
-
|
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
|
32
49
|
self.jsonb_set_enabled = true
|
33
50
|
end
|
34
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
|
35
57
|
def self.disable_jsonb_set(klass = nil, *classes)
|
36
58
|
remove_hash_type(*Array(klass), *classes) unless klass.nil?
|
37
59
|
self.jsonb_set_enabled = false
|
38
60
|
end
|
39
61
|
|
62
|
+
##
|
63
|
+
# Add class(es) to the list of classes that are able to be upated when
|
64
|
+
# `jsonb_set_enabled` is true
|
40
65
|
def self.add_hash_type(*classes)
|
41
|
-
self.
|
66
|
+
self.jsonb_set_added |= classes.flatten
|
42
67
|
end
|
43
68
|
|
69
|
+
##
|
70
|
+
# Remove class(es) from the list of classes that are able to be upated when
|
71
|
+
# `jsonb_set_enabled` is true
|
44
72
|
def self.remove_hash_type(*classes)
|
45
73
|
self.jsonb_set_removed |= classes.flatten
|
46
74
|
end
|
47
75
|
|
48
|
-
|
49
|
-
|
50
|
-
|
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)
|
51
81
|
end
|
52
82
|
|
53
|
-
|
54
|
-
|
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) }
|
55
88
|
end
|
56
89
|
end
|
57
90
|
|
58
|
-
|
59
91
|
# rubocop:disable Metrics/BlockLength
|
60
92
|
ActiveSupport.on_load :active_record do
|
61
93
|
loader.eager_load
|
62
|
-
AssociateJsonb.
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
68
100
|
end
|
69
|
-
|
101
|
+
}
|
102
|
+
|
103
|
+
redefine_method.call(:jsonb_oid_class, true) do
|
104
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
|
70
105
|
end
|
71
106
|
|
72
|
-
|
107
|
+
redefine_method.call(:add_hash_type) do |*classes|
|
108
|
+
self.jsonb_hash_types |= classes.flatten
|
109
|
+
end
|
73
110
|
|
74
|
-
|
111
|
+
redefine_method.call(:remove_hash_type) do |*classes|
|
75
112
|
self.jsonb_hash_types -= classes.flatten
|
76
113
|
end
|
77
|
-
|
78
|
-
self.
|
79
|
-
self.
|
80
|
-
|
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
|
81
123
|
end
|
82
124
|
|
83
125
|
|
@@ -145,9 +187,10 @@ ActiveSupport.on_load :active_record do
|
|
145
187
|
AssociateJsonb::Associations::Preloader::Association
|
146
188
|
)
|
147
189
|
|
148
|
-
|
149
|
-
|
150
|
-
|
190
|
+
ActiveRecord::Associations::ForeignAssociation.prepend(
|
191
|
+
AssociateJsonb::Associations::ForeignAssociation
|
192
|
+
)
|
193
|
+
|
151
194
|
%i[
|
152
195
|
AlterTable
|
153
196
|
ConstraintDefinition
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: associate_jsonb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 6.1.4.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sampson Crowley
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-09-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,20 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 6.
|
19
|
+
version: 6.1.4
|
20
20
|
- - ">="
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 6.
|
22
|
+
version: 6.1.4.1
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - "~>"
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 6.
|
29
|
+
version: 6.1.4
|
30
30
|
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 6.
|
32
|
+
version: 6.1.4.1
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: pg
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,14 +96,14 @@ dependencies:
|
|
96
96
|
requirements:
|
97
97
|
- - "~>"
|
98
98
|
- !ruby/object:Gem::Version
|
99
|
-
version: 3.
|
99
|
+
version: '3.9'
|
100
100
|
type: :development
|
101
101
|
prerelease: false
|
102
102
|
version_requirements: !ruby/object:Gem::Requirement
|
103
103
|
requirements:
|
104
104
|
- - "~>"
|
105
105
|
- !ruby/object:Gem::Version
|
106
|
-
version: 3.
|
106
|
+
version: '3.9'
|
107
107
|
description: |
|
108
108
|
This gem extends ActiveRecord to add additional functionality to JSONB
|
109
109
|
|
@@ -149,6 +149,7 @@ files:
|
|
149
149
|
- lib/associate_jsonb/associations/builder/has_many.rb
|
150
150
|
- lib/associate_jsonb/associations/builder/has_one.rb
|
151
151
|
- lib/associate_jsonb/associations/conflicting_association.rb
|
152
|
+
- lib/associate_jsonb/associations/foreign_association.rb
|
152
153
|
- lib/associate_jsonb/associations/has_many_association.rb
|
153
154
|
- lib/associate_jsonb/associations/join_dependency.rb
|
154
155
|
- lib/associate_jsonb/associations/preloader/association.rb
|
@@ -190,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
190
191
|
- !ruby/object:Gem::Version
|
191
192
|
version: '0'
|
192
193
|
requirements: []
|
193
|
-
rubygems_version: 3.1.
|
194
|
+
rubygems_version: 3.1.6
|
194
195
|
signing_key:
|
195
196
|
specification_version: 4
|
196
197
|
summary: Store database references in PostgreSQL Jsonb columns
|