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.
- checksums.yaml +4 -4
- data/README.md +145 -31
- data/Rakefile +1 -3
- data/lib/associate_jsonb.rb +111 -2
- data/lib/associate_jsonb/arel_extensions/nodes/binary.rb +14 -0
- data/lib/associate_jsonb/arel_extensions/nodes/table_alias.rb +38 -0
- data/lib/associate_jsonb/arel_extensions/table.rb +40 -0
- data/lib/associate_jsonb/arel_extensions/visitors/postgresql.rb +113 -0
- data/lib/associate_jsonb/arel_extensions/visitors/visitor.rb +19 -0
- data/lib/associate_jsonb/arel_nodes/jsonb/attribute.rb +38 -0
- data/lib/associate_jsonb/arel_nodes/sql_casted_binary.rb +20 -0
- data/lib/associate_jsonb/arel_nodes/sql_casted_equality.rb +26 -12
- data/lib/associate_jsonb/associations/alias_tracker.rb +13 -0
- data/lib/associate_jsonb/associations/association_scope.rb +18 -45
- data/lib/associate_jsonb/associations/belongs_to_association.rb +8 -8
- data/lib/associate_jsonb/associations/builder/belongs_to.rb +5 -3
- data/lib/associate_jsonb/associations/join_dependency.rb +21 -0
- data/lib/associate_jsonb/attribute_methods.rb +19 -0
- data/lib/associate_jsonb/attribute_methods/read.rb +15 -0
- data/lib/associate_jsonb/connection_adapters/schema_creation.rb +167 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_foreign_key_function.rb +9 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_nested_set_function.rb +9 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/alter_table.rb +40 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/constraint_definition.rb +60 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb +102 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/table.rb +12 -0
- data/lib/associate_jsonb/connection_adapters/schema_definitions/table_definition.rb +25 -0
- data/lib/associate_jsonb/connection_adapters/schema_statements.rb +116 -0
- data/lib/associate_jsonb/persistence.rb +14 -0
- data/lib/associate_jsonb/predicate_builder.rb +15 -0
- data/lib/associate_jsonb/reflection.rb +2 -2
- data/lib/associate_jsonb/relation/where_clause.rb +19 -0
- data/lib/associate_jsonb/version.rb +1 -1
- data/lib/associate_jsonb/with_store_attribute.rb +59 -23
- metadata +34 -10
- data/lib/associate_jsonb/arel_node_extensions/binary.rb +0 -12
- 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,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,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
|