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,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,167 @@
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
+ IF store->>key = ''
98
+ THEN
99
+ return FALSE;
100
+ END IF;
101
+
102
+ EXECUTE FORMAT('SELECT EXISTS (SELECT 1 FROM %1$I WHERE %1$I.%2$I = CAST($1 AS ' || type || '))', table_name, foreign_key)
103
+ INTO does_exist
104
+ USING store->>key;
105
+
106
+ RETURN does_exist;
107
+ END;
108
+ $BODY$
109
+ LANGUAGE plpgsql;
110
+
111
+ SQL
112
+ end
113
+
114
+ def visit_AddJsonbNestedSetFunction(*)
115
+ <<~SQL
116
+ CREATE OR REPLACE FUNCTION jsonb_nested_set
117
+ (
118
+ target jsonb,
119
+ path text[],
120
+ new_value jsonb
121
+ )
122
+ RETURNS jsonb AS
123
+ $BODY$
124
+ DECLARE
125
+ new_json jsonb := '{}'::jsonb;
126
+ does_exist BOOLEAN;
127
+ current_path text[];
128
+ key text;
129
+ BEGIN
130
+ IF target #> path IS NOT NULL
131
+ THEN
132
+ return jsonb_set(target, path, new_value);
133
+ ELSE
134
+ new_json := target;
135
+
136
+ IF array_length(path, 1) > 1
137
+ THEN
138
+ FOREACH key IN ARRAY path[:(array_length(path, 1) - 1)]
139
+ LOOP
140
+ current_path := array_append(current_path, key);
141
+ IF new_json #> current_path IS NULL
142
+ THEN
143
+ new_json := jsonb_set(new_json, current_path, '{}'::jsonb, TRUE);
144
+ END IF;
145
+ END LOOP;
146
+ END IF;
147
+
148
+ return jsonb_set(new_json, path, new_value, TRUE);
149
+ END IF;
150
+ END;
151
+ $BODY$
152
+ LANGUAGE plpgsql;
153
+ SQL
154
+ end
155
+
156
+ def add_column_options!(sql, opts)
157
+ super
158
+
159
+ if opts[:constraint]
160
+ sql << " #{accept(ConstraintDefinition.new(**opts[:constraint]))}"
161
+ end
162
+
163
+ sql
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ class AddJsonbForeignKeyFunction
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ class AddJsonbNestedSetFunction
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ module AlterTable # :nodoc:
7
+ attr_reader :constraint_adds, :constraint_drops
8
+ def initialize(td)
9
+ super
10
+ @constraint_adds = []
11
+ @constraint_drops = []
12
+ end
13
+
14
+ def add_constraint(name = nil, **opts)
15
+ unless opts[:value].present?
16
+ raise ArgumentError.new("Invalid Add Constraint Options")
17
+ end
18
+
19
+ @constraint_adds << ConstraintDefinition.new(
20
+ **opts.reverse_merge(name: name)
21
+ )
22
+ end
23
+
24
+ def alter_constraint(name = nil, **opts)
25
+ opts[:force] = true
26
+ add_constraint(name, **opts)
27
+ end
28
+
29
+ def drop_constraint(name = nil, **opts)
30
+ opts = opts.reverse_merge(force: true, name: name, value: nil)
31
+
32
+ unless opts[:name].present? || opts[:value].present?
33
+ raise ArgumentError.new("Invalid Drop Constraint Options")
34
+ end
35
+
36
+ @constraint_drops << ConstraintDefinition.new(**opts)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,60 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ class ConstraintDefinition
7
+ # rubocop:disable Metrics/ParameterLists
8
+ attr_reader :name, :value, :not_valid, :deferrable, :force
9
+ def initialize(value:, name: nil, not_valid: false, force: false, deferrable: true, **)
10
+ @name = name.presence
11
+ @value = value
12
+ @not_valid = not_valid
13
+ @deferrable = deferrable
14
+ @force = force
15
+
16
+ @name ||=
17
+ "rails_constraint_" \
18
+ "#{@value.hash}" \
19
+ "_#{not_valid ? "nv" : "v"}" \
20
+ "_#{deferrable ? "d" : "nd"}"
21
+ end
22
+
23
+ def deferrable_default?
24
+ deferrable.nil?
25
+ end
26
+
27
+
28
+ def name?
29
+ !!name
30
+ end
31
+
32
+ def value?
33
+ !!value
34
+ end
35
+
36
+ def not_valid?
37
+ !!not_valid
38
+ end
39
+
40
+ def deferrable?
41
+ !!deferrable
42
+ end
43
+
44
+ def force?
45
+ !!force
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ name: name,
51
+ value: value,
52
+ not_valid: not_valid,
53
+ deferrable: deferrable,
54
+ force: force
55
+ }
56
+ end
57
+ alias :to_hash :to_h
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,102 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ module ReferenceDefinition
7
+ ForeignKeyDefinition = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition
8
+ # rubocop:disable Metrics/ParameterLists
9
+ def initialize(
10
+ name,
11
+ store: false,
12
+ store_key: false,
13
+ **options
14
+ )
15
+ @store = store && store.to_sym
16
+ @store_key = store_key && store_key.to_s unless options[:polymorphic]
17
+ @nullable = options[:null] != false
18
+
19
+ super(name, **options)
20
+ end
21
+ # rubocop:enable Metrics/ParameterLists
22
+
23
+ def column_name
24
+ store_key || super
25
+ end
26
+
27
+ def add_to(table)
28
+ return super unless store
29
+
30
+ should_add_col = false
31
+ if table.respond_to? :column_exists?
32
+ should_add_col = !table.column_exists?(store)
33
+ elsif table.respond_to? :columns
34
+ should_add_col = table.columns.none? {|col| col.name.to_sym == store}
35
+ end
36
+
37
+ if should_add_col
38
+ opts = { null: false, default: {} }
39
+ table.column(store, :jsonb, **opts)
40
+ end
41
+
42
+ if foreign_key && column_names.length == 1
43
+ fk = ForeignKeyDefinition.new(table.name, foreign_table_name, foreign_key_options)
44
+ columns.each do |col_name, type, options|
45
+ options ||= {}
46
+ value = <<-SQL.squish
47
+ jsonb_foreign_key(
48
+ '#{fk.to_table}'::text,
49
+ '#{fk.primary_key}'::text,
50
+ #{store}::jsonb,
51
+ '#{col_name}'::text,
52
+ '#{type}'::text,
53
+ #{nullable}
54
+ )
55
+ SQL
56
+ table.constraint(
57
+ name: "#{table.name}_#{col_name}_foreign_key",
58
+ value: value,
59
+ not_valid: true,
60
+ deferrable: true
61
+ )
62
+ end
63
+ elsif !nullable
64
+ columns.each do |col_name, *|
65
+ value = <<-SQL.squish
66
+ #{store}->>'#{col_name}' IS NOT NULL
67
+ AND
68
+ #{store}->>'#{col_name}' <> ''
69
+ SQL
70
+ table.constraint(
71
+ name: "#{table.name}_#{col_name}_not_null",
72
+ value: value,
73
+ not_valid: false,
74
+ deferrable: true
75
+ )
76
+ end
77
+ end
78
+
79
+ return unless index
80
+
81
+ columns.each do |col_name, type, *|
82
+ type = :text if type == :string
83
+ table.index(
84
+ "CAST (\"#{store}\"->>'#{col_name}' AS #{type || :bigint})",
85
+ using: :btree,
86
+ name: "index_#{table.name}_on_#{store}_#{col_name}"
87
+ )
88
+
89
+ table.index(
90
+ "(\"#{store}\"->>'#{col_name}')",
91
+ using: :btree,
92
+ name: "index_#{table.name}_on_#{store}_#{col_name}_text"
93
+ )
94
+ end
95
+ end
96
+
97
+ protected
98
+
99
+ attr_reader :store, :store_key, :nullable
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ module Table
7
+ def constraint(**opts)
8
+ @base.add_constraint(name, **opts)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ module TableDefinition
7
+ attr_reader :constraints
8
+
9
+ def initialize(*,**)
10
+ super
11
+ @constraints = []
12
+ end
13
+
14
+ def constraint(name = nil, **opts)
15
+ unless opts[:value].present?
16
+ raise ArgumentError.new("Invalid Drop Constraint Options")
17
+ end
18
+
19
+ @constraints << ConstraintDefinition.new(
20
+ **opts.reverse_merge(name: name)
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end