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
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module PredicateBuilder
6
+ # Turns a hash of properties into a proper set of conditions over the
7
+ # document that holds them, handing each of them back to the predicate
8
+ # builder so that the whole +where+ vocabulary works inside a struct
9
+ class StructHandler
10
+ # Properties are extracted as text, so only the ones whose type has a
11
+ # counterpart on the database are casted before being compared
12
+ PLAIN_TYPES = %i[string text].freeze
13
+
14
+ class << self
15
+ # Only a hash describes properties, everything else is left alone so
16
+ # that whole documents are still compared as documents. Without a
17
+ # value, a single document has properties that can be reached
18
+ def candidate?(type, value = nil)
19
+ return false if !type.is_a?(Adapter::OID::Struct) || type.is_a?(Adapter::OID::StructList)
20
+
21
+ value.nil? || value.is_a?(::Hash)
22
+ end
23
+ end
24
+
25
+ def initialize(predicate_builder)
26
+ @predicate_builder = predicate_builder
27
+ end
28
+
29
+ def call(attribute, struct, value)
30
+ table = table_for(attribute, struct)
31
+ nodes = predicate_builder.with(table).build_from_hash(value.stringify_keys)
32
+
33
+ ::Arel::Nodes::Grouping.new(nodes.reduce(:and))
34
+ end
35
+
36
+ # The properties of the document as the columns of a table, so that any
37
+ # of them can be resolved by name. A document that no class describes
38
+ # is open, which means any path into it is accepted, without a type
39
+ def table_for(attribute, struct)
40
+ raise ArgumentError, <<~MSG.squish if struct.is_a?(Adapter::OID::Struct) && struct.type != :jsonb
41
+ Unable to build a condition over "#{attribute.name}" because json
42
+ columns cannot be queried by their properties. Use jsonb instead.
43
+ MSG
44
+
45
+ @struct = struct
46
+ PredicateTable.new(self, attribute, Arel::Nodes::Property)
47
+ end
48
+
49
+ # The node for a path into the document, resolved one property at a
50
+ # time so that each level is typed by the class that declares it
51
+ def property_for(attribute, struct, path)
52
+ path.reduce(attribute) do |node, key|
53
+ struct = node.type_caster if node.is_a?(Arel::Nodes::Property)
54
+ table_for(node, struct)[key]
55
+ end
56
+ end
57
+
58
+ # The type of a single property, plus what it has to be casted to once
59
+ # it is extracted from the document as text
60
+ def type_of(name)
61
+ return [ActiveModel::Type.default_value, nil] unless @struct.is_a?(Adapter::OID::Struct)
62
+
63
+ klass = @struct.klass
64
+ name = name.to_s
65
+
66
+ raise ArgumentError, <<~MSG.squish if klass.strict? && !klass.attribute_names.include?(name)
67
+ Unable to build a condition for "#{name}" because it is not a
68
+ declared property of #{klass.name}.
69
+ MSG
70
+
71
+ type = klass.attribute_types[name]
72
+ [type, cast_for(type)]
73
+ end
74
+
75
+ private
76
+
77
+ attr_reader :predicate_builder
78
+
79
+ def cast_for(type)
80
+ name = type&.type
81
+ return if name.nil? || PLAIN_TYPES.include?(name)
82
+
83
+ ActiveRecord::Base.with_connection { |c| c.type_to_sql(name) }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require_relative 'predicate_builder/array_handler'
4
4
 
5
+ require_relative 'predicate_builder/composite_handler'
6
+ require_relative 'predicate_builder/ltree_handler'
7
+ require_relative 'predicate_builder/struct_handler'
5
8
  require_relative 'predicate_builder/regexp_handler'
6
9
  require_relative 'predicate_builder/arel_attribute_handler'
7
10
  require_relative 'predicate_builder/enumerator_lazy_handler'
@@ -11,6 +14,20 @@ module Torque
11
14
  module PredicateBuilder
12
15
  ARRAY_OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
13
16
 
