associate_jsonb 0.0.7 → 6.1.4.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0832e64c43e22ba206a98d0603c0e231c1606b17315f715a4050e19a1fdb21b
4
- data.tar.gz: ff37b2777adf131f20ad9a95076d8797ecf30dc2ddeaa86913de5a457cf9fed4
3
+ metadata.gz: f26470e30b277fe8ef0189ca4bef7eacf8cf7d4fa22008a879f5c1b187f9c476
4
+ data.tar.gz: 28ca2bd98358795e4e9d6a59e14738536e7a2696e0b4eacb11612e5bc33840a6
5
5
  SHA512:
6
- metadata.gz: 00fbd32bc9e66fa8bd85fcf9cab51850bc65443a99f66dbc3bcff5ca5d05e855624c62ac9a0aa7bcbed330a2650dbe149806c49bc3bb40ecd3aa6cdf002fde19
7
- data.tar.gz: b134266f93576482f3558be28840081b803982ce369bd75497af2d9d2d2db30e3c27364247485ef2a4712705fb01183d80b812f6fa28f18d8f7db37db555bb88
6
+ metadata.gz: 6475d5e18d9c67d1b4b13a4770640ba79031357317dd185d77b8c959b8fdf1f59b52d6ee12fe1bed2e18652ed36747ce22c33af390bd63a3dbb87249c81479d2
7
+ data.tar.gz: 300b6fb1c2385bab73823765ee42d27e2b0a047efcf10b58727ad5e2973672910ca07a57ae76b64ff4ab32f006bf934d6376ddd975b9380600404d6264860955
data/README.md CHANGED
@@ -2,97 +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
- #### PostgreSQL JSONB extensions including:
6
- - Basic ActiveRecord Associations using PostgreSQL JSONB columns, with built-in accessors and column indexes
7
- - Thread-Safe JSONB updates (well, as safe as they can be) using a custom nested version of `jsonb_set` (`jsonb_nested_set`)
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
- ## Usage
16
+ ## Installation
15
17
 
16
- ### Jsonb Associations
18
+ Add this line to your application's Gemfile:
17
19
 
20
+ ```ruby
21
+ gem 'associate_jsonb'
22
+ ```
18
23
 
19
- ### jsonb_set based hash updates
24
+ And then execute:
20
25
 
21
- When enabled, *only* keys present in the updated hash and with values changed in memory will be updated.
22
- To completely delete a key, value pair from an enabled attribute, set the key's value to `nil`.
26
+ ```bash
27
+ $ bundle install
28
+ ```
23
29
 
24
- e.g.
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
- instance.update({ key_1: "asdf", a: 1, key_2: nil, key_3: { key_5: nil }})
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 add_jsonb_nested_set_function
39
+ rails g migration add_foreign_key_store_to_my_table
44
40
  ```
45
41
  ```ruby
46
- class CreateState < ActiveRecord::Migration[6.0]
47
- def up
48
- add_jsonb_nested_set_function
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
- then in an initializer, enable key based updates:
51
+ and
52
+
54
53
  ```ruby
55
- # config/initializers/associate_jsonb.rb
56
- AssociateJsonb.enable_jsonb_set
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
- 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`
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
- #### disabling/removing attribute types
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
- You are also given the option to automatically clear nil/null values from the hash automatically
78
-
79
- in an initializer, enable stripping nil values:
68
+ ```bash
69
+ rails g migration add_jsonb_foreign_key_function
70
+ ```
80
71
  ```ruby
81
- # config/initializers/associate_jsonb.rb
82
- AssociateJsonb.jsonb_delete_nil = true
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
- Foreign keys for association on one model have to be unique, even if they use different store column.
119
+ ### Many-to-Many associations
119
120
 
120
- You can also use `add_references` in your migration to add JSONB column and index for it (if `index: true` option is set):
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
- 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 } }
124
143
  ```
125
144
 
126
- ### Many-to-many associations
145
+ #### enabling/adding attribute types
127
146
 
128
- 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
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
- #### Performance
160
+ then in an initializer, enable key based updates:
131
161
 
132
- Compared to regular associations, fetching models associated via JSONB column has no drops in performance.
162
+ ```ruby
163
+ # config/initializers/associate_jsonb.rb
164
+ AssociateJsonb.enable_jsonb_set
165
+ ```
133
166
 
134
- Getting the count of connected records is ~35% faster with associations via JSONB (tested on associations with up to 10 000 connections).
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
- 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:
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
- <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">
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
- On the other hand, unassociating models from a big amount of associated models if faster with JSONB HABTM as the associations count grows:
173
+ #### disabling/removing attribute types
141
174
 
142
- <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">
175
+ - by default `jsonb_nested_set` updates are disabled.
143
176
 
