associate_jsonb 0.0.2 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +145 -31
- data/Rakefile +1 -3
- data/lib/associate_jsonb.rb +111 -2
- data/lib/associate_jsonb/arel_extensions/nodes/binary.rb +14 -0
- data/lib/associate_jsonb/arel_extensions/nodes/table_alias.rb +38 -0
- data/lib/associate_jsonb/arel_extensions/table.rb +40 -0
- data/lib/associate_jsonb/arel_extensions/visitors/postgresql.rb +113 -0
- data/lib/associate_jsonb/arel_extensions/visitors/visitor.rb +19 -0
- data/lib/associate_jsonb/arel_nodes/jsonb/attribute.rb +38 -0
- data/lib/associate_jsonb/arel_nodes/sql_casted_binary.rb +20 -0
- data/lib/associate_jsonb/arel_nodes/sql_casted_equality.rb +26 -12
- data/lib/associate_jsonb/associations/alias_tracker.rb +13 -0
- data/lib/associate_jsonb/associations/association_scope.rb +18 -45
- data/lib/associate_jsonb/associations/belongs_to_association.rb +8 -8
- data/lib/associate_jsonb/associations/builder/belongs_to.rb +5 -3
- data/lib/associate_jsonb/associations/join_dependency.rb +21 -0
- data/lib/associate_jsonb/attribute_methods.rb +19 -0
- data/lib/associate_jsonb/attribute_methods/read.rb +15 -0
- data/lib/associate_jsonb/connection_adapters/schema_creation.rb +167 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_foreign_key_function.rb +9 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_nested_set_function.rb +9 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/alter_table.rb +40 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/constraint_definition.rb +60 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb +102 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/table.rb +12 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/table_definition.rb +25 -0
- data/lib/associate_jsonb/connection_adapters/schema_statements.rb +116 -0
- data/lib/associate_jsonb/persistence.rb +14 -0
- data/lib/associate_jsonb/predicate_builder.rb +15 -0
- data/lib/associate_jsonb/reflection.rb +2 -2
- data/lib/associate_jsonb/relation/where_clause.rb +19 -0
- data/lib/associate_jsonb/version.rb +1 -1
- data/lib/associate_jsonb/with_store_attribute.rb +59 -23
- metadata +34 -10
- data/lib/associate_jsonb/arel_node_extensions/binary.rb +0 -12
- data/lib/associate_jsonb/connection_adapters/reference_definition.rb +0 -64
@@ -0,0 +1,113 @@
|
|
1
|
+
module AssociateJsonb
|
2
|
+
module ArelExtensions
|
3
|
+
module Visitors
|
4
|
+
module PostgreSQL
|
5
|
+
private
|
6
|
+
def collect_hash_changes(original, updated, nesting = nil)
|
7
|
+
keys = original.keys.map(&:to_s)
|
8
|
+
updated_keys = updated.keys.map(&:to_s)
|
9
|
+
keys |= updated_keys
|
10
|
+
original = original.with_indifferent_access
|
11
|
+
updated = updated.with_indifferent_access
|
12
|
+
added = []
|
13
|
+
deleted = []
|
14
|
+
finished = {}
|
15
|
+
keys.each do |k|
|
16
|
+
if updated[k].is_a?(Hash)
|
17
|
+
finished[k], a, d = collect_hash_changes(original[k].is_a?(Hash) ? original[k] : {}, updated[k], nesting ? "#{nesting},#{k}" : k)
|
18
|
+
a = [[(nesting ? "{#{nesting},#{k}}" : "{#{k}}"), {}]] if original[k].nil? && a.blank?
|
19
|
+
added |= a
|
20
|
+
deleted |= d
|
21
|
+
elsif updated[k].nil?
|
22
|
+
deleted << (nesting ? "{#{nesting},#{k}}" : "{#{k}}") if updated_keys.include?(k)
|
23
|
+
elsif original[k] != updated[k]
|
24
|
+
finished[k] = updated[k]
|
25
|
+
added << [(nesting ? "{#{nesting},#{k}}" : "{#{k}}"), updated[k]]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
[ finished, added, deleted ]
|
29
|
+
end
|
30
|
+
|
31
|
+
def is_hash?(type)
|
32
|
+
AssociateJsonb.is_hash? type
|
33
|
+
end
|
34
|
+
|
35
|
+
def is_update?(collector)
|
36
|
+
collector &&
|
37
|
+
Array(collector.value).any? {|v| v.is_a?(String) && (v =~ /UPDATE/) }
|
38
|
+
rescue
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_insert?(collector)
|
43
|
+
collector &&
|
44
|
+
Array(collector.value).any? {|v| v.is_a?(String) && (v =~ /INSERT INTO/) }
|
45
|
+
rescue
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def visit_BindHashChanges(t, collector)
|
50
|
+
changes, additions, deletions =
|
51
|
+
collect_hash_changes(
|
52
|
+
t.original_value.presence || {},
|
53
|
+
t.value.presence || {}
|
54
|
+
)
|
55
|
+
|
56
|
+
base_json = +"COALESCE(#{quote_column_name(t.name)}, '{}'::jsonb)"
|
57
|
+
json = base_json
|
58
|
+
|
59
|
+
deletions.each do |del|
|
60
|
+
json = +"(#{json} #- '#{del}')"
|
61
|
+
end
|
62
|
+
|
63
|
+
coalesced_paths = []
|
64
|
+
additions.sort.each do |add, value|
|
65
|
+
collector.add_bind(t.with_value_from_user(value)) do |i|
|
66
|
+
json = +"jsonb_nested_set(#{json},'#{add}', COALESCE($#{i}, '{}'::jsonb))"
|
67
|
+
''
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
collector << json
|
72
|
+
end
|
73
|
+
|
74
|
+
def visit_Arel_Nodes_BindParam(o, collector)
|
75
|
+
catch(:nodes_bound) do
|
76
|
+
if AssociateJsonb.jsonb_set_enabled
|
77
|
+
catch(:not_hashable) do
|
78
|
+
if is_hash?(o.value.type)
|
79
|
+
if is_update?(collector)
|
80
|
+
visit_BindHashChanges(o.value, collector)
|
81
|
+
|
82
|
+
throw :nodes_bound, collector
|
83
|
+
elsif is_insert?(collector)
|
84
|
+
value = o.value
|
85
|
+
|
86
|
+
value, _, _ =
|
87
|
+
collect_hash_changes(
|
88
|
+
{},
|
89
|
+
value.value.presence || {}
|
90
|
+
)
|
91
|
+
throw :nodes_bound, collector.add_bind(o.value.with_cast_value(value)) { |i| "$#{i}"}
|
92
|
+
else
|
93
|
+
throw :not_hashable
|
94
|
+
end
|
95
|
+
else
|
96
|
+
throw :not_hashable
|
97
|
+
end
|
98
|
+
end
|
99
|
+
elsif AssociateJsonb.jsonb_delete_nil && is_hash?(o.value.type)
|
100
|
+
value, _, _ =
|
101
|
+
collect_hash_changes(
|
102
|
+
{},
|
103
|
+
o.value.value.presence || {}
|
104
|
+
)
|
105
|
+
throw :nodes_bound, collector.add_bind(o.value.with_cast_value(value)) { |i| "$#{i}" }
|
106
|
+
end
|
107
|
+
throw :nodes_bound, collector.add_bind(o.value) { |i| "$#{i}" }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module ArelExtensions
|
6
|
+
module Visitors
|
7
|
+
module Visitor
|
8
|
+
def dispatch_cache
|
9
|
+
@dispatch_cache ||= Hash.new do |hash, klass|
|
10
|
+
hash[klass] =
|
11
|
+
"visit_#{(klass.name || '').
|
12
|
+
sub("AssociateJsonb::ArelNodes::SqlCasted", "Arel::Nodes::").
|
13
|
+
gsub('::', '_')}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module ArelNodes
|
6
|
+
module Jsonb
|
7
|
+
class Attribute
|
8
|
+
attr_reader :relation, :name, :delegated
|
9
|
+
|
10
|
+
def initialize(relation, name, delegated)
|
11
|
+
@relation = relation,
|
12
|
+
@name = name
|
13
|
+
@delegated = delegated
|
14
|
+
end
|
15
|
+
|
16
|
+
def lower
|
17
|
+
relation.lower self
|
18
|
+
end
|
19
|
+
|
20
|
+
def type_cast_for_database(value)
|
21
|
+
relation.type_cast_for_database(name, value)
|
22
|
+
end
|
23
|
+
|
24
|
+
def able_to_type_cast?
|
25
|
+
relation.able_to_type_cast?
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to_missing?(mthd, include_private = false)
|
29
|
+
delegated.respond_to?(mthd, include_private)
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(mthd, *args, **opts, &block)
|
33
|
+
delegated.public_send(mthd, *args, **opts, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module ArelNodes
|
6
|
+
class SqlCastedBinary < ::Arel::Nodes::Binary
|
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
|
@@ -1,20 +1,34 @@
|
|
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
|
+
|
4
26
|
module AssociateJsonb
|
5
27
|
module ArelNodes
|
6
|
-
class SqlCastedEquality < ::
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
super(
|
11
|
-
::Arel::Nodes::NamedFunction.new(
|
12
|
-
"CAST",
|
13
|
-
[ left.as(cast_as) ]
|
14
|
-
),
|
15
|
-
right
|
16
|
-
)
|
17
|
-
end
|
28
|
+
class SqlCastedEquality < AssociateJsonb::ArelNodes::SqlCastedBinary
|
29
|
+
def operator; :== end
|
30
|
+
alias :operand1 :left
|
31
|
+
alias :operand2 :right
|
18
32
|
end
|
19
33
|
end
|
20
34
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string/conversions"
|
4
|
+
|
5
|
+
module AssociateJsonb
|
6
|
+
module Associations
|
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)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -4,6 +4,22 @@
|
|
4
4
|
module AssociateJsonb
|
5
5
|
module Associations
|
6
6
|
module AssociationScope #:nodoc:
|
7
|
+
|
8
|
+
def get_chain(reflection, association, tracker)
|
9
|
+
name = reflection.name
|
10
|
+
chain = [ActiveRecord::Reflection::RuntimeReflection.new(reflection, association)]
|
11
|
+
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
|
+
)
|
18
|
+
chain << ActiveRecord::Associations::AssociationScope::ReflectionProxy.new(refl, aliased_table)
|
19
|
+
end
|
20
|
+
chain
|
21
|
+
end
|
22
|
+
|
7
23
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
8
24
|
def last_chain_scope(scope, owner_reflection, owner)
|
9
25
|
reflection = owner_reflection.instance_variable_get(:@reflection)
|
@@ -28,40 +44,21 @@ module AssociateJsonb
|
|
28
44
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
29
45
|
|
30
46
|
def apply_jsonb_equality(scope, table, jsonb_column, store_key, foreign_key, value, foreign_klass)
|
31
|
-
sql_type = type =
|
47
|
+
sql_type = type = nil
|
32
48
|
begin
|
33
49
|
type = foreign_klass.attribute_types[foreign_key.to_s]
|
34
50
|
raise "type not found" unless type.present?
|
35
51
|
sql_type = foreign_klass.columns_hash[foreign_key.to_s]
|
36
52
|
raise "not a column" unless sql_type.present?
|
37
53
|
sql_type = sql_type.sql_type
|
38
|
-
node_klass = Arel::Nodes::Jsonb::DashArrow
|
39
54
|
rescue
|
40
55
|
type = ActiveModel::Type::String.new
|
41
56
|
sql_type = "text"
|
42
|
-
node_klass = Arel::Nodes::Jsonb::DashDoubleArrow
|
43
57
|
end
|
44
58
|
|
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
59
|
scope.where!(
|
63
60
|
Arel::Nodes::SqlCastedEquality.new(
|
64
|
-
|
61
|
+
Arel::Nodes::Jsonb::DashDoubleArrow.new(table, table[jsonb_column], store_key),
|
65
62
|
sql_type,
|
66
63
|
Arel::Nodes::BindParam.new(
|
67
64
|
ActiveRecord::Relation::QueryAttribute.new(
|
@@ -70,30 +67,6 @@ module AssociateJsonb
|
|
70
67
|
)
|
71
68
|
)
|
72
69
|
)
|
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
70
|
end
|
98
71
|
end
|
99
72
|
end
|
@@ -4,14 +4,14 @@
|
|
4
4
|
module AssociateJsonb
|
5
5
|
module Associations
|
6
6
|
module BelongsToAssociation #:nodoc:
|
7
|
-
def replace_keys(record)
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
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
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -22,7 +22,7 @@ module AssociateJsonb
|
|
22
22
|
key = (reflection.jsonb_store_key || foreign_key).to_s
|
23
23
|
store = reflection.jsonb_store_attr
|
24
24
|
|
25
|
-
mixin.instance_eval
|
25
|
+
mixin.instance_eval <<~CODE, __FILE__, __LINE__ + 1
|
26
26
|
if attribute_names.include?(foreign_key)
|
27
27
|
raise AssociateJsonb::Associations::
|
28
28
|
ConflictingAssociation,
|
@@ -33,6 +33,7 @@ module AssociateJsonb
|
|
33
33
|
|
34
34
|
opts = {}
|
35
35
|
foreign_type = :integer
|
36
|
+
sql_type = "numeric"
|
36
37
|
begin
|
37
38
|
primary_key = reflection.active_record_primary_key.to_s
|
38
39
|
primary_column = reflection.klass.columns.find {|col| col.name == primary_key }
|
@@ -40,6 +41,7 @@ module AssociateJsonb
|
|
40
41
|
if primary_column
|
41
42
|
foreign_type = primary_column.type
|
42
43
|
sql_data = primary_column.sql_type_metadata.as_json
|
44
|
+
sql_type = sql_data["sql_type"]
|
43
45
|
%i[ limit precision scale ].each do |k|
|
44
46
|
opts[k] = sql_data[k.to_s] if sql_data[k.to_s]
|
45
47
|
end
|
@@ -49,8 +51,8 @@ module AssociateJsonb
|
|
49
51
|
foreign_type = :integer
|
50
52
|
end
|
51
53
|
|
52
|
-
mixin.instance_eval
|
53
|
-
store_column_attribute(:#{store}, :#{foreign_key},
|
54
|
+
mixin.instance_eval <<~CODE, __FILE__, __LINE__ + 1
|
55
|
+
store_column_attribute(:#{store}, :#{foreign_key}, foreign_type, sql_type: sql_type, key: "#{key}", **opts)
|
54
56
|
CODE
|
55
57
|
end
|
56
58
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string/conversions"
|
4
|
+
|
5
|
+
module AssociateJsonb
|
6
|
+
module Associations
|
7
|
+
module JoinDependency # :nodoc:
|
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
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module AssociateJsonb
|
5
|
+
module AttributeMethods
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
include Read
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def attributes_with_info(attribute_names)
|
14
|
+
attribute_names.each_with_object({}) do |name, attrs|
|
15
|
+
attrs[name] = _fetch_attribute(name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|