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
@@ -6,13 +6,33 @@ module Torque
6
6
  module Inheritance
7
7
 
8
8
  # :nodoc:
9
- def cast_records_values
10
- @values.fetch(:cast_records, FROZEN_EMPTY_ARRAY)
9
+ def expand_records_values
10
+ @values.fetch(:expand_records, FROZEN_EMPTY_ARRAY)
11
11
  end
12
12
  # :nodoc:
13
- def cast_records_values=(value)
13
+ def expand_records_values=(value)
14
14
  assert_modifiable!
15
- @values[:cast_records] = value
15
+ @values[:expand_records] = value
16
+ end
17
+
18
+ # :nodoc:
19
+ def expand_records_eager_load_value
20
+ @values.fetch(:expand_records_eager_load, nil)
21
+ end
22
+ # :nodoc:
23
+ def expand_records_eager_load_value=(value)
24
+ assert_modifiable!
25
+ @values[:expand_records_eager_load] = value
26
+ end
27
+
28
+ # :nodoc:
29
+ def expand_records_scoped_value
30
+ @values.fetch(:expand_records_scoped, nil) || {}
31
+ end
32
+ # :nodoc:
33
+ def expand_records_scoped_value=(value)
34
+ assert_modifiable!
35
+ @values[:expand_records_scoped] = value
16
36
  end
17
37
 
18
38
  # :nodoc:
@@ -25,6 +45,8 @@ module Torque
25
45
  @values[:itself_only] = value
26
46
  end
27
47
 
48
+ RECORD_CLASS_TOKEN = :_regclass
49
+
28
50
  delegate :quote_table_name, :quote_column_name, to: :connection
29
51
 
30
52
  # Specify that the results should come only from the table that the
@@ -38,46 +60,90 @@ module Torque
38
60
 
39
61
  # Like #itself_only, but modifies relation in place.
40
62
  def itself_only!(*)
63
+ raise_itself_only_conflict! if expand_records_values.present?
64
+
41
65
  self.itself_only_value = true
42
66
  self
43
67
  end
44
68
 
45
- # Enables the casting of all returned records. The result will include
46
- # all the information needed to instantiate the inherited models
69
+ # Load the columns that only exist on the inherited tables, so that
70
+ # records come out complete and writable. Defaults to every dependent
71
+ # that adds columns
72
+ #
73
+ # Activity.expand_records
74
+ # # Runs one additional query per inherited table
47
75
  #
48
- # Activity.cast_records
49
- # # The result list will have many different classes, for all
50
- # # inherited models of activities
51
- def cast_records(*types, **options)
52
- spawn.cast_records!(*types, **options)
76
+ # Activity.expand_records(ActivityBook, eager_load: true)
77
+ # # Runs a single query using outer joins
78
+ def expand_records(*types, **options)
79
+ spawn.expand_records!(*types, **options)
53
80
  end
54
81
 
55
- # Like #cast_records, but modifies relation in place
56
- def cast_records!(*types, **options)
57
- where!(regclass.pg_cast(:varchar).in(types.map(&:table_name))) if options[:filter]
58
- self.select_extra_values += [regclass.as(_record_class_attribute.to_s)]
59
- self.cast_records_values = (types.present? ? types : model.casted_dependents.values)
82
+ # Like #expand_records, but modifies relation in place
83
+ def expand_records!(*types, eager_load: false, filter: false)
84
+ raise_itself_only_conflict! if itself_only_value === true
85
+
86
+ types = types.presence || model.inheritance_expandable_dependents.values
87
+
88
+ where!(regclass.pg_cast(:varchar).in(types.map(&:table_name))) if filter
89
+ self.expand_records_values = types
90
+ self.expand_records_eager_load_value = eager_load
60
91
  self
61
92
  end
62
93
 
94
+ # Accept the record class marker as a column, which is the only way an
95
+ # explicit selection can still produce casted records
96
+ #
97
+ # Activity.select(:_regclass, :id, :title)
98
+ def select(*fields)
99
+ return super if fields.empty?
100
+ return super(*fields, build_record_class_marker) if fields.delete(RECORD_CLASS_TOKEN)
101
+
102
+ super
103
+ end
104
+
63
105
  private
64
106
 
107
+ def raise_itself_only_conflict!
108
+ raise InheritanceError.new(<<~MSG.squish)
109
+ Reading from ONLY a table never returns records from its
110
+ inherited tables, so itself_only and expand_records cannot be
111
+ combined.
112
+ MSG
113
+ end
114
+
65
115
  # Hook arel build to add any necessary table
