torque-postgresql 4.0.0 → 4.1.0
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/Rakefile +24 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +89 -7
- data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
- data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
- data/lib/torque/postgresql/adapter/oid/box.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/circle.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/composite.rb +116 -0
- data/lib/torque/postgresql/adapter/oid/line.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/lquery.rb +53 -0
- data/lib/torque/postgresql/adapter/oid/ltree.rb +53 -0
- data/lib/torque/postgresql/adapter/oid/segment.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/struct.rb +150 -0
- data/lib/torque/postgresql/adapter/oid/struct_list.rb +52 -0
- data/lib/torque/postgresql/adapter/oid/struct_set.rb +38 -0
- data/lib/torque/postgresql/adapter/quoting.rb +11 -2
- data/lib/torque/postgresql/adapter/schema_definitions.rb +46 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +28 -2
- data/lib/torque/postgresql/adapter/schema_statements.rb +207 -0
- data/lib/torque/postgresql/adapter.rb +2 -5
- data/lib/torque/postgresql/arel/nodes.rb +63 -1
- data/lib/torque/postgresql/arel/visitors.rb +19 -8
- data/lib/torque/postgresql/associations/join_dependency.rb +55 -0
- data/lib/torque/postgresql/associations.rb +3 -0
- data/lib/torque/postgresql/attributes/base.rb +94 -0
- data/lib/torque/postgresql/attributes/composite.rb +102 -0
- data/lib/torque/postgresql/attributes/enum.rb +13 -2
- data/lib/torque/postgresql/attributes/lquery.rb +180 -0
- data/lib/torque/postgresql/attributes/ltree.rb +169 -0
- data/lib/torque/postgresql/attributes/simple_enum.rb +72 -0
- data/lib/torque/postgresql/attributes/struct.rb +137 -0
- data/lib/torque/postgresql/base.rb +17 -14
- data/lib/torque/postgresql/config.rb +81 -14
- data/lib/torque/postgresql/inheritance/expander.rb +66 -0
- data/lib/torque/postgresql/inheritance/record.rb +52 -0
- data/lib/torque/postgresql/inheritance.rb +138 -65
- data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
- data/lib/torque/postgresql/predicate_builder/composite_handler.rb +109 -0
- data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +44 -0
- data/lib/torque/postgresql/predicate_builder/struct_handler.rb +68 -0
- data/lib/torque/postgresql/predicate_builder.rb +26 -0
- data/lib/torque/postgresql/predicate_table.rb +61 -0
- data/lib/torque/postgresql/railtie.rb +35 -0
- data/lib/torque/postgresql/relation/inheritance.rb +146 -28
- data/lib/torque/postgresql/relation/join_series.rb +8 -6
- data/lib/torque/postgresql/relation/merger.rb +23 -7
- data/lib/torque/postgresql/relation.rb +12 -11
- data/lib/torque/postgresql/schema_cache.rb +1 -0
- data/lib/torque/postgresql/validations.rb +29 -0
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +1 -1
- data/lib/torque/postgresql.rb +6 -0
- data/spec/initialize.rb +20 -2
- data/spec/mocks/cache_query.rb +12 -0
- data/spec/models/author.rb +1 -1
- data/spec/models/comment.rb +2 -0
- data/spec/models/place.rb +2 -0
- data/spec/models/profile.rb +31 -0
- data/spec/schema.rb +34 -4
- data/spec/tests/arel_spec.rb +5 -3
- data/spec/tests/composite_spec.rb +800 -0
- data/spec/tests/ltree_spec.rb +552 -0
- data/spec/tests/struct_spec.rb +968 -0
- data/spec/tests/table_inheritance_spec.rb +1027 -56
- metadata +49 -7
|
@@ -2,19 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
module Torque
|
|
4
4
|
module PostgreSQL
|
|
5
|
-
|
|
5
|
+
# Stores a version check for compatibility purposes
|
|
6
|
+
AR810 = (ActiveRecord.gem_version >= Gem::Version.new('8.1.0'))
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
class << self
|
|
9
|
+
def config
|
|
10
|
+
@config ||= ActiveSupport::InheritableOptions.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def configure
|
|
14
|
+
yield config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Use the same logger as the Active Record one
|
|
18
|
+
def logger
|
|
19
|
+
ActiveRecord::Base.logger
|
|
20
|
+
end
|
|
10
21
|
end
|
|
11
22
|
|
|
12
23
|
# Allow nested configurations
|
|
13
|
-
# :TODO: Rely on +inheritable_copy+ to make nested configurations
|
|
14
24
|
config.define_singleton_method(:nested) do |name, &block|
|
|
15
|
-
|
|
16
|
-
block
|
|
17
|
-
|
|
25
|
+
nested = ActiveSupport::InheritableOptions.new
|
|
26
|
+
block&.call(nested)
|
|
27
|
+
self[name] = nested
|
|
18
28
|
end
|
|
19
29
|
|
|
20
30
|
# Set if any information that requires querying and searching or collecting
|
|
@@ -31,7 +41,7 @@ module Torque
|
|
|
31
41
|
# Set a list of irregular model name when associated with table names
|
|
32
42
|
config.irregular_models = {}
|
|
33
43
|
def config.irregular_models=(hash)
|
|
34
|
-
|
|
44
|
+
self[:irregular_models] = hash.map do |(table, model)|
|
|
35
45
|
[table.to_s, model.to_s]
|
|
36
46
|
end.to_h
|
|
37
47
|
end
|
|
@@ -131,6 +141,27 @@ module Torque
|
|
|
131
141
|
|
|
132
142
|
end
|
|
133
143
|
|
|
144
|
+
# Configure composite type features
|
|
145
|
+
config.nested(:composite) do |composite|
|
|
146
|
+
|
|
147
|
+
# Enables composite types handler by this gem
|
|
148
|
+
composite.enabled = true
|
|
149
|
+
|
|
150
|
+
# Specify the namespace of each composite type class
|
|
151
|
+
composite.namespace = nil
|
|
152
|
+
|
|
153
|
+
# Set a list of irregular class names when associated with composite
|
|
154
|
+
# types
|
|
155
|
+
def composite.irregular_types=(hash)
|
|
156
|
+
self[:irregular_types] = hash.map do |type, klass|
|
|
157
|
+
[type.to_s, klass.to_s]
|
|
158
|
+
end.to_h
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
composite.irregular_types = {}
|
|
162
|
+
|
|
163
|
+
end
|
|
164
|
+
|
|
134
165
|
# Configure geometry data types
|
|
135
166
|
config.nested(:geometry) do |geometry|
|
|
136
167
|
|
|
@@ -172,10 +203,21 @@ module Torque
|
|
|
172
203
|
# the name of the table that actually holds the record
|
|
173
204
|
inheritance.record_class_column_name = :_record_class
|
|
174
205
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Configure struct features
|
|
209
|
+
config.nested(:struct) do |struct|
|
|
210
|
+
|
|
211
|
+
# Enables the JSON(B) columns backed by ActiveModel classes handler
|
|
212
|
+
struct.enabled = true
|
|
213
|
+
|
|
214
|
+
# The name of the method to be used on any ActiveRecord::Base to
|
|
215
|
+
# initialize model-based struct features
|
|
216
|
+
struct.base_method = :struct_for
|
|
217
|
+
|
|
218
|
+
# Whether struct classes reject properties that they don't declare
|
|
219
|
+
# by default
|
|
220
|
+
struct.default_strict = true
|
|
179
221
|
|
|
180
222
|
end
|
|
181
223
|
|
|
@@ -260,12 +302,35 @@ module Torque
|
|
|
260
302
|
|
|
261
303
|
end
|
|
262
304
|
|
|
305
|
+
# Configure label tree features
|
|
306
|
+
config.nested(:ltree) do |ltree|
|
|
307
|
+
|
|
308
|
+
# Enables the ltree and lquery data types handler by this gem
|
|
309
|
+
ltree.enabled = true
|
|
310
|
+
|
|
311
|
+
# A hash of replacements applied to every label provided by the
|
|
312
|
+
# application before it gets validated, so that a source that does not
|
|
313
|
+
# satisfy PostgreSQL's rules on its own can still be used. Dashes, for
|
|
314
|
+
# example, are only accepted as of PostgreSQL 16
|
|
315
|
+
# { '-' => '_' } turns dashes into underscores
|
|
316
|
+
# { '-' => '' } drops dashes
|
|
317
|
+
# It never applies to values read from the database
|
|
318
|
+
ltree.sanitize = nil
|
|
319
|
+
|
|
320
|
+
# The name of a method that, whenever the given object responds to it, is
|
|
321
|
+
# used to translate that object into a path or a pattern. It is what lets
|
|
322
|
+
# any class be used wherever one is expected, on assignment, on a
|
|
323
|
+
# condition, and while comparing one path to another
|
|
324
|
+
ltree.compatible_method = :to_tree_path
|
|
325
|
+
|
|
326
|
+
end
|
|
327
|
+
|
|
263
328
|
# Configure arel additional features
|
|
264
329
|
config.nested(:arel) do |arel|
|
|
265
330
|
|
|
266
331
|
# When provided, the initializer will expose the Arel function helper on
|
|
267
332
|
# the given module
|
|
268
|
-
|
|
333
|
+
arel.expose_function_helper_on = nil
|
|
269
334
|
|
|
270
335
|
# List of Arel INFIX operators that will be made available for using as
|
|
271
336
|
# methods on Arel::Nodes::Node and Arel::Attribute
|
|
@@ -279,6 +344,8 @@ module Torque
|
|
|
279
344
|
'doesnt_right_extend' => '&<',
|
|
280
345
|
'doesnt_left_extend' => '&>',
|
|
281
346
|
'adjacent_to' => '-|-',
|
|
347
|
+
'matches_lquery' => '~',
|
|
348
|
+
'matches_any_lquery' => '?',
|
|
282
349
|
}
|
|
283
350
|
|
|
284
351
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Inheritance
|
|
6
|
+
class Expander
|
|
7
|
+
|
|
8
|
+
def initialize(model, records, targets)
|
|
9
|
+
@model = model
|
|
10
|
+
@records = records
|
|
11
|
+
@targets = targets
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Load the columns that are missing from each partial record using one
|
|
15
|
+
# query per inherited table
|
|
16
|
+
def call
|
|
17
|
+
pending.each do |klass, records|
|
|
18
|
+
amend(klass, records, fetch(klass, records.map(&:id)))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@records
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :model, :targets
|
|
27
|
+
|
|
28
|
+
def pending
|
|
29
|
+
@records.select do |record|
|
|
30
|
+
record.partial_record? && targets.include?(record.class)
|
|
31
|
+
end.group_by(&:class)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def extra_columns(klass)
|
|
35
|
+
klass.attribute_names - model.attribute_names
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def fetch(klass, ids)
|
|
39
|
+
columns = [klass.primary_key, *extra_columns(klass)]
|
|
40
|
+
table = klass.arel_table
|
|
41
|
+
query = klass.unscoped.itself_only.select(*columns.map { |name| table[name] })
|
|
42
|
+
|
|
43
|
+
klass.lease_connection.select_all(
|
|
44
|
+
query.where(klass.primary_key => ids).arel,
|
|
45
|
+
"#{klass.name} Expand",
|
|
46
|
+
).to_a.index_by { |row| row[klass.primary_key] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def amend(klass, records, rows)
|
|
50
|
+
columns = extra_columns(klass)
|
|
51
|
+
|
|
52
|
+
records.each do |record|
|
|
53
|
+
row = rows[record.id]
|
|
54
|
+
next if row.nil?
|
|
55
|
+
|
|
56
|
+
attributes = record.instance_variable_get(:@attributes)
|
|
57
|
+
columns.each { |name| attributes.write_from_database(name, row[name]) }
|
|
58
|
+
|
|
59
|
+
record.send(:mark_as_full_record!)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Inheritance
|
|
6
|
+
module Record
|
|
7
|
+
|
|
8
|
+
# Whether the record was loaded from one of its ancestor tables and
|
|
9
|
+
# therefore is missing the columns that only exist on its own table
|
|
10
|
+
def partial_record?
|
|
11
|
+
@partial_record == true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Reload always queries the record's own table, which produces a
|
|
15
|
+
# complete and writable record
|
|
16
|
+
def reload(*)
|
|
17
|
+
return super unless partial_record?
|
|
18
|
+
|
|
19
|
+
super.tap { mark_as_full_record! }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Deleting a row only needs its primary key, so the missing columns of
|
|
23
|
+
# a partial record are no reason to refuse it
|
|
24
|
+
def destroy
|
|
25
|
+
return super unless partial_record?
|
|
26
|
+
|
|
27
|
+
without_readonly { super }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def without_readonly
|
|
33
|
+
@readonly = false
|
|
34
|
+
yield
|
|
35
|
+
ensure
|
|
36
|
+
@readonly = true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def mark_as_partial_record!
|
|
40
|
+
@partial_record = true
|
|
41
|
+
readonly!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def mark_as_full_record!
|
|
45
|
+
@partial_record = false
|
|
46
|
+
@readonly = false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'inheritance/record'
|
|
4
|
+
require_relative 'inheritance/expander'
|
|
5
|
+
|
|
3
6
|
module Torque
|
|
4
7
|
module PostgreSQL
|
|
5
8
|
InheritanceError = Class.new(ArgumentError)
|
|
@@ -7,25 +10,12 @@ module Torque
|
|
|
7
10
|
module Inheritance
|
|
8
11
|
extend ActiveSupport::Concern
|
|
9
12
|
|
|
10
|
-
# Cast the given object to its correct class
|
|
11
|
-
def cast_record
|
|
12
|
-
record_class_value = send(self.class._record_class_attribute)
|
|
13
|
-
|
|
14
|
-
return self unless self.class.table_name != record_class_value
|
|
15
|
-
klass = self.class.casted_dependents[record_class_value]
|
|
16
|
-
self.class.raise_unable_to_cast(record_class_value) if klass.nil?
|
|
17
|
-
|
|
18
|
-
# The record need to be re-queried to have its attributes loaded
|
|
19
|
-
# :TODO: Improve this by only loading the necessary extra columns
|
|
20
|
-
klass.find(self.id)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
13
|
class_methods do
|
|
24
|
-
delegate :
|
|
14
|
+
delegate :_record_class_attribute, :_record_class_column_name, to: ActiveRecord::Relation
|
|
25
15
|
|
|
26
16
|
# Get a full list of all attributes from a model and all its dependents
|
|
27
17
|
def inheritance_merged_attributes
|
|
28
|
-
|
|
18
|
+
inheritance_cache(:@inheritance_merged_attributes) do
|
|
29
19
|
children = casted_dependents.values.flat_map(&:attribute_names)
|
|
30
20
|
attribute_names.to_set.merge(children).to_a.freeze
|
|
31
21
|
end
|
|
@@ -34,7 +24,7 @@ module Torque
|
|
|
34
24
|
# Get the list of attributes that can be merged while querying because
|
|
35
25
|
# they all have the same type
|
|
36
26
|
def inheritance_mergeable_attributes
|
|
37
|
-
|
|
27
|
+
inheritance_cache(:@inheritance_mergeable_attributes) do
|
|
38
28
|
base = inheritance_merged_attributes - attribute_names
|
|
39
29
|
types = base.zip(base.size.times.map { [] }).to_h
|
|
40
30
|
|
|
@@ -54,11 +44,11 @@ module Torque
|
|
|
54
44
|
|
|
55
45
|
# Check if the model's table depends on any inheritance
|
|
56
46
|
def physically_inherited?
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
inheritance_cache(:@physically_inherited) do
|
|
48
|
+
connection.schema_cache.dependencies(
|
|
49
|
+
defined?(@table_name) ? @table_name : decorated_table_name,
|
|
50
|
+
).present?
|
|
51
|
+
end
|
|
62
52
|
rescue ActiveRecord::ConnectionNotEstablished
|
|
63
53
|
false
|
|
64
54
|
end
|
|
@@ -71,15 +61,58 @@ module Torque
|
|
|
71
61
|
|
|
72
62
|
# Check whether the model's table has directly or indirectly dependents
|
|
73
63
|
def physically_inheritances?
|
|
74
|
-
inheritance_dependents.present?
|
|
64
|
+
inheritance_cache(:@physically_inheritances) { inheritance_dependents.present? }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The prefix that identifies the columns of this table when they are
|
|
68
|
+
# loaded next to the columns of a sibling table
|
|
69
|
+
def inheritance_column_prefix
|
|
70
|
+
inheritance_cache(:@inheritance_column_prefix) { "#{table_name}__".freeze }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The topmost class of this class' physical inheritance family, which
|
|
74
|
+
# is what casted_dependents needs to be called on to see every
|
|
75
|
+
# dependent in the family rather than just this class' own descendants
|
|
76
|
+
def physically_inheritance_root
|
|
77
|
+
physically_inherited? ? superclass.physically_inheritance_root : self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The columns that belong to other dependents of this class' physical
|
|
81
|
+
# inheritance family, which must never land on one of its casted
|
|
82
|
+
# records because they belong to a sibling, not to this class
|
|
83
|
+
def inheritance_foreign_attribute_names
|
|
84
|
+
inheritance_cache(:@inheritance_foreign_attribute_names) do
|
|
85
|
+
(physically_inheritance_root.inheritance_merged_attributes - attribute_names).to_set.freeze
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The exact "table_name__" prefixes build_inheritances emits for a
|
|
90
|
+
# non-mergeable column of any dependent in this class' physical
|
|
91
|
+
# inheritance family, which is what a joined sibling column looks like
|
|
92
|
+
def inheritance_dependent_prefixes
|
|
93
|
+
inheritance_cache(:@inheritance_dependent_prefixes) do
|
|
94
|
+
physically_inheritance_root.casted_dependents.keys.map { |name| "#{name}__" }.to_set.freeze
|
|
95
|
+
end
|
|
75
96
|
end
|
|
76
97
|
|
|
77
98
|
# Get the list of all ActiveRecord classes directly or indirectly
|
|
78
99
|
# associated by inheritance
|
|
79
100
|
def casted_dependents
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
inheritance_cache(:@casted_dependents) do
|
|
102
|
+
inheritance_dependents.map do |table_name|
|
|
103
|
+
[table_name, connection.schema_cache.lookup_model(table_name)]
|
|
104
|
+
end.to_h.freeze
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get the dependents that actually add columns, which are the only ones
|
|
109
|
+
# that produce incomplete records when loaded from this table
|
|
110
|
+
def inheritance_expandable_dependents
|
|
111
|
+
inheritance_cache(:@inheritance_expandable_dependents) do
|
|
112
|
+
casted_dependents.select do |_table_name, klass|
|
|
113
|
+
(klass.attribute_names - attribute_names).any?
|
|
114
|
+
end.freeze
|
|
115
|
+
end
|
|
83
116
|
end
|
|
84
117
|
|
|
85
118
|
# Manually set the model name associated with tables name in order to
|
|
@@ -125,73 +158,113 @@ module Torque
|
|
|
125
158
|
physically_inherited? ? decorated_table_name : super
|
|
126
159
|
end
|
|
127
160
|
|
|
128
|
-
# Raises an error message saying that the
|
|
129
|
-
#
|
|
161
|
+
# Raises an error message saying that the given record class could not
|
|
162
|
+
# be instantiated since the model was not identified
|
|
130
163
|
def raise_unable_to_cast(record_class_value)
|
|
131
164
|
raise InheritanceError.new(<<~MSG.squish)
|
|
132
|
-
|
|
165
|
+
A record could not be instantiated as '#{record_class_value}'.
|
|
133
166
|
If this table name doesn't represent a guessable model,
|
|
134
167
|
please use 'Torque::PostgreSQL.conf.irregular_models =
|
|
135
168
|
{ '#{record_class_value}' => 'ModelName' }'.
|
|
136
169
|
MSG
|
|
137
170
|
end
|
|
138
171
|
|
|
172
|
+
protected
|
|
173
|
+
|
|
174
|
+
# Inheritance metadata is derived from the schema, so it must be
|
|
175
|
+
# dropped whenever ActiveRecord drops its own schema-derived caches
|
|
176
|
+
def reload_schema_from_cache(recursive = true)
|
|
177
|
+
@physically_inherited = nil
|
|
178
|
+
@physically_inheritances = nil
|
|
179
|
+
@casted_dependents = nil
|
|
180
|
+
@inheritance_merged_attributes = nil
|
|
181
|
+
@inheritance_mergeable_attributes = nil
|
|
182
|
+
@inheritance_expandable_dependents = nil
|
|
183
|
+
@inheritance_column_prefix = nil
|
|
184
|
+
@inheritance_foreign_attribute_names = nil
|
|
185
|
+
@inheritance_dependent_prefixes = nil
|
|
186
|
+
super
|
|
187
|
+
end
|
|
188
|
+
|
|
139
189
|
private
|
|
140
190
|
|
|
141
|
-
#
|
|
142
|
-
#
|
|
191
|
+
# Memoize using double-checked locking against ActiveRecord's own
|
|
192
|
+
# monitor for loading the schema
|
|
193
|
+
def inheritance_cache(name)
|
|
194
|
+
current = instance_variable_get(name)
|
|
195
|
+
return current unless current.nil?
|
|
196
|
+
|
|
197
|
+
# why: @load_schema_monitor is still nil while AR's inherited reaches base_class
|
|
198
|
+
(@load_schema_monitor ||= Monitor.new).synchronize do
|
|
199
|
+
current = instance_variable_get(name)
|
|
200
|
+
return current unless current.nil?
|
|
201
|
+
|
|
202
|
+
instance_variable_set(name, yield)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Rows coming from a table with dependents carry the table they were
|
|
207
|
+
# stored in, which is what decides the class to instantiate
|
|
143
208
|
def instantiate_instance_of(klass, attributes, types = {}, &block)
|
|
144
209
|
return super unless klass.physically_inheritances?
|
|
145
210
|
|
|
146
|
-
|
|
147
|
-
|
|
211
|
+
marker = _record_class_column_name
|
|
212
|
+
record_class = attributes[marker]
|
|
213
|
+
if record_class.blank? || record_class == klass.table_name
|
|
214
|
+
values, types = sanitize_attributes(klass, attributes, types)
|
|
215
|
+
return super(klass, values, types, &block)
|
|
216
|
+
end
|
|
148
217
|
|
|
149
|
-
|
|
150
|
-
|
|
218
|
+
real_class = klass.casted_dependents[record_class]
|
|
219
|
+
klass.raise_unable_to_cast(record_class) if real_class.nil?
|
|
220
|
+
|
|
221
|
+
values, types = sanitize_attributes(real_class, attributes, types)
|
|
222
|
+
record = super(real_class, values, types, &block)
|
|
223
|
+
|
|
224
|
+
# why: expand_records may already join in real_class's own columns
|
|
225
|
+
if klass.inheritance_expandable_dependents.key?(record_class)
|
|
226
|
+
incomplete = real_class.attribute_names.any? { |name| !values.key?(name) }
|
|
227
|
+
record.send(:mark_as_partial_record!) if incomplete
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
record
|
|
151
231
|
end
|
|
152
232
|
|
|
153
|
-
#
|
|
154
|
-
#
|
|
233
|
+
# Drop what is known to be foreign: the internal record class column
|
|
234
|
+
# and the columns prefixed with another dependent table of the same
|
|
235
|
+
# family, unwrapping our own prefix and letting everything else
|
|
236
|
+
# through, including a user alias that happens to contain '__'
|
|
155
237
|
def sanitize_attributes(real_class, attributes, types)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
238
|
+
prefix = real_class.inheritance_column_prefix
|
|
239
|
+
sibling_prefixes = real_class.inheritance_dependent_prefixes
|
|
240
|
+
foreign = real_class.inheritance_foreign_attribute_names
|
|
241
|
+
skip = _record_class_column_name
|
|
159
242
|
|
|
160
|
-
|
|
243
|
+
new_values = {}
|
|
161
244
|
new_types = {}
|
|
162
245
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
indexes = indexes.each_with_object({}) do |(column, index), new_indexes|
|
|
166
|
-
attribute, prefix = column.split('__', 2).reverse
|
|
167
|
-
current_index = index - dropped
|
|
168
|
-
|
|
169
|
-
if prefix != table_name && skip.include?(attribute)
|
|
170
|
-
row.delete_at(current_index)
|
|
171
|
-
dropped += 1
|
|
172
|
-
else
|
|
173
|
-
new_types.merge!(types.slice(attribute))
|
|
174
|
-
new_types[current_index] = types[index]
|
|
175
|
-
new_indexes[attribute] = current_index
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
[ActiveRecord::Result::IndexedRow.new(indexes, row), new_types]
|
|
180
|
-
end
|
|
246
|
+
attributes.to_hash.each do |column, value|
|
|
247
|
+
next if column == skip || foreign.include?(column)
|
|
181
248
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
249
|
+
name =
|
|
250
|
+
if column.start_with?(prefix)
|
|
251
|
+
column.delete_prefix(prefix)
|
|
252
|
+
elsif sibling_prefixes.any? { |sibling_prefix| column.start_with?(sibling_prefix) }
|
|
253
|
+
next
|
|
254
|
+
else
|
|
255
|
+
column
|
|
256
|
+
end
|
|
186
257
|
|
|
187
|
-
|
|
188
|
-
|
|
258
|
+
new_values[name] = value
|
|
259
|
+
new_types[name] = types[column] if types.key?(column)
|
|
260
|
+
end
|
|
189
261
|
|
|
190
|
-
|
|
262
|
+
[new_values, new_types]
|
|
191
263
|
end
|
|
192
264
|
end
|
|
193
265
|
end
|
|
194
266
|
|
|
195
267
|
ActiveRecord::Base.include Inheritance
|
|
268
|
+
ActiveRecord::Base.include Inheritance::Record
|
|
196
269
|
end
|
|
197
270
|
end
|
|
@@ -15,6 +15,82 @@ module Torque
|
|
|
15
15
|
[:rename_type, args.reverse]
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Records the creation of a composite type
|
|
19
|
+
def create_composite_type(*args, &block)
|
|
20
|
+
record(:create_composite_type, args, &block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Inverts the creation of a composite type
|
|
24
|
+
def invert_create_composite_type(args)
|
|
25
|
+
options = args.second.try(:slice, :schema) || {}
|
|
26
|
+
[:drop_type, [args.first, options]]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Records changes to a composite type, which cannot be inverted for the
|
|
30
|
+
# same reason as +change_table+
|
|
31
|
+
def change_composite_type(*args, &block)
|
|
32
|
+
record(:change_composite_type, args, &block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Records a column being added to a composite type
|
|
36
|
+
def add_composite_column(*args, &block)
|
|
37
|
+
record(:add_composite_column, args, &block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Inverts a column being added to a composite type
|
|
41
|
+
def invert_add_composite_column(args)
|
|
42
|
+
[:remove_composite_column, args]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Records a column being removed from a composite type
|
|
46
|
+
def remove_composite_column(*args, &block)
|
|
47
|
+
record(:remove_composite_column, args, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Inverts a column being removed from a composite type, which is only
|
|
51
|
+
# possible when its type was informed
|
|
52
|
+
def invert_remove_composite_column(args)
|
|
53
|
+
raise ActiveRecord::IrreversibleMigration, <<~MSG.squish if args.third.nil?
|
|
54
|
+
remove_composite_column is only reversible if given a type.
|
|
55
|
+
MSG
|
|
56
|
+
|
|
57
|
+
[:add_composite_column, args]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Records the type of a column being changed, which cannot be inverted
|
|
61
|
+
# because the previous type is unknown
|
|
62
|
+
def change_composite_column(*args, &block)
|
|
63
|
+
record(:change_composite_column, args, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Records a column of a composite type being renamed
|
|
67
|
+
def rename_composite_column(*args, &block)
|
|
68
|
+
record(:rename_composite_column, args, &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Inverts a column of a composite type being renamed
|
|
72
|
+
def invert_rename_composite_column(args)
|
|
73
|
+
[:rename_composite_column, [args.first, args.third, args.second, *args[3..]]]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Records the spread of features into inherited tables. While reverting,
|
|
77
|
+
# the pruning form goes to the front of the list, which is played back
|
|
78
|
+
# reversed, so the children are only pruned once everything else the
|
|
79
|
+
# migration did to their parents has been undone
|
|
80
|
+
def sync_inheritance_features(*args, &block)
|
|
81
|
+
return record(:sync_inheritance_features, args, &block) unless reverting
|
|
82
|
+
|
|
83
|
+
commands.unshift(inverse_of(:sync_inheritance_features, args, &block))
|
|
84
|
+
end
|
|
85
|
+
ruby2_keywords(:sync_inheritance_features)
|
|
86
|
+
|
|
87
|
+
# Inverts the spread of features by running it again while pruning, so
|
|
88
|
+
# the children converge back to whatever the parent looks like
|
|
89
|
+
def invert_sync_inheritance_features(args)
|
|
90
|
+
options = args.extract_options!.merge(prune: true)
|
|
91
|
+
[:sync_inheritance_features, [*args, Hash.ruby2_keywords_hash(options)]]
|
|
92
|
+
end
|
|
93
|
+
|
|
18
94
|
# Records the creation of a schema
|
|
19
95
|
def create_schema(*args, &block)
|
|
20
96
|
record(:create_schema, args, &block)
|