torque-postgresql 4.0.1 → 4.1.1

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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +52 -0
  3. data/lib/generators/torque/skill_generator.rb +17 -0
  4. data/lib/generators/torque/templates/trigger.sql.erb +3 -0
  5. data/lib/generators/torque/trigger_generator.rb +13 -0
  6. data/lib/torque/postgresql/adapter/database_statements.rb +98 -7
  7. data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
  8. data/lib/torque/postgresql/adapter/oid/array.rb +13 -0
  9. data/lib/torque/postgresql/adapter/oid/box.rb +1 -1
  10. data/lib/torque/postgresql/adapter/oid/circle.rb +1 -1
  11. data/lib/torque/postgresql/adapter/oid/composite.rb +117 -0
  12. data/lib/torque/postgresql/adapter/oid/enum.rb +1 -1
  13. data/lib/torque/postgresql/adapter/oid/enum_set.rb +2 -2
  14. data/lib/torque/postgresql/adapter/oid/interval.rb +1 -1
  15. data/lib/torque/postgresql/adapter/oid/line.rb +1 -1
  16. data/lib/torque/postgresql/adapter/oid/lquery.rb +70 -0
  17. data/lib/torque/postgresql/adapter/oid/ltree.rb +111 -0
  18. data/lib/torque/postgresql/adapter/oid/segment.rb +1 -1
  19. data/lib/torque/postgresql/adapter/oid/struct.rb +150 -0
  20. data/lib/torque/postgresql/adapter/oid/struct_list.rb +52 -0
  21. data/lib/torque/postgresql/adapter/oid/struct_set.rb +38 -0
  22. data/lib/torque/postgresql/adapter/oid.rb +13 -0
  23. data/lib/torque/postgresql/adapter/quoting.rb +11 -2
  24. data/lib/torque/postgresql/adapter/schema_definitions.rb +51 -0
  25. data/lib/torque/postgresql/adapter/schema_dumper.rb +27 -0
  26. data/lib/torque/postgresql/adapter/schema_statements.rb +207 -0
  27. data/lib/torque/postgresql/adapter.rb +2 -5
  28. data/lib/torque/postgresql/arel/nodes.rb +114 -1
  29. data/lib/torque/postgresql/arel/visitors.rb +65 -8
  30. data/lib/torque/postgresql/associations/join_dependency.rb +55 -0
  31. data/lib/torque/postgresql/associations.rb +3 -0
  32. data/lib/torque/postgresql/attributes/base.rb +97 -0
  33. data/lib/torque/postgresql/attributes/composite.rb +98 -0
  34. data/lib/torque/postgresql/attributes/enum.rb +13 -7
  35. data/lib/torque/postgresql/attributes/lquery.rb +120 -0
  36. data/lib/torque/postgresql/attributes/ltree.rb +121 -0
  37. data/lib/torque/postgresql/attributes/simple_enum.rb +72 -0
  38. data/lib/torque/postgresql/attributes/struct.rb +137 -0
  39. data/lib/torque/postgresql/base.rb +26 -15
  40. data/lib/torque/postgresql/config.rb +100 -15
  41. data/lib/torque/postgresql/geometry_builder.rb +4 -10
  42. data/lib/torque/postgresql/inheritance/expander.rb +66 -0
  43. data/lib/torque/postgresql/inheritance/record.rb +52 -0
  44. data/lib/torque/postgresql/inheritance.rb +141 -68
  45. data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
  46. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +1 -1
  47. data/lib/torque/postgresql/predicate_builder/array_handler.rb +16 -8
  48. data/lib/torque/postgresql/predicate_builder/composite_handler.rb +122 -0
  49. data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +112 -0
  50. data/lib/torque/postgresql/predicate_builder/struct_handler.rb +88 -0
  51. data/lib/torque/postgresql/predicate_builder.rb +53 -0
  52. data/lib/torque/postgresql/predicate_table.rb +61 -0
  53. data/lib/torque/postgresql/railtie.rb +52 -0
  54. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -3
  55. data/lib/torque/postgresql/relation/buckets.rb +2 -1
  56. data/lib/torque/postgresql/relation/cross_join.rb +193 -0
  57. data/lib/torque/postgresql/relation/inheritance.rb +145 -28
  58. data/lib/torque/postgresql/relation/join_series.rb +1 -0
  59. data/lib/torque/postgresql/relation/merger.rb +30 -5
  60. data/lib/torque/postgresql/relation/segments.rb +140 -0
  61. data/lib/torque/postgresql/relation.rb +32 -12
  62. data/lib/torque/postgresql/schema_cache.rb +1 -0
  63. data/lib/torque/postgresql/validations.rb +29 -0
  64. data/lib/torque/postgresql/version.rb +1 -1
  65. data/lib/torque/postgresql/versioned_commands/command_migration.rb +24 -1
  66. data/lib/torque/postgresql/versioned_commands.rb +39 -0
  67. data/lib/torque/postgresql.rb +4 -0
  68. data/sig/generators/torque/function_generator.rbs +9 -0
  69. data/sig/generators/torque/skill_generator.rbs +7 -0
  70. data/sig/generators/torque/trigger_generator.rbs +9 -0
  71. data/sig/generators/torque/type_generator.rbs +9 -0
  72. data/sig/generators/torque/view_generator.rbs +9 -0
  73. data/sig/manifest.yaml +7 -0
  74. data/sig/torque/postgresql/adapter/database_statements.rbs +42 -0
  75. data/sig/torque/postgresql/adapter/inheritance_statements.rbs +36 -0
  76. data/sig/torque/postgresql/adapter/oid/array.rbs +20 -0
  77. data/sig/torque/postgresql/adapter/oid/box.rbs +28 -0
  78. data/sig/torque/postgresql/adapter/oid/circle.rbs +33 -0
  79. data/sig/torque/postgresql/adapter/oid/composite.rbs +35 -0
  80. data/sig/torque/postgresql/adapter/oid/enum.rbs +28 -0
  81. data/sig/torque/postgresql/adapter/oid/enum_set.rbs +19 -0
  82. data/sig/torque/postgresql/adapter/oid/interval.rbs +20 -0
  83. data/sig/torque/postgresql/adapter/oid/line.rbs +33 -0
  84. data/sig/torque/postgresql/adapter/oid/lquery.rbs +21 -0
  85. data/sig/torque/postgresql/adapter/oid/ltree.rbs +32 -0
  86. data/sig/torque/postgresql/adapter/oid/range.rbs +28 -0
  87. data/sig/torque/postgresql/adapter/oid/segment.rbs +38 -0
  88. data/sig/torque/postgresql/adapter/oid/struct.rbs +41 -0
  89. data/sig/torque/postgresql/adapter/oid/struct_list.rbs +20 -0
  90. data/sig/torque/postgresql/adapter/oid/struct_set.rbs +18 -0
  91. data/sig/torque/postgresql/adapter/oid.rbs +9 -0
  92. data/sig/torque/postgresql/adapter/quoting.rbs +20 -0
  93. data/sig/torque/postgresql/adapter/schema_creation.rbs +13 -0
  94. data/sig/torque/postgresql/adapter/schema_definitions.rbs +55 -0
  95. data/sig/torque/postgresql/adapter/schema_dumper.rbs +37 -0
  96. data/sig/torque/postgresql/adapter/schema_overrides.rbs +46 -0
  97. data/sig/torque/postgresql/adapter/schema_statements.rbs +50 -0
  98. data/sig/torque/postgresql/adapter.rbs +24 -0
  99. data/sig/torque/postgresql/arel/infix_operation.rbs +79 -0
  100. data/sig/torque/postgresql/arel/join_source.rbs +19 -0
  101. data/sig/torque/postgresql/arel/nodes.rbs +83 -0
  102. data/sig/torque/postgresql/arel/operations.rbs +28 -0
  103. data/sig/torque/postgresql/arel/select_manager.rbs +15 -0
  104. data/sig/torque/postgresql/arel/visitors.rbs +30 -0
  105. data/sig/torque/postgresql/arel.rbs +6 -0
  106. data/sig/torque/postgresql/associations/association_scope.rbs +28 -0
  107. data/sig/torque/postgresql/associations/belongs_to_many_association.rbs +51 -0
  108. data/sig/torque/postgresql/associations/builder/belongs_to_many.rbs +24 -0
  109. data/sig/torque/postgresql/associations/builder/has_many.rbs +15 -0
  110. data/sig/torque/postgresql/associations/builder.rbs +8 -0
  111. data/sig/torque/postgresql/associations/foreign_association.rbs +16 -0
  112. data/sig/torque/postgresql/associations/join_dependency.rbs +16 -0
  113. data/sig/torque/postgresql/associations/preloader/association.rbs +32 -0
  114. data/sig/torque/postgresql/associations/preloader/loader_query.rbs +20 -0
  115. data/sig/torque/postgresql/associations/preloader.rbs +8 -0
  116. data/sig/torque/postgresql/associations.rbs +17 -0
  117. data/sig/torque/postgresql/attributes/base.rbs +37 -0
  118. data/sig/torque/postgresql/attributes/builder/enum.rbs +38 -0
  119. data/sig/torque/postgresql/attributes/builder/full_text_search.rbs +27 -0
  120. data/sig/torque/postgresql/attributes/builder/period.rbs +106 -0
  121. data/sig/torque/postgresql/attributes/builder.rbs +19 -0
  122. data/sig/torque/postgresql/attributes/composite.rbs +28 -0
  123. data/sig/torque/postgresql/attributes/enum.rbs +69 -0
  124. data/sig/torque/postgresql/attributes/enum_set.rbs +75 -0
  125. data/sig/torque/postgresql/attributes/full_text_search.rbs +13 -0
  126. data/sig/torque/postgresql/attributes/lazy.rbs +17 -0
  127. data/sig/torque/postgresql/attributes/lquery.rbs +48 -0
  128. data/sig/torque/postgresql/attributes/ltree.rbs +46 -0
  129. data/sig/torque/postgresql/attributes/period.rbs +13 -0
  130. data/sig/torque/postgresql/attributes/simple_enum.rbs +39 -0
  131. data/sig/torque/postgresql/attributes/struct.rbs +38 -0
  132. data/sig/torque/postgresql/attributes.rbs +6 -0
  133. data/sig/torque/postgresql/autosave_association.rbs +16 -0
  134. data/sig/torque/postgresql/auxiliary_statement/recursive.rbs +22 -0
  135. data/sig/torque/postgresql/auxiliary_statement/settings.rbs +63 -0
  136. data/sig/torque/postgresql/auxiliary_statement.rbs +57 -0
  137. data/sig/torque/postgresql/base.rbs +41 -0
  138. data/sig/torque/postgresql/collector.rbs +7 -0
  139. data/sig/torque/postgresql/config.rbs +9 -0
  140. data/sig/torque/postgresql/function.rbs +21 -0
  141. data/sig/torque/postgresql/geometry_builder.rbs +20 -0
  142. data/sig/torque/postgresql/i18n.rbs +15 -0
  143. data/sig/torque/postgresql/inheritance/expander.rbs +23 -0
  144. data/sig/torque/postgresql/inheritance/record.rbs +20 -0
  145. data/sig/torque/postgresql/inheritance.rbs +47 -0
  146. data/sig/torque/postgresql/insert_all.rbs +22 -0
  147. data/sig/torque/postgresql/migration/command_recorder.rbs +30 -0
  148. data/sig/torque/postgresql/migration.rbs +6 -0
  149. data/sig/torque/postgresql/predicate_builder/arel_attribute_handler.rbs +15 -0
  150. data/sig/torque/postgresql/predicate_builder/array_handler.rbs +26 -0
  151. data/sig/torque/postgresql/predicate_builder/composite_handler.rbs +26 -0
  152. data/sig/torque/postgresql/predicate_builder/enumerator_lazy_handler.rbs +18 -0
  153. data/sig/torque/postgresql/predicate_builder/ltree_handler.rbs +36 -0
  154. data/sig/torque/postgresql/predicate_builder/regexp_handler.rbs +16 -0
  155. data/sig/torque/postgresql/predicate_builder/struct_handler.rbs +24 -0
  156. data/sig/torque/postgresql/predicate_builder.rbs +23 -0
  157. data/sig/torque/postgresql/predicate_table.rbs +24 -0
  158. data/sig/torque/postgresql/railtie.rbs +10 -0
  159. data/sig/torque/postgresql/reflection/abstract_reflection.rbs +22 -0
  160. data/sig/torque/postgresql/reflection/association_reflection.rbs +18 -0
  161. data/sig/torque/postgresql/reflection/belongs_to_many_reflection.rbs +30 -0
  162. data/sig/torque/postgresql/reflection/has_many_reflection.rbs +14 -0
  163. data/sig/torque/postgresql/reflection/runtime_reflection.rbs +18 -0
  164. data/sig/torque/postgresql/reflection/through_reflection.rbs +14 -0
  165. data/sig/torque/postgresql/reflection.rbs +11 -0
  166. data/sig/torque/postgresql/relation/auxiliary_statement.rbs +29 -0
  167. data/sig/torque/postgresql/relation/buckets.rbs +35 -0
  168. data/sig/torque/postgresql/relation/cross_join.rbs +63 -0
  169. data/sig/torque/postgresql/relation/distinct_on.rbs +16 -0
  170. data/sig/torque/postgresql/relation/inheritance.rbs +48 -0
  171. data/sig/torque/postgresql/relation/join_series.rbs +28 -0
  172. data/sig/torque/postgresql/relation/merger.rbs +23 -0
  173. data/sig/torque/postgresql/relation/segments.rbs +27 -0
  174. data/sig/torque/postgresql/relation.rbs +48 -0
  175. data/sig/torque/postgresql/schema_cache/bound_schema_reflection.rbs +14 -0
  176. data/sig/torque/postgresql/schema_cache/inheritance.rbs +12 -0
  177. data/sig/torque/postgresql/schema_cache/schema_reflection.rbs +14 -0
  178. data/sig/torque/postgresql/schema_cache.rbs +43 -0
  179. data/sig/torque/postgresql/table_name.rbs +20 -0
  180. data/sig/torque/postgresql/validations.rbs +13 -0
  181. data/sig/torque/postgresql/version.rbs +5 -0
  182. data/sig/torque/postgresql/versioned_commands/command_migration.rbs +54 -0
  183. data/sig/torque/postgresql/versioned_commands/generator.rbs +17 -0
  184. data/sig/torque/postgresql/versioned_commands/migration_context.rbs +30 -0
  185. data/sig/torque/postgresql/versioned_commands/migrator.rbs +19 -0
  186. data/sig/torque/postgresql/versioned_commands/schema_table.rbs +23 -0
  187. data/sig/torque/postgresql/versioned_commands.rbs +25 -0
  188. data/sig/torque/postgresql.rbs +26 -0
  189. data/skills/torque-postgresql/SKILL.md +70 -0
  190. data/spec/fixtures/migrations/20250101000008_create_trigger_audit_users_v1.sql +3 -0
  191. data/spec/initialize.rb +29 -2
  192. data/spec/mocks/cache_query.rb +13 -1
  193. data/spec/models/author.rb +1 -1
  194. data/spec/models/comment.rb +2 -0
  195. data/spec/models/place.rb +2 -0
  196. data/spec/models/profile.rb +31 -0
  197. data/spec/rbs_helper.rb +8 -0
  198. data/spec/schema.rb +35 -4
  199. data/spec/spec_helper.rb +1 -0
  200. data/spec/tests/arel_spec.rb +3 -3
  201. data/spec/tests/auxiliary_statement_spec.rb +12 -0
  202. data/spec/tests/buckets_spec.rb +122 -0
  203. data/spec/tests/composite_spec.rb +867 -0
  204. data/spec/tests/cross_join_spec.rb +185 -0
  205. data/spec/tests/distinct_on_spec.rb +5 -0
  206. data/spec/tests/enum_set_spec.rb +4 -0
  207. data/spec/tests/enum_spec.rb +20 -4
  208. data/spec/tests/geometric_builder_spec.rb +16 -5
  209. data/spec/tests/has_many_spec.rb +2 -1
  210. data/spec/tests/interval_spec.rb +6 -0
  211. data/spec/tests/ltree_spec.rb +886 -0
  212. data/spec/tests/predicate_builder_spec.rb +120 -0
  213. data/spec/tests/relation_spec.rb +25 -69
  214. data/spec/tests/segments_spec.rb +264 -0
  215. data/spec/tests/struct_spec.rb +1170 -0
  216. data/spec/tests/table_inheritance_spec.rb +1037 -56
  217. data/spec/tests/versioned_commands_spec.rb +145 -0
  218. metadata +198 -2