66
116
  def build_arel(*)
67
117
  arel = super
68
118
  arel.only if self.itself_only_value === true
69
- build_inheritances(arel)
119
+
120
+ arel.project(build_record_class_marker) if inheritance_discriminated?
121
+ build_inheritances(arel) if self.expand_records_eager_load_value
70
122
  arel
71
123
  end
72
124
 
125
+ def inheritance_discriminated?
126
+ return false if self.itself_only_value === true
127
+ return false if select_values.present?
128
+ return false unless from_clause.empty?
129
+
130
+ model.physically_inheritances?
131
+ end
132
+
133
+ def build_record_class_marker
134
+ regclass.as(_record_class_column_name)
135
+ end
136
+
73
137
  # Build all necessary data for inheritances
74
138
  def build_inheritances(arel)
75
- return if self.cast_records_values.empty?
139
+ return if self.expand_records_values.empty?
76
140
 
77
- mergeable = inheritance_mergeable_attributes
141
+ columns = build_inheritances_joins(arel, self.expand_records_values)
142
+ # The joins are still needed, but an explicit select owns the projection
143
+ return if columns.empty? || select_values.present?
78
144
 
79
- columns = build_inheritances_joins(arel, self.cast_records_values)
80
- columns = columns.map do |column, arel_tables|
145
+ mergeable = inheritance_mergeable_attributes
146
+ projections = columns.map do |column, arel_tables|
81
147
  next arel_tables.first[column] if arel_tables.size == 1
82
148
 
83
149
  if mergeable.include?(column)
@@ -87,8 +153,7 @@ module Torque
87
153
  end
88
154
  end
89
155
 
90
- columns.push(build_auto_caster_marker(arel, self.cast_records_values))
91
- self.select_extra_values += columns.flatten if columns.any?
156
+ arel.project(*projections.flatten)
92
157
  end
93
158
 
94
159
  # Build as many left outer join as necessary for each dependent table
@@ -111,15 +176,67 @@ module Torque
111
176
  columns
112
177
  end
113
178
 
114
- def build_auto_caster_marker(arel, types)
115
- attribute = regclass.pg_cast(:varchar).in(types.map(&:table_name))
116
- attribute.as(self.class._auto_cast_attribute.to_s)
117
- end
118
-
119
179
  def regclass
120
180
  arel_table['tableoid'].pg_cast(:regclass)
121
181
  end
122
182
 
183
+ module Expansion
184
+ def preload_associations(records)
185
+ super
186
+ return if expand_records_scoped_value.blank?
187
+
188
+ expand_records_scoped_value.each do |base_model, targets|
189
+ preloaded_association_names.each do |name|
190
+ reflection = model.reflect_on_association(name)
191
+ next if reflection.nil? || reflection.polymorphic? || !(reflection.klass <= base_model)
192
+
193
+ loaded = records.flat_map { |record| record.association(name).target }
194
+ PostgreSQL::Inheritance::Expander.new(reflection.klass, loaded.compact, targets).call
195
+ end
196
+ end
197
+ end
198
+
199
+ private
200
+
201
+ # Records only carry the columns of the queried table, so
202
+ # expanding has to happen after they have been instantiated
203
+ def exec_queries
204
+ records = super
205
+ warn_about_missing_record_class(records)
206
+ return records if expand_records_values.empty? || expand_records_eager_load_value
207
+
208
+ PostgreSQL::Inheritance::Expander.new(model, records, expand_records_values).call
209
+ records.each(&:readonly!) if readonly_value
210
+ records
211
+ end
212
+
213
+ def preloaded_association_names
214
+ list = preload_values + (eager_loading? ? [] : includes_values)
215
+ list.flat_map { |entry| entry.is_a?(Hash) ? entry.keys : entry }
216
+ end
217
+
218
+ # Warn once per query that actually lost the real class, instead of
219
+ # once per explicit select that merely omits the marker
220
+ def warn_about_missing_record_class(records)
221
+ return unless model.physically_inheritances?
222
+ return if itself_only_value === true
223
+ return if records.empty? || group_values.present? || distinct_value || select_values.empty?
224
+ return unless from_clause.empty?
225
+ return if select_values.any? { |value| record_class_marker?(value) }
226
+
227
+ warn(<<~MSG.squish)
228
+ #{model.name} was queried with an explicit select that omits
229
+ :_regclass, so its records will not be instantiated as their
230
+ real class.
231
+ MSG
232
+ end
233
+
234
+ def record_class_marker?(value)
235
+ return value.right.to_s == _record_class_column_name if value.is_a?(::Arel::Nodes::As)
236
+ value.is_a?(String) && value.include?(_record_class_column_name)
237
+ end
238
+
239
+ end
123
240
  end
