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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +24 -0
  3. data/lib/torque/postgresql/adapter/database_statements.rb +89 -7
  4. data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
  5. data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
  6. data/lib/torque/postgresql/adapter/oid/box.rb +1 -1
  7. data/lib/torque/postgresql/adapter/oid/circle.rb +1 -1
  8. data/lib/torque/postgresql/adapter/oid/composite.rb +116 -0
  9. data/lib/torque/postgresql/adapter/oid/line.rb +1 -1
  10. data/lib/torque/postgresql/adapter/oid/lquery.rb +53 -0
  11. data/lib/torque/postgresql/adapter/oid/ltree.rb +53 -0
  12. data/lib/torque/postgresql/adapter/oid/segment.rb +1 -1
  13. data/lib/torque/postgresql/adapter/oid/struct.rb +150 -0
  14. data/lib/torque/postgresql/adapter/oid/struct_list.rb +52 -0
  15. data/lib/torque/postgresql/adapter/oid/struct_set.rb +38 -0
  16. data/lib/torque/postgresql/adapter/quoting.rb +11 -2
  17. data/lib/torque/postgresql/adapter/schema_definitions.rb +46 -0
  18. data/lib/torque/postgresql/adapter/schema_dumper.rb +28 -2
  19. data/lib/torque/postgresql/adapter/schema_statements.rb +207 -0
  20. data/lib/torque/postgresql/adapter.rb +2 -5
  21. data/lib/torque/postgresql/arel/nodes.rb +63 -1
  22. data/lib/torque/postgresql/arel/visitors.rb +19 -8
  23. data/lib/torque/postgresql/associations/join_dependency.rb +55 -0
  24. data/lib/torque/postgresql/associations.rb +3 -0
  25. data/lib/torque/postgresql/attributes/base.rb +94 -0
  26. data/lib/torque/postgresql/attributes/composite.rb +102 -0
  27. data/lib/torque/postgresql/attributes/enum.rb +13 -2
  28. data/lib/torque/postgresql/attributes/lquery.rb +180 -0
  29. data/lib/torque/postgresql/attributes/ltree.rb +169 -0
  30. data/lib/torque/postgresql/attributes/simple_enum.rb +72 -0
  31. data/lib/torque/postgresql/attributes/struct.rb +137 -0
  32. data/lib/torque/postgresql/base.rb +17 -14
  33. data/lib/torque/postgresql/config.rb +81 -14
  34. data/lib/torque/postgresql/inheritance/expander.rb +66 -0
  35. data/lib/torque/postgresql/inheritance/record.rb +52 -0
  36. data/lib/torque/postgresql/inheritance.rb +138 -65
  37. data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
  38. data/lib/torque/postgresql/predicate_builder/composite_handler.rb +109 -0
  39. data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +44 -0
  40. data/lib/torque/postgresql/predicate_builder/struct_handler.rb +68 -0
  41. data/lib/torque/postgresql/predicate_builder.rb +26 -0
  42. data/lib/torque/postgresql/predicate_table.rb +61 -0
  43. data/lib/torque/postgresql/railtie.rb +35 -0
  44. data/lib/torque/postgresql/relation/inheritance.rb +146 -28
  45. data/lib/torque/postgresql/relation/join_series.rb +8 -6
  46. data/lib/torque/postgresql/relation/merger.rb +23 -7
  47. data/lib/torque/postgresql/relation.rb +12 -11
  48. data/lib/torque/postgresql/schema_cache.rb +1 -0
  49. data/lib/torque/postgresql/validations.rb +29 -0
  50. data/lib/torque/postgresql/version.rb +1 -1
  51. data/lib/torque/postgresql/versioned_commands/command_migration.rb +1 -1
  52. data/lib/torque/postgresql.rb +6 -0
  53. data/spec/initialize.rb +20 -2
  54. data/spec/mocks/cache_query.rb +12 -0
  55. data/spec/models/author.rb +1 -1
  56. data/spec/models/comment.rb +2 -0
  57. data/spec/models/place.rb +2 -0
  58. data/spec/models/profile.rb +31 -0
  59. data/spec/schema.rb +34 -4
  60. data/spec/tests/arel_spec.rb +5 -3
  61. data/spec/tests/composite_spec.rb +800 -0
  62. data/spec/tests/ltree_spec.rb +552 -0
  63. data/spec/tests/struct_spec.rb +968 -0
  64. data/spec/tests/table_inheritance_spec.rb +1027 -56
  65. metadata +49 -7
@@ -2,19 +2,29 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- include ActiveSupport::Configurable
5
+ # Stores a version check for compatibility purposes
6
+ AR810 = (ActiveRecord.gem_version >= Gem::Version.new('8.1.0'))
6
7
 
7
- # Use the same logger as the Active Record one
8
- def self.logger
9
- ActiveRecord::Base.logger
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
- klass = Class.new(ActiveSupport::Configurable::Configuration).new
16
- block.call(klass) if block
17
- send("#{name}=", klass)
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
- PostgreSQL.config[:irregular_models] = hash.map do |(table, model)|
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
- # Determines the name of the column used when identifying that the loaded
176
- # records should be casted to its correctly model. This will be TRUE for
177
- # the records mentioned on `cast_records`
178
- inheritance.auto_cast_column_name = :_auto_cast
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
- config.expose_function_helper_on = nil
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 :_auto_cast_attribute, :_record_class_attribute, to: ActiveRecord::Relation
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
- @inheritance_merged_attributes ||= begin
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
- @inheritance_mergeable_attributes ||= begin
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
- return @physically_inherited if defined?(@physically_inherited)
58
-
59
- @physically_inherited = connection.schema_cache.dependencies(
60
- defined?(@table_name) ? @table_name : decorated_table_name,
61
- ).present?
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
- @casted_dependents ||= inheritance_dependents.map do |table_name|
81
- [table_name, connection.schema_cache.lookup_model(table_name)]
82
- end.to_h
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 giver record class was not
129
- # able to be casted since the model was not identified
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
- An record was not able to be casted to type '#{record_class_value}'.
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
- # If the class is physically inherited, the klass needs to be properly
142
- # changed before moving forward
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
- real_class = torque_discriminate_class_for_record(klass, attributes)
147
- return super if real_class.nil?
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
- attributes, types = sanitize_attributes(real_class, attributes, types)
150
- super(real_class, attributes, types, &block)
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
- # Unwrap the attributes and column types from the given class when
154
- # there are unmergeable attributes
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
- skip = (inheritance_merged_attributes - real_class.attribute_names).to_set
157
- skip.merge(real_class.attribute_names - inheritance_mergeable_attributes)
158
- return [attributes, types] if skip.empty?
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
- dropped = 0
243
+ new_values = {}
161
244
  new_types = {}
162
245
 
163
- row = attributes.instance_variable_get(:@row).dup
164
- indexes = attributes.instance_variable_get(:@column_indexes).dup
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
- # Get the real class when handling physical inheritances and casting
183
- # the record when existing properly is present
184
- def torque_discriminate_class_for_record(klass, record)
185
- return if record[_auto_cast_attribute.to_s] == false
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
- embedded_type = record[_record_class_attribute.to_s]
188
- return if embedded_type.blank? || embedded_type == table_name
258
+ new_values[name] = value
259
+ new_types[name] = types[column] if types.key?(column)
260
+ end
189
261
 
190
- casted_dependents[embedded_type] || raise_unable_to_cast(embedded_type)
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)