@@ -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_pool.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
@@ -66,20 +56,63 @@ module Torque
66
56
  # Get the list of all tables directly or indirectly dependent of the
67
57
  # current one
68
58
  def inheritance_dependents
69
- connection.schema_cache.associations(table_name) || []
59
+ connection_pool.schema_cache.associations(table_name) || []
70
60
  end
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_pool.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
@@ -88,8 +121,8 @@ module Torque
88
121
  table = super
89
122
 
90
123
  adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
91
- if Torque::PostgreSQL.config.eager_load && connection.is_a?(adapter)
92
- connection.schema_cache.add_model_name(table, self)
124
+ if Torque::PostgreSQL.config.eager_load && connection_pool.db_config.adapter_class <= adapter
125
+ connection_pool.schema_cache.add_model_name(table, self)
93
126
  end
94
127
 
95
128
  table
@@ -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)
@@ -25,7 +25,7 @@ module Torque
25
25
  private
26
26
 
27
27
  def array_typed?(attribute)
28
- attribute.able_to_type_cast? && attribute.type_caster.is_a?(ARRAY_OID)
28
+ attribute.able_to_type_cast? && Adapter::OID.unwrap(attribute.type_caster).is_a?(ARRAY_OID)
29
29
  end
30
30
  end
31
31
  end
