associate_jsonb 0.0.1 → 0.0.7

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 +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