associate_jsonb 0.0.2 → 0.0.8

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