@@ -12,13 +12,17 @@ module Torque
12
12
  end
13
13
 
14
14
  def call_for_array(attribute, value)
15
- if !value.is_a?(::Array)
16
- call_with_value(attribute, value)
17
- elsif value.any?
18
- call_with_array(attribute, value)
19
- else
20
- call_with_empty(attribute)
21
- end
15
+ return attribute.eq(nil) if value.nil?
16
+
17
+ value = value.to_a if value.is_a?(::Set)
18
+ return call_with_value(attribute, value) unless value.is_a?(::Array)
19
+ return call_with_empty(attribute) if value.empty?
20
+
21
+ nils, values = value.map { |entry| id_of(entry) }.partition(&:nil?)
22
+ return attribute.eq(nil) if values.empty?
23
+
24
+ condition = call_with_array(attribute, values)
25
+ nils.empty? ? condition : condition.or(attribute.eq(nil))
22
26
  end
23
27
 
24
28
  private
@@ -35,8 +39,12 @@ module Torque
35
39
  FN.cardinality(attribute).eq(0)
36
40
  end
37
41
 
42
+ def id_of(entry)
43
+ entry.is_a?(::ActiveRecord::Base) ? entry.id : entry
44
+ end
45
+
38
46
  def array_attribute?(attribute)
39
- attribute.type_caster.is_a?(ARRAY_OID)
47
+ Adapter::OID.unwrap(attribute.type_caster).is_a?(ARRAY_OID)
40
48
  end