144
- ## Installation
177
+ - if you've enabled them and need to disable, use: `AssociateJsonb.disable_jsonb_set`
145
178
 
146
- Add this line to your application's Gemfile:
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
- ```ruby
149
- gem 'associate_jsonb'
150
- ```
182
+ ### Automatically delete nil value hash keys
151
183
 
152
- And then execute:
184
+ When jsonb_set updates are disabled, jsonb columns are replaced with the current document (i.e. default rails behavior)
153
185
 
154
- ```bash
155
- $ bundle install
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, just run:
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::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')
@@ -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(table_name, aliased_name, type_caster, store_tracker = nil)
9
- super(table_name, aliased_name, type_caster).with_store_tracker(store_tracker)
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
- v = attributes.delete(reflection.foreign_store_key.to_s)
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.table_name,
14
- refl.alias_candidate(name),
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
- key = reflection.foreign_store_key || join_keys.key
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
- key.to_s,
39
- join_keys.foreign_key,
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.foreign_key.to_s
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.active_record_primary_key.to_s
39
- primary_column = reflection.klass.columns.find {|col| col.name == primary_key }
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 table_aliases_for(parent, node)
10
- node.reflection.chain.map { |reflection|
11
- alias_tracker.aliased_table_for(
12
- reflection.table_name,
13
- table_alias_for(reflection, parent, reflection != node.reflection),
14
- reflection.klass.type_caster,
15
- reflection.klass.store_column_attribute_tracker
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.each_with_object({}) do |name, attrs|
15
- attrs[name] = _fetch_attribute(name)
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, table_options(o))
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->key IS NULL
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
- sql << " #{accept(ConstraintDefinition.new(**opts[:constraint]))}"
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, opts|
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 options[:id] != false && !options[:as]
19
- pk = options.fetch(:primary_key) do
20
- ActiveRecord::Base.get_primary_key table_name.to_s.singularize
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, options.fetch(:id, :primary_key), **options.except(:comment)
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 options[:force]
33
- drop_table(table_name, **options, if_exists: true)
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 = options[:comment].presence
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 || join_keys.foreign_key
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 || join_keys.key
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 = join_keys.key
56
- foreign_key = join_keys.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 = equalities(predicates)
9
- if table_name
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, value]
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module AssociateJsonb
5
- SUPPORTED_RAILS_VERSION = "6.0.3.2"
5
+ SUPPORTED_RAILS_VERSION = "6.1.4.1"
6
6
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module AssociateJsonb
5
- VERSION = "0.0.7"
5
+ VERSION = "6.1.4.1.1"
6
6
  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.to_sym
103
- attr = attr.to_sym
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("#{attr}", :#{store}, "#{key}", { sql_type: sql_type, type: cast_type, opts: attribute_opts })
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
- ? %Q(Array(#{value})) \
124
- : %Q(#{value})
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)})"
@@ -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, default: []
24
- mattr_accessor :jsonb_set_removed, default: []
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, default: false
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
- add_hash_type(*Array(klass), *classes) unless klass.nil?
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.jsonb_hash_types |= classes.flatten
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
- def self.merge_hash?(v)
49
- return false unless jsonb_set_enabled && v
50
- self.jsonb_hash_types.any? { |type| v.is_a?(type) }
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
- def self.is_hash?(v)
54
- self.jsonb_hash_types.any? { |type| v.is_a?(type) }
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.class_eval do
63
- def self.enable_jsonb_set(klass = nil, *classes)
64
- if klass.nil?
65
- add_hash_type ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb if jsonb_hash_types.empty?
66
- else
67
- add_hash_type |= [*Array(klass), *classes].flatten
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
- self.jsonb_set_enabled = true
101
+ }
102
+
103
+ redefine_method.call(:jsonb_oid_class, true) do
104
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
70
105
  end
71
106
 
72
- self.enable_jsonb_set if jsonb_set_enabled
107
+ redefine_method.call(:add_hash_type) do |*classes|
108
+ self.jsonb_hash_types |= classes.flatten
109
+ end
73
110
 
74
- def self.remove_hash_type(*classes)
111
+ redefine_method.call(:remove_hash_type) do |*classes|
75
112
  self.jsonb_hash_types -= classes.flatten
76
113
  end
77
- removed = jsonb_set_removed
78
- self.remove_hash_type removed
79
- self.send :remove_method, :jsonb_set_removed
80
- self.send :remove_method, :jsonb_set_removed=
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
- # ActiveRecord::Associations::Preloader::HasMany.prepend(
149
- # AssociateJsonb::Associations::Preloader::HasMany
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: 0.0.7
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: 2020-08-04 00:00:00.000000000 Z
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.0.3
19
+ version: 6.1.4
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 6.0.3.2
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.0.3
29
+ version: 6.1.4
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 6.0.3.2
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.7.0
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.7.0
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.3
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