17
+ # The handler that serves a column, either to build a condition with the
18
+ # given value or, without one, to resolve the parts of the column. The
19
+ # ltree handler is picked by the column alone, composite and struct also
20
+ # look at the value: a Hash means the parts, anything else the whole
21
+ def self.handler_for(type, value = nil)
22
+ if PostgreSQL.config.composite.enabled && CompositeHandler.candidate?(type, value)
23
+ CompositeHandler
24
+ elsif PostgreSQL.config.struct.enabled && StructHandler.candidate?(type, value)
25
+ StructHandler
26
+ elsif PostgreSQL.config.ltree.enabled && !value.nil? && LtreeHandler.candidate?(type)
27
+ LtreeHandler
28
+ end
29
+ end
30
+
14
31
  def initialize(*)
15
32
  super
16
33
 
@@ -28,6 +45,42 @@ module Torque
28
45
  register_handler(::Arel::Attributes::Attribute, ArelAttributeHandler.new(self))
29
46
  end
30
47
  end
48
+
49
+ # Values described as a hash of columns or properties are turned into
50
+ # conditions over the column that holds them. Whole values are left to
51
+ # the regular handlers, so that they are compared as a single value, and
52
+ # so is nil, which is a null check on every kind of column
53
+ def build(attribute, value, operator = nil)
54
+ return super if value.nil?
55
+
56
+ type = Adapter::OID.unwrap(table.type(attribute.name))
57
+ handler = PredicateBuilder.handler_for(type, value) || handler_for_path(attribute, value)
58
+ return super if handler.nil?
59
+
60
+ handler.new(self).call(attribute, type, value)
61
+ end
62
+
63
+ # A struct or composite column stands for the table of its own parts, so
64
+ # a Hash given to order, group or pluck reaches a part the way where does
65
+ def resolve_arel_attribute(table_name, column_name, &block)
66
+ return super unless table.has_column?(table_name)
67
+
68
+ type = Adapter::OID.unwrap(table.type(table_name))
69
+ handler = PredicateBuilder.handler_for(type)
70
+ return super if handler.nil?
71
+
72
+ handler.new(self).table_for(table.arel_table[table_name], type)[column_name]
73
+ end
74
+
75
+ private
76
+
77
+ # A Hash beneath a property that is not a document of its own is just
78
+ # a deeper path, the way jsonb reads it, which is how an entry of an
79
+ # array is reached
80
+ def handler_for_path(attribute, value)
81
+ return unless PostgreSQL.config.struct.enabled
82
+ StructHandler if attribute.is_a?(Arel::Nodes::Property) && value.is_a?(::Hash)
83
+ end
31
84
  end
32
85
 
33
86
  ::ActiveRecord::PredicateBuilder.prepend(PredicateBuilder)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ # Mimics ActiveRecord::TableMetadata so that the parts of a value that holds
6
+ # other values, like the columns of a composite type or the properties of a
7
+ # document, can be treated as the columns of the value they belong to. That
8
+ # is what allows each of them to be handed back to the predicate builder
9
+ #
10
+ # The handler is the one that knows how to resolve a name, and the node is
11
+ # the one that knows how to reach it
12
+ class PredicateTable
13
+ def initialize(handler, source, node)
14
+ @handler = handler
15
+ @source = source
16
+ @node = node
17
+ end
18
+
19
+ def arel_table
20
+ self
21
+ end
22
+
23
+ def [](name)
24
+ @node.new(@source, name, *@handler.type_of(name))
25
+ end
26
+
27
+ def type(name)
28
+ @handler.type_of(name).first
29
+ end
30
+
31
+ # Anything that gets here is meant to be resolved as a part of the value,
32
+ # which is what +type_of+ is there to accept or reject
33
+ def has_column?(*)
34
+ true
35
+ end
36
+
37
+ def primary_key
38
+ nil
39
+ end
40
+
41
+ # Rails 8.1 renamed this, so both names answer for the supported versions
42
+ def associated_with(*)
43
+ false
44
+ end
45
+
46
+ alias associated_with? associated_with
47
+
48
+ def aggregated_with?(*)
49
+ false
50
+ end
51
+
52
+ def polymorphic_association?
53
+ false
54
+ end
55
+
56
+ def through_association?
57
+ false
58
+ end
59
+ end
60
+ end
61
+ end
@@ -30,6 +30,20 @@ module Torque
30
30
  Relation.include(Relation::Buckets)
