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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -31
  3. data/Rakefile +1 -3
  4. data/lib/associate_jsonb.rb +111 -2
  5. data/lib/associate_jsonb/arel_extensions/nodes/binary.rb +14 -0
  6. data/lib/associate_jsonb/arel_extensions/nodes/table_alias.rb +38 -0
  7. data/lib/associate_jsonb/arel_extensions/table.rb +40 -0
  8. data/lib/associate_jsonb/arel_extensions/visitors/postgresql.rb +113 -0
  9. data/lib/associate_jsonb/arel_extensions/visitors/visitor.rb +19 -0
  10. data/lib/associate_jsonb/arel_nodes/jsonb/attribute.rb +38 -0
  11. data/lib/associate_jsonb/arel_nodes/sql_casted_binary.rb +20 -0
  12. data/lib/associate_jsonb/arel_nodes/sql_casted_equality.rb +26 -12
  13. data/lib/associate_jsonb/associations/alias_tracker.rb +13 -0
  14. data/lib/associate_jsonb/associations/association_scope.rb +18 -45
  15. data/lib/associate_jsonb/associations/belongs_to_association.rb +8 -8
  16. data/lib/associate_jsonb/associations/builder/belongs_to.rb +5 -3
  17. data/lib/associate_jsonb/associations/join_dependency.rb +21 -0
  18. data/lib/associate_jsonb/attribute_methods.rb +19 -0
  19. data/lib/associate_jsonb/attribute_methods/read.rb +15 -0
  20. data/lib/associate_jsonb/connection_adapters/schema_creation.rb +167 -0
  21. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_foreign_key_function.rb +9 -0
  22. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_nested_set_function.rb +9 -0
  23. data/lib/associate_jsonb/connection_adapters/schema_definitions/alter_table.rb +40 -0
  24. data/lib/associate_jsonb/connection_adapters/schema_definitions/constraint_definition.rb +60 -0
  25. data/lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb +102 -0
  26. data/lib/associate_jsonb/connection_adapters/schema_definitions/table.rb +12 -0
  27. data/lib/associate_jsonb/connection_adapters/schema_definitions/table_definition.rb +25 -0
  28. data/lib/associate_jsonb/connection_adapters/schema_statements.rb +116 -0
  29. data/lib/associate_jsonb/persistence.rb +14 -0
  30. data/lib/associate_jsonb/predicate_builder.rb +15 -0
  31. data/lib/associate_jsonb/reflection.rb +2 -2
  32. data/lib/associate_jsonb/relation/where_clause.rb +19 -0
  33. data/lib/associate_jsonb/version.rb +1 -1
  34. data/lib/associate_jsonb/with_store_attribute.rb +59 -23
  35. metadata +34 -10
  36. data/lib/associate_jsonb/arel_node_extensions/binary.rb +0 -12
  37. 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 < ::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
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 = node_klass = nil
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
- node_klass.new(table, table[jsonb_column], store_key),
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
- 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
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 <<-CODE, __FILE__, __LINE__ + 1
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 <<-CODE, __FILE__, __LINE__ + 1
53
- store_column_attribute(:#{store}, :#{foreign_key}, :#{foreign_type}, key: "#{key}", **opts)
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