carl-activerecord-jsonb-associations 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c6f265762ccbacb8a4e8b40b02bc3ad49946d06e48bcb6a1789dd8bb5c41ded5
4
+ data.tar.gz: 0da67ebbd5b7af62f8a3801dacc5fa2ddc41e14cee1de3d8eaf191a0953373e4
5
+ SHA512:
6
+ metadata.gz: 51229c5583a137999fc0112f341098864a6fce3ce326ee9918dc04115100085eccd4536be93efb69b8bcbc5a7fa310c6d96ccfd7f453b8180ff1c760620c5d3f
7
+ data.tar.gz: f4e876e65a827f71b207af0ed033a11aa34aeb8c4d8330d72c3a6726d01eab4a65ef22b0681567367035ee179c5869c3a7a8e47331b6b09d7af6a13dd8bd639a
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Yury Lebedev
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # activerecord-json-associations
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/activerecord-jsonb-associations.svg)](https://badge.fury.io/rb/activerecord-jsonb-associations)
4
+
5
+ Use PostgreSQL JSONB fields to store association information of your models.
6
+
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
+
9
+ **Requirements:**
10
+
11
+ - PostgreSQL (>= 9.6)
12
+
13
+ ## Usage
14
+
15
+ Add to Gemfile
16
+
17
+ ```
18
+ gem 'carl-activerecord-jsonb-associations', require 'activerecord-jsonb-associations'
19
+ ```
20
+
21
+ ### One-to-one and One-to-many associations
22
+
23
+ You can store all foreign keys of your model in one JSONB column, without having to create multiple columns:
24
+
25
+ ```ruby
26
+ class Profile < ActiveRecord::Base
27
+ # Setting additional :store option on :belongs_to association
28
+ # enables saving of foreign ids in :extra JSONB column
29
+ belongs_to :user, store: :extra
30
+ end
31
+
32
+ class SocialProfile < ActiveRecord::Base
33
+ belongs_to :user, store: :extra
34
+ end
35
+
36
+ class User < ActiveRecord::Base
37
+ # Parent model association needs to specify :foreign_store
38
+ # for associations with JSONB storage
39
+ has_one :profile, foreign_store: :extra
40
+ has_many :social_profiles, foreign_store: :extra
41
+ end
42
+ ```
43
+
44
+ Foreign keys for association on one model have to be unique, even if they use different store column.
45
+
46
+ You can also use `add_references` in your migration to add JSONB column and index for it (if `index: true` option is set):
47
+
48
+ ```ruby
49
+ add_reference :profiles, :users, store: :extra, index: true
50
+ ```
51
+
52
+ ### Many-to-many associations
53
+
54
+ You can also use JSONB columns on 2 sides of a HABTM association. This way you won't have to create a join table.
55
+
56
+ ```ruby
57
+ class Label < ActiveRecord::Base
58
+ # extra['user_ids'] will store associated user ids
59
+ has_and_belongs_to_many :users, store: :extra
60
+ end
61
+
62
+ class User < ActiveRecord::Base
63
+ # extra['label_ids'] will store associated label ids
64
+ has_and_belongs_to_many :labels, store: :extra
65
+ end
66
+ ```
67
+
68
+ #### Performance
69
+
70
+ Compared to regular associations, fetching models associated via JSONB column has no drops in performance.
71
+
72
+ Getting the count of connected records is ~35% faster with associations via JSONB (tested on associations with up to 10 000 connections).
73
+
74
+ 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:
75
+
76
+ <img src="https://github.com/lebedev-yury/activerecord-jsonb-associations/blob/master/doc/images/adding-associations.png?raw=true | width=500" alt="JSONB HAMTB is slower on adding associations" width="600">
77
+
78
+ On the other hand, unassociating models from a big amount of associated models if faster with JSONB HABTM as the associations count grows:
79
+
80
+ <img src="https://github.com/lebedev-yury/activerecord-jsonb-associations/blob/master/doc/images/deleting-associations.png?raw=true | width=500" alt="JSONB HAMTB is faster on removing associations" width="600">
81
+
82
+ ## Installation
83
+
84
+ Add this line to your application's Gemfile:
85
+
86
+ ```ruby
87
+ gem 'activerecord-jsonb-associations'
88
+ ```
89
+
90
+ And then execute:
91
+
92
+ ```bash
93
+ $ bundle install
94
+ ```
95
+
96
+ ## Developing
97
+
98
+ To setup development environment, just run:
99
+
100
+ ```bash
101
+ $ bin/setup
102
+ ```
103
+
104
+ To run specs:
105
+
106
+ ```bash
107
+ $ bundle exec rspec
108
+ ```
109
+
110
+ To run benchmarks (that will take a while):
111
+
112
+ ```bash
113
+ $ bundle exec rake benchmarks:habtm
114
+ ```
115
+
116
+ ## License
117
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ Rake.add_rakelib 'benchmarks'
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'ActiveRecord::JSONB::Associations'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ require 'bundler/gem_tasks'
@@ -0,0 +1,30 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module Association #:nodoc:
5
+ def creation_attributes
6
+ return super unless reflection.options.key?(:foreign_store)
7
+
8
+ attributes = {}
9
+ jsonb_store = reflection.options[:foreign_store]
10
+ attributes[jsonb_store] ||= {}
11
+ attributes[jsonb_store][reflection.foreign_key] =
12
+ owner[reflection.active_record_primary_key]
13
+
14
+ attributes
15
+ end
16
+
17
+ # rubocop:disable Metrics/AbcSize
18
+ def create_scope
19
+ super.tap do |scope|
20
+ next unless options.key?(:foreign_store)
21
+ scope[options[:foreign_store].to_s] ||= {}
22
+ scope[options[:foreign_store].to_s][reflection.foreign_key] =
23
+ owner[reflection.active_record_primary_key]
24
+ end
25
+ end
26
+ # rubocop:enable Metrics/AbcSize
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,70 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module AssociationScope #:nodoc:
5
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
6
+ def last_chain_scope(scope, owner_reflection, owner)
7
+ reflection = owner_reflection.instance_variable_get(:@reflection)
8
+ return super unless reflection
9
+
10
+ table = owner_reflection.aliased_table
11
+ key = owner_reflection.join_primary_key
12
+ foreign_key = owner_reflection.join_foreign_key
13
+ value = transform_value(owner[foreign_key])
14
+
15
+ if reflection.options.key?(:foreign_store)
16
+ apply_jsonb_equality(
17
+ scope,
18
+ table,
19
+ reflection.options[:foreign_store],
20
+ key,
21
+ value
22
+ )
23
+ elsif reflection.options.key?(:store)
24
+ return super if reflection.belongs_to?
25
+ pluralized_key = key.pluralize
26
+
27
+ apply_jsonb_containment(
28
+ scope,
29
+ table,
30
+ reflection.options[:store],
31
+ pluralized_key,
32
+ value
33
+ )
34
+ else
35
+ super
36
+ end
37
+ end
38
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
39
+
40
+ def apply_jsonb_equality(scope, table, jsonb_column, key, value)
41
+ scope.where!(
42
+ Arel::Nodes::JSONBDashDoubleArrow.new(
43
+ table, table[jsonb_column], key
44
+ ).eq(
45
+ Arel::Nodes::BindParam.new(
46
+ Relation::QueryAttribute.new(
47
+ key.to_s, value, ActiveModel::Type::String.new
48
+ )
49
+ )
50
+ )
51
+ )
52
+ end
53
+
54
+ def apply_jsonb_containment(scope, table, jsonb_column, key, value)
55
+ scope.where!(
56
+ Arel::Nodes::JSONBHashArrow.new(
57
+ table, table[jsonb_column], key
58
+ ).contains(
59
+ Arel::Nodes::BindParam.new(
60
+ Relation::QueryAttribute.new(
61
+ key.to_s, value, ActiveModel::Type::String.new
62
+ )
63
+ )
64
+ )
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module BelongsToAssociation #:nodoc:
5
+ def replace_keys(record, force: false)
6
+ return super unless reflection.options.key?(:store)
7
+
8
+ owner[reflection.options[:store]][reflection.foreign_key] =
9
+ record._read_attribute(
10
+ reflection.association_primary_key(record.class)
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,72 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module Builder
5
+ module BelongsTo #:nodoc:
6
+ def valid_options(options)
7
+ super + [:store]
8
+ end
9
+
10
+ def define_accessors(mixin, reflection)
11
+ if reflection.options.key?(:store)
12
+ mixin.attribute reflection.foreign_key, :integer
13
+ add_association_accessor_methods(mixin, reflection)
14
+ end
15
+
16
+ super
17
+ end
18
+
19
+ def add_association_accessor_methods(mixin, reflection)
20
+ foreign_key = reflection.foreign_key.to_s
21
+
22
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
23
+ if method_defined?(foreign_key)
24
+ raise ActiveRecord::JSONB::Associations::
25
+ ConflictingAssociation,
26
+ "Association with foreign key :#{foreign_key} already "\
27
+ "exists on #{reflection.active_record.name}"
28
+ end
29
+
30
+ def #{foreign_key}=(value)
31
+ #{reflection.options[:store]}['#{foreign_key}'] = value
32
+ end
33
+
34
+ def #{foreign_key}
35
+ #{reflection.options[:store]}['#{foreign_key}']
36
+ end
37
+
38
+ def _read_attribute(attr_name)
39
+ key = attr_name.to_s
40
+ if key.ends_with?('_id') && #{reflection.options[:store]}.keys.include?(key)
41
+ #{reflection.options[:store]}[key]
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ def [](key)
48
+ key = key.to_s
49
+ if key.ends_with?('_id') &&
50
+ #{reflection.options[:store]}.keys.include?(key)
51
+ #{reflection.options[:store]}[key]
52
+ else
53
+ super
54
+ end
55
+ end
56
+
57
+ def []=(key, value)
58
+ key = key.to_s
59
+ if key.ends_with?('_id') &&
60
+ #{reflection.options[:store]}.keys.include?(key)
61
+ #{reflection.options[:store]}[key] = value
62
+ else
63
+ super
64
+ end
65
+ end
66
+ CODE
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,35 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module Builder
5
+ module HasMany #:nodoc:
6
+ def valid_options(options)
7
+ super + %i[store foreign_store]
8
+ end
9
+
10
+ def define_accessors(mixin, reflection)
11
+ if reflection.options.key?(:store)
12
+ add_association_accessor_methods(mixin, reflection)
13
+ end
14
+
15
+ super
16
+ end
17
+
18
+ def add_association_accessor_methods(mixin, reflection)
19
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
20
+ def [](key)
21
+ key = key.to_s
22
+ if key.ends_with?('_ids') &&
23
+ #{reflection.options[:store]}.keys.include?(key)
24
+ #{reflection.options[:store]}[key]
25
+ else
26
+ super
27
+ end
28
+ end
29
+ CODE
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module Builder
5
+ module HasOne #:nodoc:
6
+ def valid_options(options)
7
+ super + [:foreign_store]
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module ClassMethods #:nodoc:
5
+ # rubocop:disable Naming/PredicateName
6
+ def has_and_belongs_to_many(name, scope = nil, **options, &extension)
7
+ return super unless options.key?(:store)
8
+ has_many(name, scope, **options, &extension)
9
+ end
10
+ # rubocop:enable Naming/PredicateName
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module ForeignAssociation #:nodoc:
5
+ def set_owner_attributes(record)
6
+ return super unless options.key?(:foreign_store)
7
+
8
+ key = owner._read_attribute(reflection.foreign_store)
9
+ record._write_attribute(reflection.foreign_store, key)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,134 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module HasManyAssociation #:nodoc:
5
+ def ids_reader
6
+ return super unless reflection.options.key?(:store)
7
+
8
+ Array(
9
+ owner[reflection.options[:store]][
10
+ "#{reflection.name.to_s.singularize}_ids"
11
+ ]
12
+ )
13
+ end
14
+
15
+ # rubocop:disable Naming/AccessorMethodName
16
+ def set_owner_attributes(record)
17
+ return super unless reflection.options.key?(:store)
18
+
19
+ creation_attributes.each do |key, value|
20
+ if key == reflection.options[:store]
21
+ set_store_attributes(record, key, value)
22
+ else
23
+ record[key] = value
24
+ end
25
+ end
26
+ end
27
+ # rubocop:enable Naming/AccessorMethodName
28
+
29
+ def set_store_attributes(record, store_column, attributes)
30
+ attributes.each do |key, value|
31
+ if value.is_a?(Array)
32
+ record[store_column][key] ||= []
33
+ record[store_column][key] =
34
+ record[store_column][key].concat(value).uniq
35
+ else
36
+ record[store_column] = value
37
+ end
38
+ end
39
+ end
40
+
41
+ # rubocop:disable Metrics/AbcSize
42
+ def creation_attributes
43
+ return super unless reflection.options.key?(:store)
44
+
45
+ attributes = {}
46
+ jsonb_store = reflection.options[:store]
47
+ attributes[jsonb_store] ||= {}
48
+ attributes[jsonb_store][reflection.foreign_key.pluralize] = []
49
+ attributes[jsonb_store][reflection.foreign_key.pluralize] <<
50
+ owner[reflection.active_record_primary_key]
51
+
52
+ attributes
53
+ end
54
+ # rubocop:enable Metrics/AbcSize
55
+
56
+ # rubocop:disable Metrics/AbcSize
57
+ def create_scope
58
+ super.tap do |scope|
59
+ next unless options.key?(:store)
60
+
61
+ key = reflection.foreign_key.pluralize
62
+ scope[options[:store].to_s] ||= {}
63
+ scope[options[:store].to_s][key] ||= []
64
+ scope[options[:store].to_s][key] << owner[
65
+ reflection.active_record_primary_key
66
+ ]
67
+ end
68
+ end
69
+ # rubocop:enable Metrics/AbcSize
70
+
71
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
72
+ def insert_record(record, validate = true, raise = false)
73
+ super.tap do |super_result|
74
+ next unless options.key?(:store)
75
+ next unless super_result
76
+
77
+ key = "#{record.model_name.singular}_ids"
78
+ jsonb_column = options[:store]
79
+
80
+ owner.class.where(
81
+ owner.class.primary_key => owner[owner.class.primary_key]
82
+ ).update_all(%(
83
+ #{jsonb_column} = jsonb_set(#{jsonb_column}, '{#{key}}',
84
+ coalesce(#{jsonb_column}->'#{key}', '[]'::jsonb) ||
85
+ '[#{record[klass.primary_key]}]'::jsonb)
86
+ ))
87
+ end
88
+ end
89
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
90
+
91
+ def delete_records(records, method)
92
+ return super unless options.key?(:store)
93
+ super(records, :delete)
94
+ end
95
+
96
+ # rubocop:disable Metrics/AbcSize
97
+ def delete_count(method, scope)
98
+ store = reflection.options[:foreign_store] ||
99
+ reflection.options[:store]
100
+ return super if method == :delete_all || !store
101
+
102
+ if reflection.options.key?(:foreign_store)
103
+ remove_jsonb_foreign_id_on_belongs_to(store, reflection.foreign_key)
104
+ else
105
+ remove_jsonb_foreign_id_on_habtm(
106
+ store, reflection.foreign_key.pluralize, owner.id
107
+ )
108
+ end
109
+ end
110
+ # rubocop:enable Metrics/AbcSize
111
+
112
+ def remove_jsonb_foreign_id_on_belongs_to(store, foreign_key)
113
+ scope.update_all("#{store} = #{store} #- '{#{foreign_key}}'")
114
+ end
115
+
116
+ def remove_jsonb_foreign_id_on_habtm(store, foreign_key, owner_id)
117
+ # PostgreSQL can only delete jsonb array elements by text or index.
118
+ # Therefore we have to convert the jsonb array to PostgreSQl array,
119
+ # remove the element, and convert it back
120
+ scope.update_all(
121
+ %(
122
+ #{store} = jsonb_set(#{store}, '{#{foreign_key}}',
123
+ to_jsonb(
124
+ array_remove(
125
+ array(select * from jsonb_array_elements(
126
+ (#{store}->'#{foreign_key}'))),
127
+ '#{owner_id}')))
128
+ )
129
+ )
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,52 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module JoinDependency
5
+ module JoinAssociation #:nodoc:
6
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
7
+ def build_constraint(klass, table, key, foreign_table, foreign_key)
8
+ if reflection.options.key?(:foreign_store)
9
+ build_eq_constraint(
10
+ table, table[reflection.options[:foreign_store]],
11
+ key, foreign_table, foreign_key
12
+ )
13
+ elsif reflection.options.key?(:store) && reflection.belongs_to?
14
+ build_eq_constraint(
15
+ foreign_table, foreign_table[reflection.options[:store]],
16
+ foreign_key, table, key
17
+ )
18
+ elsif reflection.options.key?(:store) # && reflection.has_one?
19
+ build_contains_constraint(
20
+ table, table[reflection.options[:store]],
21
+ key.pluralize, foreign_table, foreign_key
22
+ )
23
+ else
24
+ super
25
+ end
26
+ end
27
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
28
+
29
+ def build_eq_constraint(
30
+ table, jsonb_column, key, foreign_table, foreign_key
31
+ )
32
+ Arel::Nodes::JSONBDashDoubleArrow.new(table, jsonb_column, key).eq(
33
+ ::Arel::Nodes::SqlLiteral.new(
34
+ "CAST(#{foreign_table.name}.#{foreign_key} AS text)"
35
+ )
36
+ )
37
+ end
38
+
39
+ def build_contains_constraint(
40
+ table, jsonb_column, key, foreign_table, foreign_key
41
+ )
42
+ Arel::Nodes::JSONBHashArrow.new(table, jsonb_column, key).contains(
43
+ ::Arel::Nodes::SqlLiteral.new(
44
+ "jsonb_build_array(#{foreign_table.name}.#{foreign_key})"
45
+ )
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module Preloader
5
+ module Association #:nodoc:
6
+ def records_for(ids)
7
+ return super unless reflection.options.key?(:foreign_store)
8
+
9
+ scope.where(
10
+ Arel::Nodes::JSONBHashArrow.new(
11
+ table,
12
+ table[reflection.options[:foreign_store]],
13
+ association_key_name
14
+ ).intersects_with(ids)
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,62 @@
1
+ module ActiveRecord
2
+ module JSONB
3
+ module Associations
4
+ module Preloader
5
+ module HasMany #:nodoc:
6
+ # rubocop:disable Metrics/AbcSize
7
+ def records_for(ids)
8
+ return super unless reflection.options.key?(:store)
9
+
10
+ scope.where(
11
+ Arel::Nodes::JSONBHashArrow.new(
12
+ table,
13
+ table[reflection.options[:store]],
14
+ association_key_name.pluralize
15
+ ).intersects_with(ids)
16
+ )
17
+ end
18
+ # rubocop:enable Metrics/AbcSize
19
+
20
+ def association_key_name
21
+ super_value = super
22
+ return super_value unless reflection.options.key?(:store)
23
+ super_value.pluralize
24
+ end
25
+
26
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
27
+ def associated_records_by_owner(preloader)
28
+ return super unless reflection.options.key?(:store)
29
+
30
+ records = load_records do |record|
31
+ record[association_key_name].each do |owner_key|
32
+ owner = owners_by_key[convert_key(owner_key)]
33
+ association = owner.association(reflection.name)
34
+ association.set_inverse_instance(record)
35
+ end
36
+ end
37
+
38
+ owners.each_with_object({}) do |owner, result|
39
+ result[owner] = records[convert_key(owner[owner_key_name])] || []
40
+ end
41
+ end
42
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
43
+
44
+ # rubocop:disable Metrics/AbcSize
45
+ def load_records(&block)
46
+ return super unless reflection.options.key?(:store)
47
+
48
+ return {} if owner_keys.empty?
49
+ @preloaded_records = records_for(owner_keys).load(&block)
50
+ @preloaded_records.each_with_object({}) do |record, result|
51
+ record[association_key_name].each do |owner_key|
52
+ result[convert_key(owner_key)] ||= []
53
+ result[convert_key(owner_key)] << record
54
+ end
55
+ end
56
+ end
57
+ # rubocop:enable Metrics/AbcSize
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module JSONB
5
+ module Associations
6
+ VERSION = '7.0.0'.freeze
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,80 @@
1
+ require 'active_record'
2
+
3
+ require 'arel/nodes/jsonb_operators'
4
+ require 'activerecord/jsonb/associations/class_methods'
5
+ require 'activerecord/jsonb/associations/builder/belongs_to'
6
+ require 'activerecord/jsonb/associations/builder/has_one'
7
+ require 'activerecord/jsonb/associations/builder/has_many'
8
+ require 'activerecord/jsonb/associations/belongs_to_association'
9
+ require 'activerecord/jsonb/associations/association'
10
+ require 'activerecord/jsonb/associations/has_many_association'
11
+ require 'activerecord/jsonb/associations/association_scope'
12
+ require 'activerecord/jsonb/associations/foreign_association'
13
+ require 'activerecord/jsonb/associations/preloader/association'
14
+ require 'activerecord/jsonb/associations/preloader/has_many'
15
+ require 'activerecord/jsonb/associations/join_dependency/join_association'
16
+ require 'activerecord/jsonb/connection_adapters/reference_definition'
17
+
18
+ module ActiveRecord #:nodoc:
19
+ module JSONB #:nodoc:
20
+ module Associations #:nodoc:
21
+ class ConflictingAssociation < StandardError; end
22
+ end
23
+ end
24
+ end
25
+
26
+ # rubocop:disable Metrics/BlockLength
27
+ ActiveSupport.on_load :active_record do
28
+ ::ActiveRecord::Base.extend(
29
+ ActiveRecord::JSONB::Associations::ClassMethods
30
+ )
31
+
32
+ ::ActiveRecord::Associations::Builder::BelongsTo.extend(
33
+ ActiveRecord::JSONB::Associations::Builder::BelongsTo
34
+ )
35
+
36
+ ::ActiveRecord::Associations::Builder::HasOne.extend(
37
+ ActiveRecord::JSONB::Associations::Builder::HasOne
38
+ )
39
+
40
+ ::ActiveRecord::Associations::Builder::HasMany.extend(
41
+ ActiveRecord::JSONB::Associations::Builder::HasMany
42
+ )
43
+
44
+ ::ActiveRecord::Associations::Association.prepend(
45
+ ActiveRecord::JSONB::Associations::Association
46
+ )
47
+
48
+ ::ActiveRecord::Associations::ForeignAssociation.prepend(
49
+ ActiveRecord::JSONB::Associations::ForeignAssociation
50
+ )
51
+
52
+ ::ActiveRecord::Associations::BelongsToAssociation.prepend(
53
+ ActiveRecord::JSONB::Associations::BelongsToAssociation
54
+ )
55
+
56
+ ::ActiveRecord::Associations::HasManyAssociation.prepend(
57
+ ActiveRecord::JSONB::Associations::HasManyAssociation
58
+ )
59
+
60
+ ::ActiveRecord::Associations::AssociationScope.prepend(
61
+ ActiveRecord::JSONB::Associations::AssociationScope
62
+ )
63
+
64
+ ::ActiveRecord::Associations::Preloader::Association.prepend(
65
+ ActiveRecord::JSONB::Associations::Preloader::Association
66
+ )
67
+
68
+ # ::ActiveRecord::Associations::Preloader::HasMany.prepend(
69
+ # ActiveRecord::JSONB::Associations::Preloader::HasMany
70
+ # )
71
+
72
+ ::ActiveRecord::Associations::JoinDependency::JoinAssociation.prepend(
73
+ ActiveRecord::JSONB::Associations::JoinDependency::JoinAssociation
74
+ )
75
+
76
+ ::ActiveRecord::ConnectionAdapters::ReferenceDefinition.prepend(
77
+ ActiveRecord::JSONB::ConnectionAdapters::ReferenceDefinition
78
+ )
79
+ end
80
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module JSONB
5
+ module ConnectionAdapters
6
+ module ReferenceDefinition #:nodoc:
7
+ # rubocop:disable Metrics/ParameterLists
8
+ def initialize(
9
+ name,
10
+ polymorphic: false,
11
+ index: true,
12
+ foreign_key: false,
13
+ type: :bigint,
14
+ store: false,
15
+ **options
16
+ )
17
+ @store = store
18
+
19
+ super(
20
+ name,
21
+ polymorphic: polymorphic,
22
+ index: index,
23
+ foreign_key: foreign_key,
24
+ type: type,
25
+ **options
26
+ )
27
+ end
28
+ # rubocop:enable Metrics/ParameterLists
29
+
30
+ def add_to(table)
31
+ return super unless store
32
+
33
+ table.column(store, :jsonb, null: false, default: {})
34
+
35
+ return unless index
36
+
37
+ column_names.each do |column_name|
38
+ table.index(
39
+ "(#{store}->>'#{column_name}')",
40
+ using: :hash,
41
+ name: "index_#{table.name}_on_#{store}_#{column_name}"
42
+ )
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ attr_reader :store
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,16 @@
1
+ module Arel
2
+ module Nodes
3
+ class JSONBAtArrow < JSONBOperator #:nodoc:
4
+ def operator
5
+ '@>'
6
+ end
7
+
8
+ def right_side
9
+ return name if name.is_a?(::Arel::Nodes::BindParam) ||
10
+ name.is_a?(::Arel::Nodes::SqlLiteral)
11
+
12
+ ::Arel::Nodes::SqlLiteral.new("'#{name.to_json}'")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ module Arel
2
+ module Nodes
3
+ class JSONBDashDoubleArrow < JSONBOperator #:nodoc:
4
+ def operator
5
+ '->>'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ module Arel
2
+ module Nodes
3
+ class JSONBDoublePipe < JSONBOperator #:nodoc:
4
+ def operator
5
+ '||'
6
+ end
7
+
8
+ def right_side
9
+ return name if name.is_a?(::Arel::Nodes::BindParam)
10
+ ::Arel::Nodes::SqlLiteral.new("'#{name.to_json}'")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ module Arel
2
+ module Nodes
3
+ class JSONBHashArrow < Arel::Nodes::JSONBOperator #:nodoc:
4
+ def operator
5
+ '#>'
6
+ end
7
+
8
+ def right_side
9
+ ::Arel::Nodes::SqlLiteral.new("'{#{name}}'")
10
+ end
11
+
12
+ def contains(value)
13
+ Arel::Nodes::JSONBAtArrow.new(relation, self, value)
14
+ end
15
+
16
+ def intersects_with(array)
17
+ ::Arel::Nodes::InfixOperation.new(
18
+ '>',
19
+ ::Arel::Nodes::NamedFunction.new(
20
+ 'jsonb_array_length',
21
+ [Arel::Nodes::JSONBDoublePipe.new(relation, self, array)]
22
+ ),
23
+ 0
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module Arel
2
+ module Nodes
3
+ class JSONBOperator < ::Arel::Nodes::InfixOperation #:nodoc:
4
+ attr_reader :relation
5
+ attr_reader :name
6
+
7
+ def initialize(relation, left_side, key)
8
+ @relation = relation
9
+ @name = key
10
+
11
+ super(operator, left_side, right_side)
12
+ end
13
+
14
+ def right_side
15
+ ::Arel::Nodes::SqlLiteral.new("'#{name}'")
16
+ end
17
+
18
+ def operator
19
+ raise NotImplementedError,
20
+ 'Subclasses must implement an #operator method'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ require 'arel/nodes/jsonb_operator'
2
+ require 'arel/nodes/jsonb_dash_double_arrow'
3
+ require 'arel/nodes/jsonb_at_arrow'
4
+ require 'arel/nodes/jsonb_double_pipe'
5
+ require 'arel/nodes/jsonb_hash_arrow'
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: carl-activerecord-jsonb-associations
3
+ version: !ruby/object:Gem::Version
4
+ version: 7.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Carl Allen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-09-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "<"
18
+ - !ruby/object:Gem::Version
19
+ version: '8.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "<"
25
+ - !ruby/object:Gem::Version
26
+ version: '8.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.7.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.7.0
41
+ description: Use PostgreSQL JSONB fields to store association information of your
42
+ models
43
+ email:
44
+ - github@allenofn.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - lib/activerecord/jsonb/associations.rb
53
+ - lib/activerecord/jsonb/associations/association.rb
54
+ - lib/activerecord/jsonb/associations/association_scope.rb
55
+ - lib/activerecord/jsonb/associations/belongs_to_association.rb
56
+ - lib/activerecord/jsonb/associations/builder/belongs_to.rb
57
+ - lib/activerecord/jsonb/associations/builder/has_many.rb
58
+ - lib/activerecord/jsonb/associations/builder/has_one.rb
59
+ - lib/activerecord/jsonb/associations/class_methods.rb
60
+ - lib/activerecord/jsonb/associations/foreign_association.rb
61
+ - lib/activerecord/jsonb/associations/has_many_association.rb
62
+ - lib/activerecord/jsonb/associations/join_dependency/join_association.rb
63
+ - lib/activerecord/jsonb/associations/preloader/association.rb
64
+ - lib/activerecord/jsonb/associations/preloader/has_many.rb
65
+ - lib/activerecord/jsonb/associations/version.rb
66
+ - lib/activerecord/jsonb/connection_adapters/reference_definition.rb
67
+ - lib/arel/nodes/jsonb_at_arrow.rb
68
+ - lib/arel/nodes/jsonb_dash_double_arrow.rb
69
+ - lib/arel/nodes/jsonb_double_pipe.rb
70
+ - lib/arel/nodes/jsonb_hash_arrow.rb
71
+ - lib/arel/nodes/jsonb_operator.rb
72
+ - lib/arel/nodes/jsonb_operators.rb
73
+ homepage: https://github.com/carlallen/activerecord-jsonb-associations
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '2.4'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.3.7
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Gem for storing association information using PostgreSQL JSONB columns
96
+ test_files: []