124
241
  end
125
242
  end
@@ -87,6 +87,7 @@ module Torque
87
87
 
88
88
  return yield(result, relation.arel_table) if block_given?
89
89
 
90
+ with = relation.resolve_column(with).first if with.is_a?(Hash)
90
91
  with = relation.arel_table[with.to_s] if with.is_a?(Symbol)
91
92
  with = with.pg_cast(cast) if cast && with.respond_to?(:pg_cast)
92
93
 
@@ -13,6 +13,7 @@ module Torque
13
13
  merge_auxiliary_statements
14
14
  merge_inheritance
15
15
  merge_buckets
16
+ merge_segments
16
17
 
17
18
  relation
18
19
  end
@@ -46,18 +47,34 @@ module Torque
46
47
  end
47
48
  end
48
49
 
49
- # Merge settings related to inheritance tables
50
+ # Merge settings related to inheritance tables, going through the
51
+ # public operations so that their conflicts are still detected
50
52
  def merge_inheritance
51
53
  return unless relation.is_a?(Relation::Inheritance)
52
54
 
53
- relation.itself_only_value = true if other.itself_only_value.present?
55
+ relation.itself_only! if other.itself_only_value.present?
54
56
 
55
- if other.cast_records_values.present?
56
- relation.cast_records_values += other.cast_records_values
57
- relation.cast_records_values.uniq!
57
+ return if other.expand_records_values.blank?
58
+
59
+ if relation.model == other.model
60
+ types = (relation.expand_records_values + other.expand_records_values).uniq
61
+ relation.expand_records!(*types, eager_load: other.expand_records_eager_load_value)
62
+ else
63
+ raise_eager_load_conflict! if other.expand_records_eager_load_value
64
+
65
+ relation.expand_records_scoped_value = relation.expand_records_scoped_value
66
+ .merge(other.model => other.expand_records_values) { |_, a, b| (a + b).uniq }
58
67
  end
59
68
  end
60
69
 
70
+ def raise_eager_load_conflict!
71
+ raise InheritanceError.new(<<~MSG.squish)
72
+ Expanding the records of a different model happens through a
73
+ preload, which has no query to eager load the inherited tables
74
+ into, so the two cannot be merged.
75
+ MSG
76
+ end
77
+
61
78
  # Merge settings related to buckets
62
79
  def merge_buckets
63
80
  return unless defined?(Relation::Buckets) && relation.is_a?(Relation::Buckets)
@@ -66,6 +83,14 @@ module Torque
66
83
  relation.buckets_value = other.buckets_value
67
84
  end
68
85
 
86
+ # Merge the named conditions used to segment calculations
87
+ def merge_segments
88
+ return unless defined?(Relation::Segments) && relation.is_a?(Relation::Segments)
89
+ return if other.segments_value.blank?
90
+
91
+ relation.segments_value = relation.segments_value.merge(other.segments_value)
92
+ end
93
+
69
94
  end
70
95
 