31
31
  end
32
32
 
33
+ if torque_config.segments
34
+ require_relative 'relation/segments'
35
+ Relation.include(Relation::Segments)
36
+ end
37
+
38
+ ## Cross Join Setup
39
+ if torque_config.cross_join.enabled
40
+ require_relative 'relation/cross_join'
41
+ Relation.include(Relation::CrossJoin)
42
+
43
+ # TODO: delegate join_struct and join_composite once they exist
44
+ ActiveRecord::Base.singleton_class.delegate(:cross_join, to: :all)
45
+ end
46
+
33
47
  ## Schemas Enabled Setup
34
48
  if (config = torque_config.schemas).enabled
35
49
  require_relative 'adapter/schema_overrides'
@@ -85,6 +99,32 @@ module Torque
85
99
  end
86
100
  end
87
101
 
102
+ ## Struct Enabled Setup
103
+ if (config = torque_config.struct).enabled
104
+ require_relative 'adapter/oid/struct'
105
+ require_relative 'adapter/oid/struct_list'
106
+ require_relative 'adapter/oid/struct_set'
107
+
108
+ require_relative 'attributes/struct'
109
+
110
+ Attributes::Struct.include_on(ActiveRecord::Base)
111
+ end
112
+
113
+ ## Composite Enabled Setup
114
+ if (config = torque_config.composite).enabled
115
+ require_relative 'adapter/oid/composite'
116
+ require_relative 'attributes/composite'
117
+
118
+ ar_type.register(:composite, Adapter::OID::Composite, adapter: :postgresql)
119
+
120
+ config.namespace ||= ::Object.const_set('Composite', Module.new)
121
+
122
+ # Define a method to find composite classes based on the namespace
123
+ config.namespace.define_singleton_method(:const_missing) do |name|
124
+ Attributes::Composite.lookup(name)
125
+ end
126
+ end
127
+
88
128
  ## Geometry Enabled Setup
89
129
  if (config = torque_config.geometry).enabled
90
130
  require_relative 'adapter/oid/box'
@@ -110,6 +150,18 @@ module Torque
110
150
  ar_type.register(:interval, Adapter::OID::Interval, adapter: :postgresql)
111
151
  end
112
152
 
153
+ ## LTree Enabled Setup
154
+ if (config = torque_config.ltree).enabled
155
+ require_relative 'adapter/oid/ltree'
156
+ require_relative 'adapter/oid/lquery'
157
+
158
+ require_relative 'attributes/ltree'
159
+ require_relative 'attributes/lquery'
160
+
161
+ ar_type.register(:ltree, Adapter::OID::Ltree, adapter: :postgresql)
162
+ ar_type.register(:lquery, Adapter::OID::Lquery, adapter: :postgresql)
163
+ end
164
+
113
165
  ## Full Text Search Enabled Setup
114
166
  if (config = torque_config.full_text_search).enabled
115
167
  require_relative 'attributes/full_text_search'
@@ -32,11 +32,15 @@ module Torque
32
32
  # Get all auxiliary statements bound attributes and the base bound
33
33
  # attributes as well
34
34
  def bound_attributes
35
- visitor = ::Arel::Visitors::PostgreSQL.new(ActiveRecord::Base.connection)
36
- visitor.accept(self.arel.ast, ::Arel::Collectors::Composite.new(
35
+ collector = ::Arel::Collectors::Composite.new(
37
36
  ::Arel::Collectors::SQLString.new,
38
37
  ::Arel::Collectors::Bind.new,
39
- )).value.last
38
+ )
39
+
40
+ klass.with_connection do |connection|
41
+ visitor = ::Arel::Visitors::PostgreSQL.new(connection)
42
+ visitor.accept(arel.ast, collector).value.last
43
+ end
40
44
  end
41
45
 
42
46
  private
@@ -34,6 +34,7 @@ module Torque
34
34
  MSG
35
35
 
36
36
  count ||= 1 if values.is_a?(Range)
37
+ attribute = resolve_column(attribute).first if attribute.is_a?(Hash)
37
38
  attribute = arel_table[attribute] unless ::Arel.arel_node?(attribute)
