associate_jsonb 0.0.3 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
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 +168 -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 +54 -31
  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