71
96
  ActiveRecord::Relation::Merger.prepend Merger
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Relation
6
+ module Segments
7
+
8
+ # :nodoc:
9
+ def segments_value
10
+ @values.fetch(:segments, ::ActiveRecord::QueryMethods::FROZEN_EMPTY_HASH)
11
+ end
12
+
13
+ # :nodoc:
14
+ def segments_value=(value)
15
+ assert_modifiable!
16
+ @values[:segments] = value
17
+ end
18
+
19
+ # Maps names to conditions so that the next calculation returns one
20
+ # value per name, each aggregate filtered by its condition. Anything
21
+ # that can become a relation is accepted as a condition, an array
22
+ # applies each of its items in order, and +nil+ means the aggregate is
23
+ # not filtered at all. For example:
24
+ #
25
+ # User.segments(all: nil, adults: { age: 18.. }).count
26
+ # # Returns { all: 15, adults: 12 }
27
+ #
28
+ # User.group(:role).segments(adults: { age: 18.. }).sum(:age)
29
+ # # Returns { 'visitor' => { adults: 480 }, ... }
30
+ def segments(**map)
31
+ spawn.segments!(**map)
32
+ end
33
+
34
+ # Like #segments, but modifies relation in place.
35
+ def segments!(**map)
36
+ predicates = map.to_h { |name, condition| [name, segment_predicate_for(name, condition)] }
37
+ self.segments_value = segments_value.merge(predicates)
38
+ self
39
+ end
40
+
41
+ private
42
+
43
+ def execute_simple_calculation(operation, column_name, distinct)
44
+ return super if segments_value.blank?
45
+
46
+ execute_segments_calculation(operation, column_name, distinct)
47
+ end
48
+
49
+ def execute_grouped_calculation(operation, column_name, distinct)
50
+ return super if segments_value.blank?
51
+
52
+ execute_segments_calculation(operation, column_name, distinct)
53
+ end
54
+
55
+ def execute_segments_calculation(operation, column_name, distinct)
56
+ column_name = primary_key if operation == 'count' && column_name == :all && distinct
57
+ column = aggregate_column(column_name)
58
+ aggregate = operation_over_aggregate_column(column, operation, distinct)
59
+
60
+ relation = except(:group, :select).distinct!(false)
61
+ relation.order_values = [] if group_values.empty?
62
+
63
+ groups = relation.arel_columns(group_values)
64
+ relation.group_values = groups
65
+
66
+ result = skip_query_cache_if_necessary do
67
+ klass.with_connection do |connection|
68
+ relation.select_values = segments_projections(groups, aggregate, connection)
69
+ connection.select_all(relation.arel, "#{klass.name} Segments", async: @async)
70
+ end
71
+ end
72
+
73
+ result.then { |data| segments_result(data, groups, column, operation) }
74
+ end
75
+
76
+ def segments_projections(groups, aggregate, connection)
77
+ tracker = ::ActiveRecord::Calculations::ColumnAliasTracker.new(connection)
78
+ projections = groups.map do |group|
79
+ name = tracker.alias_for(connection.visitor.compile(group).downcase)
80
+ group.as(connection.quote_column_name(name))
81
+ end
82
+
83
+ segments_value.each_with_object(projections) do |(name, predicate), list|
84
+ node = predicate.nil? ? aggregate.dup : aggregate.filter(predicate)
85
+ list << node.as(connection.quote_column_name(name.to_s))
86
+ end
87
+ end
88
+
89
+ def segments_result(data, groups, column, operation)
90
+ type = column.try(:type_caster) || ::ActiveRecord::Type.default_value
91
+ type = type.subtype if ::ActiveRecord::Enum::EnumType === type
92
+
93
+ key_types = groups.each_with_index.to_h do |group, index|
94
+ name = data.columns[index]
95
+ key_type = group.try(:type_caster)
96
+ key_type ||= type_for(group) { data.column_types.fetch(name, ::ActiveRecord::Type.default_value) }
97
+ [name, key_type]
98
+ end
99
+
100
+ rows = data.cast_values(key_types)
101
+ rows = rows.map { |value| [value] } if data.columns.size == 1
102
+
103
+ rows = rows.to_h do |row|
104
+ key = row.take(groups.size)
105
+ key = key.first if key.size == 1
106
+
107
+ values = row.drop(groups.size).map do |value|
108
+ type_cast_calculated_value(value, operation, type)
109
+ end
110
+
111
+ [key, segments_value.keys.zip(values).to_h]
112
+ end
113
+
114
+ groups.empty? ? rows.values.first : rows
115
+ end
116
+
117
+ def segment_predicate_for(name, condition)
118
+ return if !condition.is_a?(::ActiveRecord::Relation) && condition.blank?
119
+
120
+ items = condition.is_a?(::Array) ? condition : [condition]
121
+ relation = items.reduce(klass.unscoped) do |result, item|
122
+ case item
123
+ when ::ActiveRecord::Relation then result.merge(item)
124
+ when Symbol then result.public_send(item)
125
+ when Proc then item.arity.zero? ? result.instance_exec(&item) : item.call(result)
126
+ else result.where(item)
127
+ end
128
+ end
129
+
130
+ raise ArgumentError, <<~MSG.squish if relation.where_clause.empty?
131
+ Segment #{name} does not filter anything, use nil for an unfiltered segment.
132
+ MSG
133
+
134
+ relation.where_clause.ast
135
+ end
136
+
137
+ end
138
+ end
139
+ end
140
+ end
@@ -13,9 +13,11 @@ module Torque
13
13
  include DistinctOn
14
14
  include Inheritance
15
15
 