41
49
  end
42
50
 
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module PredicateBuilder
6
+ # Turns a hash of columns into a proper set of conditions over a composite
7
+ # column, handing each of them back to the predicate builder so that the
8
+ # whole +where+ vocabulary works inside a composite
9
+ class CompositeHandler
10
+ class << self
11
+ # Values that this handler knows how to deal with. Without a value,
12
+ # only a single composite has parts that can be reached on their own
13
+ def candidate?(type, value = nil)
14
+ return false unless Adapter::OID::Composite.from(type)
15
+ return !type.is_a?(ARRAY_OID) if value.nil?
16
+
17
+ case value
18
+ when ::Hash, Attributes::Composite then true
19
+ when ::Array then value.compact.any? { |entry| candidate?(type, entry) }
20
+ else false
21
+ end
22
+ end
23
+
24
+ # Whether the value describes columns, instead of whole records
25
+ def columns?(value)
26
+ value.is_a?(::Hash) || (value.is_a?(::Array) && value.all?(::Hash))
27
+ end
28
+ end
29
+
30
+ def initialize(predicate_builder)
31
+ @predicate_builder = predicate_builder
32
+ end
33
+
34
+ def call(attribute, type, value)
35
+ @composite = Adapter::OID::Composite.from(type)
36
+ array = type.is_a?(ARRAY_OID)
37
+ record = !self.class.columns?(value)
38
+
39
+ return call_for_record(attribute, value, array) if record
40
+ return call_for_array(attribute, value) if array
41
+
42
+ conditions_for(attribute, value)
43
+ end
44
+
45
+ # The columns of the composite value as the columns of a table, so that
46
+ # any of them can be resolved by name
47
+ def table_for(source, type)
48
+ @composite = Adapter::OID::Composite.from(type)
49
+ PredicateTable.new(self, source, Arel::Nodes::Column)
50
+ end
51
+
52
+ # The type of a single column of the composite type
53
+ def type_of(name)
54
+ columns = @composite.columns
55
+ name = name.to_s
56
+
57
+ raise ArgumentError, <<~MSG.squish unless columns.key?(name)
58
+ Unable to build a condition for "#{name}" because it is not a
59
+ column of the "#{@composite.name}" composite type.
60
+ MSG
61
+
62
+ [columns[name]]
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :predicate_builder, :composite
68
+
69
+ # Whole records cannot be sent as anonymous binds, because the
70
+ # comparison operators resolve them to the +record+ pseudo type, so
71
+ # they are always casted to the type they belong to
72
+ def call_for_record(attribute, value, array)
73
+ name = attribute.name
74
+ cast = composite.name
75
+
76
+ if !array && value.is_a?(::Array)
77
+ records = value.map { |entry| FN.bind(name, entry, composite).pg_cast(cast) }
78
+ return attribute.in(records)
79
+ end
80
+
81
+ return attribute.eq(FN.bind(name, value, composite).pg_cast(cast)) unless array
82
+
83
+ if value.is_a?(::Array)
84
+ list = FN.bind(name, value, ARRAY_OID.new(composite))
85
+ return attribute.overlaps(list.pg_cast(cast, true))
86
+ end
87
+
88
+ entry = FN.bind(name, value, composite).pg_cast(cast)
89
+ FN.infix(:"=", entry, FN.any(attribute))
90
+ end
91
+
92
+ # Entries of an array of composite values can only be matched one by
93
+ # one, so the conditions are checked against each unnested entry
94
+ def call_for_array(attribute, value)
95
+ source = Arel::Nodes::Ref.new(composite.name)
96
+ entries = ::Arel::Nodes::TableAlias.new(FN.unnest(attribute), composite.name)
97
+
98
+ manager = ::Arel::SelectManager.new
99
+ manager.from(entries)
100
+ manager.project(::Arel.sql('1'))
101
+ manager.where(conditions_for(source, value))
102
+ manager.exists
103
+ end
104
+
105
+ # A list of values means that any of them is a match
106
+ def conditions_for(source, value)
107
+ return group_for(source, value) unless value.is_a?(::Array)
108
+
109
+ groups = value.map { |entry| group_for(source, entry) }
110
+ groups.reduce(:or)
111
+ end
112
+
113
+ def group_for(source, entry)
114
+ entry = entry.to_h unless entry.is_a?(::Hash)
115
+ table = table_for(source, composite)
116
+ nodes = predicate_builder.with(table).build_from_hash(entry.stringify_keys)
117
+ ::Arel::Nodes::Grouping.new(nodes.reduce(:and))
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module PredicateBuilder
6
+ # Serves every column of the ltree extension. A value of the same kind as
7
+ # the column is an equality, a value of the opposite kind is a match, and
8
+ # an Array is a list of values as soon as its first entry is a whole one
9
+ class LtreeHandler
10
+ class << self
11
+ def candidate?(type)
12
+ column_of(type).is_a?(Adapter::OID::Ltree)
13
+ end
14
+
15
+ def column_of(type)
16
+ type.is_a?(ARRAY_OID) ? type.subtype : type
17
+ end
18
+ end
19
+
20
+ def initialize(predicate_builder)
21
+ @predicate_builder = predicate_builder
22
+ end
23
+
24
+ def call(attribute, type, value)
25
+ @attribute = attribute
26
+ @column = self.class.column_of(type)
27
+ @array = type.is_a?(ARRAY_OID)
28
+ @list = list?(value)
29
+
30
+ values = list ? value : [value]
31
+ pattern = values.any? { |entry| lquery.cast(entry)&.pattern? }
32
+ pattern == lquery_column? ? equality(values) : match(values)
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :attribute, :column, :array, :list
38
+
39
+ def list?(value)
40
+ return false unless value.is_a?(::Array)
41
+
42
+ first = value.first
43
+ value.empty? || first.is_a?(::Array) || first.is_a?(LTree) || first.is_a?(LQuery)
44
+ end
45
+
46
+ def lquery_column?
47
+ column.is_a?(Adapter::OID::Lquery)
48
+ end
49
+
50
+ def lquery
51
+ Adapter::OID::Lquery.new
52
+ end
53
+
54
+ def ltree
55
+ Adapter::OID::Ltree.new
56
+ end
57
+
58
+ # Bound as the value the type produces, since Rails deep dups a raw
59
+ # value before compiling the query, and a dup of a record has no id
60
+ def bind(value, type)
61
+ FN.bind(attribute.name, type.cast(value), type)
62
+ end
63
+
64
+ # PostgreSQL defines no equality operator for lquery, so a pattern
65
+ # column is compared by its text form
66
+ def equality(values)
67
+ target = lquery_column? ? attribute.pg_cast('text', array) : attribute
68
+ return single_equality(target, bind(values.first, column)) unless list
69
+ return empty_equality(target) if values.empty?
70
+ return target.overlaps(bind(values, ARRAY_OID.new(column))) if array
71
+
72
+ binds = values.map { |entry| bind(entry, column) }
73
+ target.in(binds)
74
+ end
75
+
76
+ def single_equality(target, bind)
77
+ return target.eq(bind) unless array
78
+
79
+ FN.infix(:"=", bind, FN.any(target))
80
+ end
81
+
82
+ def empty_equality(target)
83
+ return target.in([]) unless array
84
+
85
+ FN.cardinality(attribute).eq(0)
86
+ end
87
+
88
+ def match(values)
89
+ other = lquery_column? ? ltree : lquery
90
+ cast = other.type.to_s
91
+ return single_match(bind(values.first, other).pg_cast(cast)) unless list
92
+
93
+ list_match(bind(values, ARRAY_OID.new(other)).pg_cast(cast, true))
94
+ end
95
+
96
+ # PostgreSQL defines ~ for every pair except lquery[] against ltree
97
+ def single_match(bind)
98
+ return attribute.matches_lquery(bind) unless array && lquery_column?
99
+
100
+ FN.infix(:"~", bind, FN.any(attribute))
101
+ end
102
+
103
+ # PostgreSQL defines ? for every pair except lquery against ltree[]
104
+ def list_match(binds)
105
+ return attribute.matches_any_lquery(binds) if array || !lquery_column?
106
+
107
+ FN.infix(:"~", attribute, FN.any(binds))
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end