associate_jsonb 0.0.1 → 0.0.7

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 +81 -1
  3. data/lib/associate_jsonb.rb +111 -1
  4. data/lib/associate_jsonb/arel_extensions/nodes/binary.rb +14 -0
  5. data/lib/associate_jsonb/arel_extensions/nodes/table_alias.rb +38 -0
  6. data/lib/associate_jsonb/arel_extensions/table.rb +40 -0
  7. data/lib/associate_jsonb/arel_extensions/visitors/postgresql.rb +113 -0
  8. data/lib/associate_jsonb/arel_extensions/visitors/visitor.rb +19 -0
  9. data/lib/associate_jsonb/arel_nodes/jsonb/attribute.rb +38 -0
  10. data/lib/associate_jsonb/arel_nodes/sql_casted_binary.rb +20 -0
  11. data/lib/associate_jsonb/arel_nodes/sql_casted_equality.rb +26 -12
  12. data/lib/associate_jsonb/associations/alias_tracker.rb +13 -0
  13. data/lib/associate_jsonb/associations/association_scope.rb +18 -45
  14. data/lib/associate_jsonb/associations/belongs_to_association.rb +8 -8
  15. data/lib/associate_jsonb/associations/builder/belongs_to.rb +5 -3
  16. data/lib/associate_jsonb/associations/join_dependency.rb +21 -0
  17. data/lib/associate_jsonb/attribute_methods.rb +19 -0
  18. data/lib/associate_jsonb/attribute_methods/read.rb +15 -0
  19. data/lib/associate_jsonb/connection_adapters/schema_creation.rb +162 -0
  20. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_foreign_key_function.rb +9 -0
  21. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_nested_set_function.rb +9 -0
  22. data/lib/associate_jsonb/connection_adapters/schema_definitions/alter_table.rb +40 -0
  23. data/lib/associate_jsonb/connection_adapters/schema_definitions/constraint_definition.rb +60 -0
  24. data/lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb +88 -0
  25. data/lib/associate_jsonb/connection_adapters/schema_definitions/table.rb +12 -0
  26. data/lib/associate_jsonb/connection_adapters/schema_definitions/table_definition.rb +25 -0
  27. data/lib/associate_jsonb/connection_adapters/schema_statements.rb +116 -0
  28. data/lib/associate_jsonb/persistence.rb +14 -0
  29. data/lib/associate_jsonb/predicate_builder.rb +15 -0
  30. data/lib/associate_jsonb/reflection.rb +2 -2
  31. data/lib/associate_jsonb/relation/where_clause.rb +19 -0
  32. data/lib/associate_jsonb/supported_rails_version.rb +6 -0
  33. data/lib/associate_jsonb/version.rb +1 -1
  34. data/lib/associate_jsonb/with_store_attribute.rb +59 -23
  35. metadata +39 -14
  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,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
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module AttributeMethods
6
+ module Read
7
+ extend ActiveSupport::Concern
8
+
9
+ def _fetch_attribute(attr_name, &block) # :nodoc
10
+ sync_with_transaction_state if @transaction_state&.finalized?
11
+ @attributes.fetch(attr_name.to_s, &block)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,162 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ module SchemaCreation
7
+ private
8
+ def visit_AlterTable(o)
9
+ sql = super
10
+ sql << o.constraint_adds.map {|ct| visit_AddConstraint ct }.join(" ")
11
+ sql << o.constraint_drops.map {|ct| visit_DropConstraint ct }.join(" ")
12
+ sql
13
+ end
14
+
15
+ def visit_TableDefinition(o)
16
+ create_sql = +"CREATE#{table_modifier_in_create(o)} TABLE "
17
+ create_sql << "IF NOT EXISTS " if o.if_not_exists
18
+ create_sql << "#{quote_table_name(o.name)} "
19
+
20
+ statements = o.columns.map { |c| accept c }
21
+ statements << accept(o.primary_keys) if o.primary_keys
22
+
23
+ if supports_indexes_in_create?
24
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
25
+ end
26
+
27
+ if supports_foreign_keys?
28
+ statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
29
+ # statements.concat(o.constraints.map { |ct| visit_ConstraintDefinition(ct) })
30
+ end
31
+
32
+ create_sql << "(#{statements.join(', ')})" if statements.present?
33
+ add_table_options!(create_sql, table_options(o))
34
+ create_sql << " AS #{to_sql(o.as)}" if o.as
35
+ create_sql
36
+ end
37
+
38
+ def visit_ConstraintDeferral(o)
39
+ return "" unless o.deferrable_default?
40
+ return "NOT DEFERRABLE" unless o.deferrable?
41
+ initial =
42
+ case o.deferrable
43
+ when :immediate
44
+ "IMMEDIATE"
45
+ else
46
+ "DEFERRED"
47
+ end
48
+ "DEFERRABLE INITIALLY #{initial}"
49
+ end
50
+
51
+ def visit_ConstraintDefinition(o)
52
+ +<<-SQL.squish
53
+ CONSTRAINT #{quote_column_name(o.name)}
54
+ CHECK (#{o.value})
55
+ #{visit_ConstraintDeferral(o)}
56
+ #{o.not_valid? ? "NOT VALID" : ''}
57
+ SQL
58
+ end
59
+
60
+ def visit_AddConstraint(o)
61
+ sql = +""
62
+ if o.force?
63
+ sql << visit_DropConstraint(o)
64
+ sql << " "
65
+ end
66
+ sql << "ADD #{accept(o)}"
67
+ end
68
+
69
+ def visit_DropConstraint(o, if_exists: false)
70
+ +<<-SQL.squish
71
+ DROP CONSTRAINT #{quote_column_name(o.name)}
72
+ #{o.force? ? "IF EXISTS" : ""}
73
+ SQL
74
+ end
75
+
76
+ def visit_AddJsonbForeignKeyFunction(*)
77
+ <<~SQL
78
+ CREATE OR REPLACE FUNCTION jsonb_foreign_key
79
+ (
80
+ table_name text,
81
+ foreign_key text,
82
+ store jsonb,
83
+ key text,
84
+ type text default 'numeric',
85
+ nullable boolean default TRUE
86
+ )
87
+ RETURNS BOOLEAN AS
88
+ $BODY$
89
+ DECLARE
90
+ does_exist BOOLEAN;
91
+ BEGIN
92
+ IF store->key IS NULL
93
+ THEN
94
+ return nullable;
95
+ END IF;
96
+
97
+ EXECUTE FORMAT('SELECT EXISTS (SELECT 1 FROM %1$I WHERE %1$I.%2$I = CAST($1 AS ' || type || '))', table_name, foreign_key)
98
+ INTO does_exist
99
+ USING store->>key;
100
+
101
+ RETURN does_exist;
102
+ END;
103
+ $BODY$
104
+ LANGUAGE plpgsql;
105
+
106
+ SQL
107
+ end
108
+
109
+ def visit_AddJsonbNestedSetFunction(*)
110
+ <<~SQL
111
+ CREATE OR REPLACE FUNCTION jsonb_nested_set
112
+ (
113
+ target jsonb,
114
+ path text[],
115
+ new_value jsonb
116
+ )
117
+ RETURNS jsonb AS
118
+ $BODY$
119
+ DECLARE
120
+ new_json jsonb := '{}'::jsonb;
121
+ does_exist BOOLEAN;
122
+ current_path text[];
123
+ key text;
124
+ BEGIN
125
+ IF target #> path IS NOT NULL
126
+ THEN
127
+ return jsonb_set(target, path, new_value);
128
+ ELSE
129
+ new_json := target;
130
+
131
+ IF array_length(path, 1) > 1
132
+ THEN
133
+ FOREACH key IN ARRAY path[:(array_length(path, 1) - 1)]
134
+ LOOP
135
+ current_path := array_append(current_path, key);
136
+ IF new_json #> current_path IS NULL
137
+ THEN
138
+ new_json := jsonb_set(new_json, current_path, '{}'::jsonb, TRUE);
139
+ END IF;
140
+ END LOOP;
141
+ END IF;
142
+
143
+ return jsonb_set(new_json, path, new_value, TRUE);
144
+ END IF;
145
+ END;
146
+ $BODY$
147
+ LANGUAGE plpgsql;
148
+ SQL
149
+ end
150
+
151
+ def add_column_options!(sql, opts)
152
+ super
153
+
154
+ if opts[:constraint]
155
+ sql << " #{accept(ConstraintDefinition.new(**opts[:constraint]))}"
156
+ end
157
+
158
+ sql
159
+ end
160
+ end
161
+ end
162
+ end