16
- SINGLE_VALUE_METHODS = %i[itself_only buckets]
16
+ SINGLE_VALUE_METHODS = %i[
17
+ itself_only buckets segments expand_records_eager_load expand_records_scoped
18
+ ]
17
19
  MULTI_VALUE_METHODS = %i[
18
- select_extra distinct_on auxiliary_statements cast_records
20
+ select_extra distinct_on auxiliary_statements expand_records
19
21
  ]
20
22
 
21
23
  VALUE_METHODS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS
@@ -76,7 +78,11 @@ module Torque
76
78
  return unless relation
77
79
 
78
80
  table = predicate_builder.send(:table)
79
- if table.associated_with?(relation.to_s)
81
+ part = part_table_for(table, relation.to_s)
82
+ return part if part
83
+
84
+ associated = AR810 ? table.associated_with(relation.to_s) : table.associated_with?(relation.to_s)
85
+ if associated
80
86
  table.associated_table(relation.to_s).send(:klass)
81
87
  else
82
88
  raise ArgumentError, "Relation for #{relation} not found on #{klass}"
@@ -87,8 +93,11 @@ module Torque
87
93
  # the given column
88
94
  def cast_for_condition(column, value)
89
95
  column = columns_hash[column.to_s] unless column.is_a?(ARColumn)
90
- caster = connection.lookup_cast_type_from_column(column)
91
- connection.type_cast(caster.serialize(value))
96
+
97
+ klass.with_connection do |connection|
98
+ caster = connection.lookup_cast_type_for_column(column)
99
+ connection.type_cast(caster.serialize(value))
100
+ end
92
101
  end
93
102
 
94
103
  private
@@ -99,6 +108,16 @@ module Torque
99
108
  arel
100
109
  end
101
110
 
111
+ def part_table_for(table, name)
112
+ return unless table.has_column?(name)
113
+
114
+ type = Adapter::OID.unwrap(table.type(name))
115
+ handler = PredicateBuilder.handler_for(type)
116
+ return if handler.nil?
117
+
118
+ handler.new(predicate_builder).table_for(table.arel_table[name], type)
119
+ end
120
+
102
121
  class_methods do
103
122
  # Easy and storable way to access the name used to get the record table
104
123
  # name when using inheritance tables
@@ -107,11 +126,10 @@ module Torque
107
126
  .inheritance.record_class_column_name.to_sym
108
127
  end
109
128
 
110
- # Easy and storable way to access the name used to get the indicate of
111
- # auto casting inherited records
112
- def _auto_cast_attribute
113
- @@auto_cast ||= Torque::PostgreSQL.config
114
- .inheritance.auto_cast_column_name.to_sym
129
+ # The same name as a string, which is how it arrives on every single
130
+ # row that has to be discriminated
131
+ def _record_class_column_name
132
+ @@record_class_column_name ||= _record_class_attribute.to_s.freeze
115
133
  end
116
134
  end
117
135
 
@@ -138,12 +156,14 @@ module Torque
138
156
  # the operation of ActiveRecord Relation
139
157
  ActiveRecord::Relation.include Relation
140
158
  ActiveRecord::Relation.prepend Relation::Initializer
159
+ ActiveRecord::Relation.prepend Relation::Inheritance::Expansion
141
160
 
142
161
  ActiveRecord::Relation::SINGLE_VALUE_METHODS.concat(Relation::SINGLE_VALUE_METHODS)
143
162
  ActiveRecord::Relation::MULTI_VALUE_METHODS.concat(Relation::MULTI_VALUE_METHODS)
144
163
  ActiveRecord::Relation::VALUE_METHODS.concat(Relation::VALUE_METHODS)
145
- ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES.merge(%i[cast_records itself_only
146
- distinct_on auxiliary_statements buckets])
164
+ ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES.merge(%i[expand_records itself_only
165
+ expand_records_eager_load expand_records_scoped distinct_on auxiliary_statements buckets
166
+ segments])
147
167
 
148
168
  end
149
169
  end
@@ -74,6 +74,7 @@ module Torque
74
74
  @data_sources_model_names.delete name
75
75
  @inheritance_dependencies.delete name
76
76
  @inheritance_associations.delete name
77
+ @inheritance_loaded = false
77
78
  end
78
79
 
