associate_jsonb 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +99 -0
- data/Rakefile +19 -0
- data/lib/associate_jsonb.rb +74 -0
- data/lib/associate_jsonb/arel_node_extensions/binary.rb +12 -0
- data/lib/associate_jsonb/arel_nodes/jsonb/at_arrow.rb +14 -0
- data/lib/associate_jsonb/arel_nodes/jsonb/bindable_operator.rb +17 -0
- data/lib/associate_jsonb/arel_nodes/jsonb/dash_arrow.rb +14 -0
- data/lib/associate_jsonb/arel_nodes/jsonb/dash_double_arrow.rb +14 -0
- data/lib/associate_jsonb/arel_nodes/jsonb/double_pipe.rb +14 -0
- data/lib/associate_jsonb/arel_nodes/jsonb/hash_arrow.rb +33 -0
- data/lib/associate_jsonb/arel_nodes/jsonb/operator.rb +26 -0
- data/lib/associate_jsonb/arel_nodes/sql_casted_equality.rb +20 -0
- data/lib/associate_jsonb/associations/association.rb +39 -0
- data/lib/associate_jsonb/associations/association_scope.rb +100 -0
- data/lib/associate_jsonb/associations/belongs_to_association.rb +17 -0
- data/lib/associate_jsonb/associations/builder/belongs_to.rb +59 -0
- data/lib/associate_jsonb/associations/builder/has_many.rb +14 -0
- data/lib/associate_jsonb/associations/builder/has_one.rb +14 -0
- data/lib/associate_jsonb/associations/conflicting_association.rb +8 -0
- data/lib/associate_jsonb/associations/has_many_association.rb +17 -0
- data/lib/associate_jsonb/associations/preloader/association.rb +24 -0
- data/lib/associate_jsonb/connection_adapters.rb +7 -0
- data/lib/associate_jsonb/connection_adapters/reference_definition.rb +64 -0
- data/lib/associate_jsonb/reflection.rb +103 -0
- data/lib/associate_jsonb/relation/where_clause.rb +20 -0
- data/lib/associate_jsonb/version.rb +6 -0
- data/lib/associate_jsonb/with_store_attribute.rb +156 -0
- metadata +172 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ad94cc18f8d5f87d25f2ebe9d0b75309fb43a3f22fc1438266e05ce39c2a3064
|
4
|
+
data.tar.gz: 016ce6443d9fdca1155d4b63fc18e6f6aaa18ef8887f73fe22312ee876d65f5d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7534ab4739036cff3691d4da6ac3d612cb6149c948088a3b7c24095e10bafce0d98e4bd96b7276c1ce84fd5e1017f039af48d59ed7d29f42827e90379a5e277d
|
7
|
+
data.tar.gz: 97e169f1942e48131f7de7be8dc99551c525fbdec7914a3de26abe3738614f1ae838bbd3edf2fb8828097c60ae32a47546638053f447510b4ad3792677dd639b
|
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,99 @@
|
|
1
|
+
# associate_jsonb
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/associate_jsonb.svg)](https://badge.fury.io/rb/associate_jsonb)
|
4
|
+
|
5
|
+
Basic ActiveRecord Associations using PostgreSQL JSONB columns, with built-in accessors and column indexes
|
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
|
+
### One-to-one and One-to-many associations
|
16
|
+
|
17
|
+
You can store all foreign keys of your model in one JSONB column, without having to create multiple columns:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
class Profile < ActiveRecord::Base
|
21
|
+
# Setting additional :store option on :belongs_to association
|
22
|
+
# enables saving of foreign ids in :extra JSONB column
|
23
|
+
belongs_to :user, store: :extra
|
24
|
+
end
|
25
|
+
|
26
|
+
class SocialProfile < ActiveRecord::Base
|
27
|
+
belongs_to :user, store: :extra
|
28
|
+
end
|
29
|
+
|
30
|
+
class User < ActiveRecord::Base
|
31
|
+
# Parent model association needs to specify :foreign_store
|
32
|
+
# for associations with JSONB storage
|
33
|
+
has_one :profile, foreign_store: :extra
|
34
|
+
has_many :social_profiles, foreign_store: :extra
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
Foreign keys for association on one model have to be unique, even if they use different store column.
|
39
|
+
|
40
|
+
You can also use `add_references` in your migration to add JSONB column and index for it (if `index: true` option is set):
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
add_reference :profiles, :users, store: :extra, index: true
|
44
|
+
```
|
45
|
+
|
46
|
+
### Many-to-many associations
|
47
|
+
|
48
|
+
Due to the ease of getting out-of-sync, and the complexity needed to build it, HABTM relation functionality has not been implemented through JSONB
|
49
|
+
|
50
|
+
#### Performance
|
51
|
+
|
52
|
+
Compared to regular associations, fetching models associated via JSONB column has no drops in performance.
|
53
|
+
|
54
|
+
Getting the count of connected records is ~35% faster with associations via JSONB (tested on associations with up to 10 000 connections).
|
55
|
+
|
56
|
+
Adding new connections is slightly faster with JSONB, for scopes up to 500 records connected to another record (total count of records in the table does not matter that much. If you have more then ~500 records connected to one record on average, and you want to add new records to the scope, JSONB associations will be slower then traditional:
|
57
|
+
|
58
|
+
<img src="https://github.com/lebedev-yury/associate_jsonb/blob/master/doc/images/adding-associations.png?raw=true | width=500" alt="JSONB HAMTB is slower on adding associations" width="600">
|
59
|
+
|
60
|
+
On the other hand, unassociating models from a big amount of associated models if faster with JSONB HABTM as the associations count grows:
|
61
|
+
|
62
|
+
<img src="https://github.com/lebedev-yury/associate_jsonb/blob/master/doc/images/deleting-associations.png?raw=true | width=500" alt="JSONB HAMTB is faster on removing associations" width="600">
|
63
|
+
|
64
|
+
## Installation
|
65
|
+
|
66
|
+
Add this line to your application's Gemfile:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
gem 'associate_jsonb'
|
70
|
+
```
|
71
|
+
|
72
|
+
And then execute:
|
73
|
+
|
74
|
+
```bash
|
75
|
+
$ bundle install
|
76
|
+
```
|
77
|
+
|
78
|
+
## Developing
|
79
|
+
|
80
|
+
To setup development environment, just run:
|
81
|
+
|
82
|
+
```bash
|
83
|
+
$ bin/setup
|
84
|
+
```
|
85
|
+
|
86
|
+
To run specs:
|
87
|
+
|
88
|
+
```bash
|
89
|
+
$ bundle exec rspec
|
90
|
+
```
|
91
|
+
|
92
|
+
To run benchmarks (that will take a while):
|
93
|
+
|
94
|
+
```bash
|
95
|
+
$ bundle exec rake benchmarks:habtm
|
96
|
+
``` -->
|
97
|
+
|
98
|
+
## License
|
99
|
+
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 = 'AssociateJsonb::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,74 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "active_record"
|
5
|
+
require "arel"
|
6
|
+
require "active_support/core_ext"
|
7
|
+
require "active_support/lazy_load_hooks"
|
8
|
+
require "active_support/concern"
|
9
|
+
require "pg"
|
10
|
+
require "active_record/connection_adapters/postgresql_adapter"
|
11
|
+
require "mutex_m"
|
12
|
+
|
13
|
+
require "zeitwerk"
|
14
|
+
loader = Zeitwerk::Loader.for_gem
|
15
|
+
loader.setup # ready!
|
16
|
+
|
17
|
+
module AssociateJsonb
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# rubocop:disable Metrics/BlockLength
|
22
|
+
ActiveSupport.on_load :active_record do
|
23
|
+
loader.eager_load
|
24
|
+
|
25
|
+
ActiveRecord::Base.include AssociateJsonb::WithStoreAttribute
|
26
|
+
ActiveRecord::Base.include AssociateJsonb::Associations
|
27
|
+
|
28
|
+
Arel::Nodes.include AssociateJsonb::ArelNodes
|
29
|
+
Arel::Nodes::Binary.include AssociateJsonb::ArelNodeExtensions::Binary
|
30
|
+
|
31
|
+
ActiveRecord::Associations::Builder::BelongsTo.extend(
|
32
|
+
AssociateJsonb::Associations::Builder::BelongsTo
|
33
|
+
)
|
34
|
+
|
35
|
+
ActiveRecord::Associations::Builder::HasOne.extend(
|
36
|
+
AssociateJsonb::Associations::Builder::HasOne
|
37
|
+
)
|
38
|
+
|
39
|
+
ActiveRecord::Associations::Builder::HasMany.extend(
|
40
|
+
AssociateJsonb::Associations::Builder::HasMany
|
41
|
+
)
|
42
|
+
|
43
|
+
ActiveRecord::Associations::Association.prepend(
|
44
|
+
AssociateJsonb::Associations::Association
|
45
|
+
)
|
46
|
+
|
47
|
+
ActiveRecord::Associations::BelongsToAssociation.prepend(
|
48
|
+
AssociateJsonb::Associations::BelongsToAssociation
|
49
|
+
)
|
50
|
+
|
51
|
+
ActiveRecord::Associations::HasManyAssociation.prepend(
|
52
|
+
AssociateJsonb::Associations::HasManyAssociation
|
53
|
+
)
|
54
|
+
|
55
|
+
ActiveRecord::Associations::AssociationScope.prepend(
|
56
|
+
AssociateJsonb::Associations::AssociationScope
|
57
|
+
)
|
58
|
+
|
59
|
+
ActiveRecord::Associations::Preloader::Association.prepend(
|
60
|
+
AssociateJsonb::Associations::Preloader::Association
|
61
|
+
)
|
62
|
+
|
63
|
+
# ActiveRecord::Associations::Preloader::HasMany.prepend(
|
64
|
+
# AssociateJsonb::Associations::Preloader::HasMany
|
65
|
+
# )
|
66
|
+
|
67
|
+
ActiveRecord::Reflection::AbstractReflection.prepend AssociateJsonb::Reflection
|
68
|
+
ActiveRecord::Relation::WhereClause.prepend AssociateJsonb::Relation::WhereClause
|
69
|
+
|
70
|
+
ActiveRecord::ConnectionAdapters::ReferenceDefinition.prepend(
|
71
|
+
AssociateJsonb::ConnectionAdapters::ReferenceDefinition
|
72
|
+
)
|
73
|
+
end
|
74
|
+
# rubocop:enable Metrics/BlockLength
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module ArelNodes
|
6
|
+
module Jsonb
|
7
|
+
class BindableOperator < AssociateJsonb::ArelNodes::Jsonb::Operator
|
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.as_json}'")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module ArelNodes
|
6
|
+
module Jsonb
|
7
|
+
class HashArrow < AssociateJsonb::ArelNodes::Jsonb::Operator #:nodoc:
|
8
|
+
def operator
|
9
|
+
'#>'
|
10
|
+
end
|
11
|
+
|
12
|
+
def right_side
|
13
|
+
::Arel::Nodes::SqlLiteral.new("'{#{name}}'")
|
14
|
+
end
|
15
|
+
|
16
|
+
def contains(value)
|
17
|
+
ArelNodes::Jsonb::AtArrow.new(relation, self, value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def intersects_with(array)
|
21
|
+
::Arel::Nodes::InfixOperation.new(
|
22
|
+
'>',
|
23
|
+
::Arel::Nodes::NamedFunction.new(
|
24
|
+
'jsonb_array_length',
|
25
|
+
[ ArelNodes::Jsonb::DoublePipe.new(relation, self, array) ]
|
26
|
+
),
|
27
|
+
0
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module AssociateJsonb
|
2
|
+
module ArelNodes
|
3
|
+
module Jsonb
|
4
|
+
class Operator < ::Arel::Nodes::InfixOperation #:nodoc:
|
5
|
+
attr_reader :relation
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
def initialize(relation, left_side, key)
|
9
|
+
@relation = relation
|
10
|
+
@name = key
|
11
|
+
|
12
|
+
super(operator, left_side, right_side)
|
13
|
+
end
|
14
|
+
|
15
|
+
def right_side
|
16
|
+
::Arel::Nodes::SqlLiteral.new("'#{name}'")
|
17
|
+
end
|
18
|
+
|
19
|
+
def operator
|
20
|
+
raise NotImplementedError,
|
21
|
+
'Subclasses must implement an #operator method'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
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
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module Associations
|
6
|
+
module Association #:nodoc:
|
7
|
+
def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc:
|
8
|
+
super unless reflection.foreign_store? && reflection.foreign_store_key?(reflection.foreign_key)
|
9
|
+
|
10
|
+
except_from_scope_attributes ||= {}
|
11
|
+
skip_assign = [reflection.foreign_key, reflection.type, reflection.foreign_store_key].compact
|
12
|
+
assigned_keys = record.changed_attribute_names_to_save
|
13
|
+
assigned_keys += except_from_scope_attributes.keys.map(&:to_s)
|
14
|
+
attributes = scope_for_create.except!(*(assigned_keys - skip_assign))
|
15
|
+
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
|
18
|
+
end
|
19
|
+
record.send(:_assign_attributes, attributes) if attributes.any?
|
20
|
+
set_inverse_instance(record)
|
21
|
+
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
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module Associations
|
6
|
+
module AssociationScope #:nodoc:
|
7
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
8
|
+
def last_chain_scope(scope, owner_reflection, owner)
|
9
|
+
reflection = owner_reflection.instance_variable_get(:@reflection)
|
10
|
+
return super unless reflection&.foreign_store?
|
11
|
+
|
12
|
+
|
13
|
+
join_keys = owner_reflection.join_keys
|
14
|
+
table = owner_reflection.aliased_table
|
15
|
+
key = reflection.foreign_store_key || join_keys.key
|
16
|
+
value = transform_value(owner[join_keys.foreign_key])
|
17
|
+
|
18
|
+
apply_jsonb_equality(
|
19
|
+
scope,
|
20
|
+
table,
|
21
|
+
reflection.foreign_store_attr,
|
22
|
+
key.to_s,
|
23
|
+
join_keys.foreign_key,
|
24
|
+
value,
|
25
|
+
reflection.active_record
|
26
|
+
)
|
27
|
+
end
|
28
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
29
|
+
|
30
|
+
def apply_jsonb_equality(scope, table, jsonb_column, store_key, foreign_key, value, foreign_klass)
|
31
|
+
sql_type = type = node_klass = nil
|
32
|
+
begin
|
33
|
+
type = foreign_klass.attribute_types[foreign_key.to_s]
|
34
|
+
raise "type not found" unless type.present?
|
35
|
+
sql_type = foreign_klass.columns_hash[foreign_key.to_s]
|
36
|
+
raise "not a column" unless sql_type.present?
|
37
|
+
sql_type = sql_type.sql_type
|
38
|
+
node_klass = Arel::Nodes::Jsonb::DashArrow
|
39
|
+
rescue
|
40
|
+
type = ActiveModel::Type::String.new
|
41
|
+
sql_type = "text"
|
42
|
+
node_klass = Arel::Nodes::Jsonb::DashDoubleArrow
|
43
|
+
end
|
44
|
+
|
45
|
+
# scope.where!(
|
46
|
+
# Arel::Nodes::HashableNamedFunction.new(
|
47
|
+
# "CAST",
|
48
|
+
# [
|
49
|
+
# node_klass.
|
50
|
+
# new(table, table[jsonb_column], store_key).
|
51
|
+
# as(sql_type)
|
52
|
+
# ]
|
53
|
+
# ).eq(
|
54
|
+
# Arel::Nodes::BindParam.new(
|
55
|
+
# ActiveRecord::Relation::QueryAttribute.new(
|
56
|
+
# store_key, value, type
|
57
|
+
# )
|
58
|
+
# )
|
59
|
+
# )
|
60
|
+
# )
|
61
|
+
|
62
|
+
scope.where!(
|
63
|
+
Arel::Nodes::SqlCastedEquality.new(
|
64
|
+
node_klass.new(table, table[jsonb_column], store_key),
|
65
|
+
sql_type,
|
66
|
+
Arel::Nodes::BindParam.new(
|
67
|
+
ActiveRecord::Relation::QueryAttribute.new(
|
68
|
+
store_key, value, type
|
69
|
+
)
|
70
|
+
)
|
71
|
+
)
|
72
|
+
)
|
73
|
+
|
74
|
+
# scope.where!(
|
75
|
+
# Arel::Nodes::Jsonb::DashDoubleArrow.
|
76
|
+
# new(table, table[jsonb_column], store_key).
|
77
|
+
# eq(
|
78
|
+
# Arel::Nodes::BindParam.new(
|
79
|
+
# ActiveRecord::Relation::QueryAttribute.new(
|
80
|
+
# store_key, value, ActiveModel::Type::String.new
|
81
|
+
# )
|
82
|
+
# )
|
83
|
+
# )
|
84
|
+
# )
|
85
|
+
|
86
|
+
# scope.where!(
|
87
|
+
# node_klass.new(
|
88
|
+
# table, table[jsonb_column], store_key
|
89
|
+
# ).eq(
|
90
|
+
# Arel::Nodes::BindParam.new(
|
91
|
+
# ActiveRecord::Relation::QueryAttribute.new(
|
92
|
+
# store_key, value, type
|
93
|
+
# )
|
94
|
+
# )
|
95
|
+
# )
|
96
|
+
# )
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module Associations
|
6
|
+
module BelongsToAssociation #:nodoc:
|
7
|
+
def replace_keys(record)
|
8
|
+
return super unless reflection.options.key?(:store)
|
9
|
+
|
10
|
+
owner[reflection.foreign_key] =
|
11
|
+
record._read_attribute(
|
12
|
+
reflection.association_primary_key(record.class)
|
13
|
+
)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module Associations
|
6
|
+
module Builder
|
7
|
+
module BelongsTo #:nodoc:
|
8
|
+
def valid_options(options)
|
9
|
+
super + %i[ store store_key ]
|
10
|
+
end
|
11
|
+
|
12
|
+
def define_accessors(mixin, reflection)
|
13
|
+
if reflection.options.key?(:store)
|
14
|
+
add_association_accessor_methods(mixin, reflection)
|
15
|
+
end
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_association_accessor_methods(mixin, reflection)
|
21
|
+
foreign_key = reflection.foreign_key.to_s
|
22
|
+
key = (reflection.jsonb_store_key || foreign_key).to_s
|
23
|
+
store = reflection.jsonb_store_attr
|
24
|
+
|
25
|
+
mixin.instance_eval <<-CODE, __FILE__, __LINE__ + 1
|
26
|
+
if attribute_names.include?(foreign_key)
|
27
|
+
raise AssociateJsonb::Associations::
|
28
|
+
ConflictingAssociation,
|
29
|
+
"Association with foreign key :#{foreign_key} already "\
|
30
|
+
"exists on #{reflection.active_record.name}"
|
31
|
+
end
|
32
|
+
CODE
|
33
|
+
|
34
|
+
opts = {}
|
35
|
+
foreign_type = :integer
|
36
|
+
begin
|
37
|
+
primary_key = reflection.active_record_primary_key.to_s
|
38
|
+
primary_column = reflection.klass.columns.find {|col| col.name == primary_key }
|
39
|
+
|
40
|
+
if primary_column
|
41
|
+
foreign_type = primary_column.type
|
42
|
+
sql_data = primary_column.sql_type_metadata.as_json
|
43
|
+
%i[ limit precision scale ].each do |k|
|
44
|
+
opts[k] = sql_data[k.to_s] if sql_data[k.to_s]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
rescue
|
48
|
+
opts = { limit: 8 }
|
49
|
+
foreign_type = :integer
|
50
|
+
end
|
51
|
+
|
52
|
+
mixin.instance_eval <<-CODE, __FILE__, __LINE__ + 1
|
53
|
+
store_column_attribute(:#{store}, :#{foreign_key}, :#{foreign_type}, key: "#{key}", **opts)
|
54
|
+
CODE
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module Associations
|
6
|
+
module HasManyAssociation #:nodoc:
|
7
|
+
# rubocop:disable Metrics/AbcSize
|
8
|
+
def delete_count(method, scope)
|
9
|
+
return super if method == :delete_all
|
10
|
+
return super unless store = reflection.foreign_store_attr
|
11
|
+
|
12
|
+
scope.update_all("#{store} = #{store} #- '{#{reflection.foreign_store_key}}'")
|
13
|
+
end
|
14
|
+
# rubocop:enable Metrics/AbcSize
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module Associations
|
6
|
+
module Preloader
|
7
|
+
module Association #:nodoc:
|
8
|
+
def records_for(ids)
|
9
|
+
return super if reflection.belongs_to?
|
10
|
+
return super unless reflection.foreign_store?
|
11
|
+
|
12
|
+
table = reflection.klass.arel_table
|
13
|
+
scope.where(
|
14
|
+
Arel::Nodes::Jsonb::HashArrow.new(
|
15
|
+
table,
|
16
|
+
table[reflection.foreign_store_attr],
|
17
|
+
reflection.foreign_store_key
|
18
|
+
).intersects_with(ids)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AssociateJsonb
|
4
|
+
module ConnectionAdapters
|
5
|
+
module ReferenceDefinition #:nodoc:
|
6
|
+
# rubocop:disable Metrics/ParameterLists
|
7
|
+
def initialize(
|
8
|
+
name,
|
9
|
+
store: false,
|
10
|
+
**options
|
11
|
+
)
|
12
|
+
@store = store && store.to_sym
|
13
|
+
|
14
|
+
super(name, **options)
|
15
|
+
end
|
16
|
+
# rubocop:enable Metrics/ParameterLists
|
17
|
+
|
18
|
+
def add_to(table)
|
19
|
+
return super unless store
|
20
|
+
|
21
|
+
should_add_col = false
|
22
|
+
if table.respond_to? :column_exists?
|
23
|
+
should_add_col = !table.column_exists?(store)
|
24
|
+
elsif table.respond_to? :columns
|
25
|
+
should_add_col = table.columns.none? {|col| col.name.to_sym == store}
|
26
|
+
end
|
27
|
+
|
28
|
+
table.column(store, :jsonb, null: false, default: {}) if should_add_col
|
29
|
+
|
30
|
+
return unless index
|
31
|
+
|
32
|
+
# should_add_idx = false
|
33
|
+
# if table.respond_to? :index_exists?
|
34
|
+
# should_add_idx = !table.index_exists?([ store ], using: :gin)
|
35
|
+
# elsif table.respond_to? :indexes
|
36
|
+
# should_add_idx = table.indexes.none? do |idx, opts|
|
37
|
+
# (idx == [ store ]) \
|
38
|
+
# && (opts == { using: :gin })
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# table.index([ store ], using: :gin) if should_add_idx
|
43
|
+
|
44
|
+
column_names.each do |column_name|
|
45
|
+
table.index(
|
46
|
+
"CAST (\"#{store}\"->'#{column_name}' AS #{@type || :bigint})",
|
47
|
+
using: :btree,
|
48
|
+
name: "index_#{table.name}_on_#{store}_#{column_name}"
|
49
|
+
)
|
50
|
+
|
51
|
+
table.index(
|
52
|
+
"(#{store}->>'#{column_name}')",
|
53
|
+
using: :btree,
|
54
|
+
name: "index_#{table.name}_on_#{store}_#{column_name}_text"
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
attr_reader :store
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module Reflection
|
6
|
+
def jsonb_store?
|
7
|
+
options.key?(:store) && jsonb_store_attr.present?
|
8
|
+
end
|
9
|
+
|
10
|
+
def jsonb_store_key?(fk = nil)
|
11
|
+
options.key?(:store_key) \
|
12
|
+
&& options[:store_key].present? \
|
13
|
+
&& (!fk || (jsonb_store_key.to_s != fk.to_s))
|
14
|
+
end
|
15
|
+
|
16
|
+
def jsonb_store_attr
|
17
|
+
options[:store]
|
18
|
+
end
|
19
|
+
|
20
|
+
def jsonb_store_key
|
21
|
+
options[:store_key].presence || join_keys.foreign_key
|
22
|
+
end
|
23
|
+
|
24
|
+
def foreign_store?
|
25
|
+
options.key?(:foreign_store) && options[:foreign_store].present?
|
26
|
+
end
|
27
|
+
|
28
|
+
def foreign_store_key?(fk = nil)
|
29
|
+
options.key?(:foreign_store_key) \
|
30
|
+
&& options[:foreign_store_key].present? \
|
31
|
+
&& (!fk || (foreign_store_key.to_s != fk.to_s))
|
32
|
+
end
|
33
|
+
|
34
|
+
def foreign_store_attr
|
35
|
+
options[:foreign_store]
|
36
|
+
end
|
37
|
+
|
38
|
+
def foreign_store_key
|
39
|
+
options[:foreign_store_key].presence || join_keys.key
|
40
|
+
end
|
41
|
+
|
42
|
+
def join_scope(table, foreign_table, foreign_klass)
|
43
|
+
return super unless jsonb_store? || foreign_store?
|
44
|
+
|
45
|
+
predicate_builder = predicate_builder(table)
|
46
|
+
scope_chain_items = join_scopes(table, predicate_builder)
|
47
|
+
klass_scope = klass_join_scope(table, predicate_builder)
|
48
|
+
|
49
|
+
if type
|
50
|
+
klass_scope.where!(type => foreign_klass.polymorphic_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
scope_chain_items.inject(klass_scope, &:merge!)
|
54
|
+
|
55
|
+
key = join_keys.key
|
56
|
+
foreign_key = join_keys.foreign_key
|
57
|
+
|
58
|
+
if foreign_store?
|
59
|
+
klass_scope.where!(
|
60
|
+
Arel::Nodes::NamedFunction.new(
|
61
|
+
"CAST",
|
62
|
+
[
|
63
|
+
Arel::Nodes::Jsonb::DashArrow.
|
64
|
+
new(table, table[foreign_store_attr], foreign_store_key || key).
|
65
|
+
as(foreign_klass.columns_hash[foreign_key.to_s].sql_type)
|
66
|
+
]
|
67
|
+
).eq(
|
68
|
+
::Arel::Nodes::SqlLiteral.
|
69
|
+
new("#{foreign_table.name}.#{foreign_key}")
|
70
|
+
)
|
71
|
+
)
|
72
|
+
|
73
|
+
# klass_scope.where!(
|
74
|
+
# Arel::Nodes::Jsonb::DashDoubleArrow.
|
75
|
+
# new(table, table[foreign_store_attr], foreign_store_key || key).
|
76
|
+
# eq(
|
77
|
+
# ::Arel::Nodes::SqlLiteral.
|
78
|
+
# new("#{foreign_table.name}.#{foreign_key}::text")
|
79
|
+
# )
|
80
|
+
# )
|
81
|
+
elsif jsonb_store?
|
82
|
+
klass_scope.where!(
|
83
|
+
Arel::Nodes::NamedFunction.new(
|
84
|
+
"CAST",
|
85
|
+
[
|
86
|
+
Arel::Nodes::Jsonb::DashArrow.
|
87
|
+
new(foreign_table, foreign_table[jsonb_store_attr], jsonb_store_key || foreign_key).
|
88
|
+
as(klass.columns_hash[key.to_s].sql_type)
|
89
|
+
]
|
90
|
+
).eq(
|
91
|
+
::Arel::Nodes::SqlLiteral.new("#{table.name}.#{key}")
|
92
|
+
)
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
if klass.finder_needs_type_condition?
|
97
|
+
klass_scope.where!(klass.send(:type_condition, table))
|
98
|
+
end
|
99
|
+
|
100
|
+
klass_scope
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AssociateJsonb
|
2
|
+
module Relation
|
3
|
+
module WhereClause
|
4
|
+
def to_h(table_name = nil)
|
5
|
+
equalities = equalities(predicates)
|
6
|
+
if table_name
|
7
|
+
equalities = equalities.select do |node|
|
8
|
+
node.original_left.relation.name == table_name
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
equalities.map { |node|
|
13
|
+
name = node.original_left.name.to_s
|
14
|
+
value = extract_node_value(node.right)
|
15
|
+
[name, value]
|
16
|
+
}.to_h
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module WithStoreAttribute
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class StoreColumnAttributeTracker < Module #:nodoc:
|
9
|
+
include Mutex_m
|
10
|
+
|
11
|
+
def names_list
|
12
|
+
@names_list ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_name(name, store, key)
|
16
|
+
@names_list ||= {}
|
17
|
+
@names_list[name.to_s.freeze] = { store: store, key: key }
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_name?(name)
|
21
|
+
names_list.key? name.to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
included do
|
26
|
+
instance_eval <<-CODE, __FILE__, __LINE__ + 1
|
27
|
+
initialize_store_column_attribute_tracker
|
28
|
+
|
29
|
+
after_initialize &set_store_column_attribute_values_on_init
|
30
|
+
after_commit &set_store_column_attribute_values_on_init
|
31
|
+
CODE
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
def inherited(child)
|
36
|
+
child.initialize_store_column_attribute_tracker
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize_store_column_attribute_tracker
|
41
|
+
@store_column_attribute_tracker = const_set(:StoreColumnAttributeTracker, StoreColumnAttributeTracker.new)
|
42
|
+
private_constant :StoreColumnAttributeTracker
|
43
|
+
|
44
|
+
store_column_attribute_tracker
|
45
|
+
end
|
46
|
+
|
47
|
+
def store_column_attribute_tracker
|
48
|
+
@store_column_attribute_tracker ||= initialize_store_column_attribute_tracker
|
49
|
+
end
|
50
|
+
|
51
|
+
def store_column_attribute_names
|
52
|
+
store_column_attribute_tracker.synchronize do
|
53
|
+
current_store_col_names = {}
|
54
|
+
current_store_col_names.merge!(super) if defined? super
|
55
|
+
current_store_col_names.merge!(store_column_attribute_tracker.names_list)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_store_column_attribute_name(name, store, key)
|
60
|
+
store_column_attribute_tracker.synchronize do
|
61
|
+
store_column_attribute_tracker.add_name(name, store, key)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def is_store_column_attribute?(name)
|
66
|
+
store_column_attribute_tracker.synchronize do
|
67
|
+
store_column_attribute_tracker.has_name?(name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_store_column_attribute_values_on_init
|
72
|
+
lambda do
|
73
|
+
self.class.store_column_attribute_names.each do |attr, opts|
|
74
|
+
_write_attribute(attr, _read_attribute(opts[:store])[opts[:key]])
|
75
|
+
clear_attribute_change(attr) if persisted?
|
76
|
+
end
|
77
|
+
rescue
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def data_column_attribute(*args, **opts)
|
83
|
+
store_column_attribute :data, *args, **opts
|
84
|
+
end
|
85
|
+
|
86
|
+
def store_column_attribute(store, attr, *opts, key: nil, **attribute_opts)
|
87
|
+
store = store.to_sym
|
88
|
+
attr = attr.to_sym
|
89
|
+
key ||= attr
|
90
|
+
key = key.to_s
|
91
|
+
array = attribute_opts[:array]
|
92
|
+
attribute attr, *opts, **attribute_opts
|
93
|
+
|
94
|
+
instance_eval <<-CODE, __FILE__, __LINE__ + 1
|
95
|
+
add_store_column_attribute_name("#{attr}", :#{store}, "#{key}")
|
96
|
+
CODE
|
97
|
+
|
98
|
+
include WithStoreAttribute::InstanceMethodsOnActivation.new(self, store, attr, key, array)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class InstanceMethodsOnActivation < Module
|
103
|
+
def initialize(mixin, store, attribute, key, is_array)
|
104
|
+
is_array = !!(is_array && attribute.to_s =~ /_ids$/)
|
105
|
+
on_attr_change =
|
106
|
+
is_array \
|
107
|
+
? "write_attribute(:#{attribute}, Array(given))" \
|
108
|
+
: "super(given)"
|
109
|
+
if is_array
|
110
|
+
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
111
|
+
def #{attribute}
|
112
|
+
_read_attribute(:#{attribute}) || []
|
113
|
+
end
|
114
|
+
CODE
|
115
|
+
end
|
116
|
+
|
117
|
+
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
118
|
+
def #{store}=(given)
|
119
|
+
given = super((given || {}).with_indifferent_access)
|
120
|
+
write_attribute(:#{attribute}, given["#{key}"])
|
121
|
+
given["#{key}"] = #{attribute} unless #{attribute}.nil?
|
122
|
+
super(given)
|
123
|
+
end
|
124
|
+
|
125
|
+
def #{attribute}=(given)
|
126
|
+
#{on_attr_change}
|
127
|
+
value = #{store}["#{key}"] = #{attribute}
|
128
|
+
#{store}.delete("#{key}") if value.nil?
|
129
|
+
_write_attribute(:#{store}, #{store})
|
130
|
+
value
|
131
|
+
end
|
132
|
+
CODE
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def is_store_column_attribute?(name)
|
137
|
+
self.class.is_store_column_attribute?(name)
|
138
|
+
end
|
139
|
+
|
140
|
+
def [](k)
|
141
|
+
if is_store_column_attribute?(k)
|
142
|
+
self.public_send(k)
|
143
|
+
else
|
144
|
+
super
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def []=(k, v)
|
149
|
+
if is_store_column_attribute?(k)
|
150
|
+
self.public_send(:"#{k}=", v)
|
151
|
+
else
|
152
|
+
super
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
metadata
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: associate_jsonb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sampson Crowley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 6.0.3.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '6'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 6.0.3.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: pg
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.1'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 1.1.2
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '1.1'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 1.1.2
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: zeitwerk
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '2'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 2.2.2
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 2.2.2
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: mutex_m
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - "~>"
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0.1'
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.1.0
|
83
|
+
type: :runtime
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.1'
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 0.1.0
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: rspec
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 3.7.0
|
100
|
+
type: :development
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - "~>"
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 3.7.0
|
107
|
+
description: |2
|
108
|
+
This gem extends ActiveRecord to let you use PostgreSQL JSONB data for associations
|
109
|
+
|
110
|
+
Inspired by activerecord-jsonb-associations, but for use in Rails 6+ and
|
111
|
+
ruby 2.7+ and with some unnecessary options and features (HABTM) removed
|
112
|
+
|
113
|
+
BONUS: extended `table#references` for easy migrations and indexes
|
114
|
+
(NOTE: real foreign key constraints are not possible with PostgreSQL JSONB)
|
115
|
+
email:
|
116
|
+
- sampsonsprojects@gmail.com
|
117
|
+
executables: []
|
118
|
+
extensions: []
|
119
|
+
extra_rdoc_files: []
|
120
|
+
files:
|
121
|
+
- MIT-LICENSE
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- lib/associate_jsonb.rb
|
125
|
+
- lib/associate_jsonb/arel_node_extensions/binary.rb
|
126
|
+
- lib/associate_jsonb/arel_nodes/jsonb/at_arrow.rb
|
127
|
+
- lib/associate_jsonb/arel_nodes/jsonb/bindable_operator.rb
|
128
|
+
- lib/associate_jsonb/arel_nodes/jsonb/dash_arrow.rb
|
129
|
+
- lib/associate_jsonb/arel_nodes/jsonb/dash_double_arrow.rb
|
130
|
+
- lib/associate_jsonb/arel_nodes/jsonb/double_pipe.rb
|
131
|
+
- lib/associate_jsonb/arel_nodes/jsonb/hash_arrow.rb
|
132
|
+
- lib/associate_jsonb/arel_nodes/jsonb/operator.rb
|
133
|
+
- lib/associate_jsonb/arel_nodes/sql_casted_equality.rb
|
134
|
+
- lib/associate_jsonb/associations/association.rb
|
135
|
+
- lib/associate_jsonb/associations/association_scope.rb
|
136
|
+
- lib/associate_jsonb/associations/belongs_to_association.rb
|
137
|
+
- lib/associate_jsonb/associations/builder/belongs_to.rb
|
138
|
+
- lib/associate_jsonb/associations/builder/has_many.rb
|
139
|
+
- lib/associate_jsonb/associations/builder/has_one.rb
|
140
|
+
- lib/associate_jsonb/associations/conflicting_association.rb
|
141
|
+
- lib/associate_jsonb/associations/has_many_association.rb
|
142
|
+
- lib/associate_jsonb/associations/preloader/association.rb
|
143
|
+
- lib/associate_jsonb/connection_adapters.rb
|
144
|
+
- lib/associate_jsonb/connection_adapters/reference_definition.rb
|
145
|
+
- lib/associate_jsonb/reflection.rb
|
146
|
+
- lib/associate_jsonb/relation/where_clause.rb
|
147
|
+
- lib/associate_jsonb/version.rb
|
148
|
+
- lib/associate_jsonb/with_store_attribute.rb
|
149
|
+
homepage: https://github.com/SampsonCrowley/associate_jsonb
|
150
|
+
licenses:
|
151
|
+
- MIT
|
152
|
+
metadata: {}
|
153
|
+
post_install_message:
|
154
|
+
rdoc_options: []
|
155
|
+
require_paths:
|
156
|
+
- lib
|
157
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '2.7'
|
162
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
requirements: []
|
168
|
+
rubygems_version: 3.1.3
|
169
|
+
signing_key:
|
170
|
+
specification_version: 4
|
171
|
+
summary: Store database references in PostgreSQL Jsonb columns
|
172
|
+
test_files: []
|