38
39
  self.buckets_value = [attribute, values, count, cast, as]
39
40
  self
@@ -51,7 +52,7 @@ module Torque
51
52
 
52
53
  keys = buckets_keys
53
54
  self.group_values = [FN.group_by(build_buckets_node, :bucket)]
54
- super.transform_keys { |key| keys[key - 1] }
55
+ super.transform_keys { |key| keys[key - 1] if key&.positive? }
55
56
  end
56
57
 
57
58
  module Initializer
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Relation
6
+ module CrossJoin
7
+
8
+ Entry = ::Struct.new(:node, :table)
9
+
10
+ class << self
11
+ # Assemble the join node and the table that stands for what it
12
+ # exposes, which is what makes the result reachable by name
13
+ def build(source, as, columns, mode, with_ordinality, definitions = nil)
14
+ columns = columns_for(source, columns)
15
+ ordinality = ordinality_for(with_ordinality)
16
+ validate_ordinality!(ordinality, columns, definitions)
17
+
18
+ source, lateral = source_for(source, columns)
19
+ item = Arel::Nodes::FromItem.new(source, as, columns: columns,
20
+ definitions: definitions, ordinality: ordinality, lateral: lateral)
21
+
22
+ Entry.new(join_for(mode, item), ::Arel::Table.new(as.to_s))
23
+ end
24
+
25
+ private
26
+
27
+ # A list of documents describes its own columns, everything else
28
+ # has to be told what they are
29
+ def columns_for(source, columns)
30
+ return columns if columns.present? || !source.is_a?(::Array)
31
+
32
+ sample = source.first
33
+ sample.keys if sample.is_a?(::Hash)
34
+ end
35
+
36
+ # The name of the counter column, which +true+ takes from the
37
+ # configuration
38
+ def ordinality_for(value)
39
+ case value
40
+ when true then PostgreSQL.config.cross_join.ordinality_column
41
+ when ::Symbol, ::String then value
42
+ end
43
+ end
44
+
45
+ # PostgreSQL matches the alias list positionally, so a list that
46
+ # does not name every column silently renames the ones it reaches
47
+ # instead of naming the counter. Without knowing the columns, the
48
+ # only name that can be promised is the one PostgreSQL picks
49
+ def validate_ordinality!(ordinality, columns, definitions)
50
+ return if ordinality.blank? || columns.present? || definitions.present?
51
+ return if ordinality.to_s == 'ordinality'
52
+
53
+ raise ArgumentError, <<~MSG.squish
54
+ Unable to name the ordinality column "#{ordinality}" because the
55
+ columns of the source are unknown. Provide "columns:" so that
56
+ all of them can be named, or let it be called "ordinality".
57
+ MSG
58
+ end
59
+
60
+ # Only a subquery needs LATERAL, since it is a noise word in front
61
+ # of a function and PostgreSQL lets those see the row already
62
+ def source_for(source, columns)
63
+ case source
64
+ when ::ActiveRecord::Relation then [::Arel::Nodes::Grouping.new(source.arel.ast), true]
65
+ when ::Arel::SelectManager then [::Arel::Nodes::Grouping.new(source.ast), true]
66
+ when ::Array then [unnest_rows(source, columns), false]
67
+ when ::Arel::Nodes::Node then [source, false]
68
+ else
69
+ raise ArgumentError, <<~MSG.squish
70
+ Unable to join "#{source.class}". Provide an Arel node, a
71
+ relation, or an Array of rows.
72
+ MSG
73
+ end
74
+ end
75
+
76
+ # Rows become one array per column, which costs a single bind for
77
+ # each of them instead of one for every single value
78
+ def unnest_rows(rows, columns)
79
+ raise ArgumentError, 'Unable to join an empty list of rows.' if rows.empty?
80
+
81
+ raise ArgumentError, <<~MSG.squish if columns.blank?
82
+ Unable to name the columns of the given rows. Provide
83
+ "columns:" or use a Hash for each row.
84
+ MSG
85
+
86
+ keys = columns.map(&:to_sym)
87
+ values = rows.map { |row| row_values(row, keys) }
88
+ args = values.transpose.map { |list| ::Arel.array(list) }
89
+ FN.unnest(*args)
90
+ end
91
+
92
+ # Hash rows are read by name, so that they can be given in any
93
+ # order, and their keys are compared as symbols because that is
94
+ # not something the caller should have to match
95
+ def row_values(row, keys)
96
+ return ::Array.wrap(row) unless row.is_a?(::Hash)
97
+
98
+ row.symbolize_keys.values_at(*keys)
99
+ end
100
+
101
+ # A lateral reference can only be combined with an inner or a left
102
+ # join, and a cross join is how an inner one without a condition
103
+ # has to be written
104
+ def join_for(mode, item)
105
+ case mode.to_sym
106
+ when :cross then Arel::Nodes::CrossJoin.new(item)
107
+ when :left
108
+ on = ::Arel::Nodes::On.new(::Arel::Nodes::True.new)
109
+ ::Arel::Nodes::OuterJoin.new(item, on)
110
+ else
111
+ raise ArgumentError, <<~MSG.squish
112
+ The "#{mode}" is not implemented as a join type. PostgreSQL
113
+ only combines a lateral reference with :cross or :left.
114
+ MSG
115
+ end
116
+ end
117
+ end
118
+
119
+ # :nodoc:
120
+ def cross_joins_value
121
+ @values.fetch(:cross_joins, ::ActiveRecord::QueryMethods::FROZEN_EMPTY_HASH)
122
+ end
123
+
124
+ # :nodoc:
125
+ def cross_joins_value=(value)
126
+ assert_modifiable!
127
+ @values[:cross_joins] = value
128
+ end
129
+
130
+ # Joins the query against a set produced by the given source, which
131
+ # can be an Arel node, another relation, or a list of rows. The result
132
+ # is reachable by the name given to +as+. For example:
133
+ #
134
+ # User.cross_join(FN.generate_series(1, 5), as: :sample, columns: %i[value])
135
+ # # Returns 5 rows for each user
136
+ #
137
+ # User.cross_join([{ id: 1, label: 'a' }], as: :labels)
138
+ # # Returns the rows of the list combined with each user
139
+ def cross_join(source, **xargs)
140
+ spawn.cross_join!(source, **xargs)
141
+ end
142
+
143
+ # Like #cross_join, but modifies relation in place.
144
+ def cross_join!(source, as:, columns: nil, mode: :cross, with_ordinality: false)
145
+ entry = CrossJoin.build(source, as, columns, mode, with_ordinality)
146
+ self.cross_joins_value = cross_joins_value.merge(as.to_sym => entry)
147
+ self
148
+ end
149
+
150
+ private
151
+
152
+ # Hook arel build to add the joins, which is done here instead of
153
+ # through the join values because those are inspected by Rails for
154
+ # table aliases, and these sources have no table to inspect
155
+ def build_arel(*)
156
+ arel = super
157
+ return arel if cross_joins_value.blank?
158
+
159
+ nodes = cross_joins_value.each_value.map(&:node)
160
+ arel.join_sources.concat(nodes)
161
+ arel
162
+ end
163
+
164
+ # The merger is loaded with the relation, so the part that knows about
165
+ # this feature is coordinated from here
166
+ module Merger
167
+ def merge # :nodoc:
168
+ result = super
169
+ merge_cross_joins
170
+ result
171
+ end
172
+
173
+ private
174
+
175
+ def merge_cross_joins
176
+ return unless relation.is_a?(Relation::CrossJoin)
177
+ return if other.cross_joins_value.blank?
178
+
179
+ relation.cross_joins_value =
180
+ relation.cross_joins_value.merge(other.cross_joins_value)
181
+ end
182
+ end
183
+
184
+ end
185
+
186
+ ::ActiveRecord::Relation::Merger.prepend(CrossJoin::Merger)
187
+
188
+ ::ActiveRecord::Relation::SINGLE_VALUE_METHODS.push(:cross_joins)
189
+ ::ActiveRecord::Relation::VALUE_METHODS.push(:cross_joins)
190
+ ::ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES.add(:cross_joins)
191
+ end
192
+ end
193
+ end