79
80
  def marshal_dump # :nodoc:
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Validations
6
+
7
+ # Checks if the objects nested into an attribute are valid, adding a
8
+ # single error to the attribute when any of them is not. It takes the
9
+ # regular +allow_nil+ and +allow_blank+ options, which is how values that
10
+ # are merely absent are left out of it
11
+ #
12
+ # Example:
13
+ # validates :home, nested: true
14
+ # validates :settings, nested: true, allow_blank: true
15
+ class NestedValidator < ActiveModel::EachValidator
16
+ def validate_each(record, attribute, value)
17
+ invalid = Array.wrap(value).any? do |item|
18
+ item.respond_to?(:invalid?) && item.invalid?
19
+ end
20
+
21
+ record.errors.add(attribute, :invalid) if invalid
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ ::ActiveRecord::Base.include(Validations)
28
+ end
29
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- VERSION = '4.0.1'
5
+ VERSION = '4.1.1'
6
6
  end
7
7
  end
@@ -62,7 +62,7 @@ module Torque
62
62
  end
63
63
  end
64
64
 
65
- CommandMigration = Struct.new(*%i[filename version op type object_name op_version scope]) do
65
+ CommandMigration = ::Struct.new(*%i[filename version op type object_name op_version scope]) do
66
66
  delegate :execute, to: '@migration'
67
67
 
68
68
  def initialize(filename, *args)
@@ -131,6 +131,29 @@ module Torque
131
131
  execute "DROP TYPE #{name.first.first};"
132
132
  end
133
133
 
134
+ # Drop the trigger and any function bundled with it
135
+ def drop_trigger
136
+ content = File.read(filename)
137
+ trigger, table = content.scan(Regexp.new([
138
+ '^\s*CREATE\s+(?:OR\s+REPLACE\s+)?',
139
+ "TRIGGER\\s+#{NAME_MATCH}",
140
+ '.*?\bON\s+',
141
+ NAME_MATCH,
142
+ ].join, 'mi')).first
143
+
144
+ execute "DROP TRIGGER #{trigger} ON #{table};"
145
+
146
+ definitions = content.scan(Regexp.new([
147
+ '^\s*CREATE\s+(?:OR\s+REPLACE\s+)?',
148
+ "FUNCTION\\s+#{NAME_MATCH}",
149
+ '\s*(\([_a-z0-9 ,]*\))?',
150
+ ].join, 'mi'))
151
+ return if definitions.empty?
152
+
153
+ functions = definitions.map(&:join).join(', ')
154
+ execute "DROP FUNCTION #{functions};"
155
+ end
156
+
134
157
  # Drop view or materialized view
135
158
  def drop_view
136
159
  mat, name = File.read(filename).scan(Regexp.new([
@@ -117,6 +117,45 @@ module Torque
117
117
  MSG
118
118
  end
119
119
 
120
+ # Validate that the content of the command is correct
121
+ def validate_trigger!(content, name)
122
+ result = content.scan(Regexp.new([
123
+ '^\s*CREATE\s+(OR\s+REPLACE)?\s*',
124
+ "TRIGGER\\s+#{NAME_MATCH}",
125
+ ].join, 'mi'))
126
+
127
+ raise ArgumentError, <<~MSG.squish if result.empty?
128
+ Missing or invalid trigger definition.
129
+ MSG
130
+
131
+ raise ArgumentError, <<~MSG.squish if result.size > 1
132
+ More than one trigger definition found.
133
+ MSG
134
+
135
+ raise ArgumentError, <<~MSG.squish unless result.all?(&:first)
136
+ 'OR REPLACE' is required for proper migration support.
137
+ MSG
138
+
139
+ trigger_name = result.first.last.downcase.sub('.', '_')
140
+ raise ArgumentError, <<~MSG.squish if trigger_name != name.downcase
141
+ Trigger name must match file name.
142
+ MSG
143
+
144
+ functions = content.scan(Regexp.new([
145
+ '^\s*CREATE\s+(OR\s+REPLACE)?\s*',
146
+ "FUNCTION\\s+#{NAME_MATCH}",
147
+ ].join, 'mi'))
148
+ return if functions.empty?
149
+
150
+ raise ArgumentError, <<~MSG.squish if functions.map(&:last).uniq(&:downcase).many?
151
+ Only one function can be bundled with a trigger.
152
+ MSG
153
+
154
+ raise ArgumentError, <<~MSG.squish unless functions.all?(&:first)
155
+ 'OR REPLACE' is required for proper migration support.
156
+ MSG
157
+ end
158
+
120
159
  # Validate that the content of the command is correct
121
160
  def validate_view!(content, name)
122
161
  result = content.scan(Regexp.new([