torque-postgresql 4.1.0 → 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 (194) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +28 -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 +9 -0
  7. data/lib/torque/postgresql/adapter/oid/array.rb +0 -4
  8. data/lib/torque/postgresql/adapter/oid/composite.rb +2 -1
  9. data/lib/torque/postgresql/adapter/oid/enum.rb +1 -1
  10. data/lib/torque/postgresql/adapter/oid/enum_set.rb +2 -2
  11. data/lib/torque/postgresql/adapter/oid/interval.rb +1 -1
  12. data/lib/torque/postgresql/adapter/oid/lquery.rb +45 -28
  13. data/lib/torque/postgresql/adapter/oid/ltree.rb +67 -9
  14. data/lib/torque/postgresql/adapter/oid/struct.rb +1 -1
  15. data/lib/torque/postgresql/adapter/oid.rb +13 -0
  16. data/lib/torque/postgresql/adapter/schema_definitions.rb +5 -0
  17. data/lib/torque/postgresql/adapter/schema_dumper.rb +1 -0
  18. data/lib/torque/postgresql/arel/nodes.rb +51 -0
  19. data/lib/torque/postgresql/arel/visitors.rb +46 -0
  20. data/lib/torque/postgresql/attributes/base.rb +10 -7
  21. data/lib/torque/postgresql/attributes/composite.rb +1 -5
  22. data/lib/torque/postgresql/attributes/enum.rb +1 -6
  23. data/lib/torque/postgresql/attributes/lquery.rb +67 -127
  24. data/lib/torque/postgresql/attributes/ltree.rb +35 -83
  25. data/lib/torque/postgresql/base.rb +9 -1
  26. data/lib/torque/postgresql/config.rb +21 -3
  27. data/lib/torque/postgresql/geometry_builder.rb +4 -10
  28. data/lib/torque/postgresql/inheritance.rb +5 -5
  29. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +1 -1
  30. data/lib/torque/postgresql/predicate_builder/array_handler.rb +16 -8
  31. data/lib/torque/postgresql/predicate_builder/composite_handler.rb +18 -5
  32. data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +86 -18
  33. data/lib/torque/postgresql/predicate_builder/struct_handler.rb +29 -9
  34. data/lib/torque/postgresql/predicate_builder.rb +38 -11
  35. data/lib/torque/postgresql/railtie.rb +17 -0
  36. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -3
  37. data/lib/torque/postgresql/relation/buckets.rb +2 -1
  38. data/lib/torque/postgresql/relation/cross_join.rb +193 -0
  39. data/lib/torque/postgresql/relation/inheritance.rb +1 -2
  40. data/lib/torque/postgresql/relation/join_series.rb +1 -0
  41. data/lib/torque/postgresql/relation/merger.rb +9 -0
  42. data/lib/torque/postgresql/relation/segments.rb +140 -0
  43. data/lib/torque/postgresql/relation.rb +23 -4
  44. data/lib/torque/postgresql/version.rb +1 -1
  45. data/lib/torque/postgresql/versioned_commands/command_migration.rb +23 -0
  46. data/lib/torque/postgresql/versioned_commands.rb +39 -0
  47. data/lib/torque/postgresql.rb +0 -2
  48. data/sig/generators/torque/function_generator.rbs +9 -0
  49. data/sig/generators/torque/skill_generator.rbs +7 -0
  50. data/sig/generators/torque/trigger_generator.rbs +9 -0
  51. data/sig/generators/torque/type_generator.rbs +9 -0
  52. data/sig/generators/torque/view_generator.rbs +9 -0
  53. data/sig/manifest.yaml +7 -0
  54. data/sig/torque/postgresql/adapter/database_statements.rbs +42 -0
  55. data/sig/torque/postgresql/adapter/inheritance_statements.rbs +36 -0
  56. data/sig/torque/postgresql/adapter/oid/array.rbs +20 -0
  57. data/sig/torque/postgresql/adapter/oid/box.rbs +28 -0
  58. data/sig/torque/postgresql/adapter/oid/circle.rbs +33 -0
  59. data/sig/torque/postgresql/adapter/oid/composite.rbs +35 -0
  60. data/sig/torque/postgresql/adapter/oid/enum.rbs +28 -0
  61. data/sig/torque/postgresql/adapter/oid/enum_set.rbs +19 -0
  62. data/sig/torque/postgresql/adapter/oid/interval.rbs +20 -0
  63. data/sig/torque/postgresql/adapter/oid/line.rbs +33 -0
  64. data/sig/torque/postgresql/adapter/oid/lquery.rbs +21 -0
  65. data/sig/torque/postgresql/adapter/oid/ltree.rbs +32 -0
  66. data/sig/torque/postgresql/adapter/oid/range.rbs +28 -0
  67. data/sig/torque/postgresql/adapter/oid/segment.rbs +38 -0
  68. data/sig/torque/postgresql/adapter/oid/struct.rbs +41 -0
  69. data/sig/torque/postgresql/adapter/oid/struct_list.rbs +20 -0
  70. data/sig/torque/postgresql/adapter/oid/struct_set.rbs +18 -0
  71. data/sig/torque/postgresql/adapter/oid.rbs +9 -0
  72. data/sig/torque/postgresql/adapter/quoting.rbs +20 -0
  73. data/sig/torque/postgresql/adapter/schema_creation.rbs +13 -0
  74. data/sig/torque/postgresql/adapter/schema_definitions.rbs +55 -0
  75. data/sig/torque/postgresql/adapter/schema_dumper.rbs +37 -0
  76. data/sig/torque/postgresql/adapter/schema_overrides.rbs +46 -0
  77. data/sig/torque/postgresql/adapter/schema_statements.rbs +50 -0
  78. data/sig/torque/postgresql/adapter.rbs +24 -0
  79. data/sig/torque/postgresql/arel/infix_operation.rbs +79 -0
  80. data/sig/torque/postgresql/arel/join_source.rbs +19 -0
  81. data/sig/torque/postgresql/arel/nodes.rbs +83 -0
  82. data/sig/torque/postgresql/arel/operations.rbs +28 -0
  83. data/sig/torque/postgresql/arel/select_manager.rbs +15 -0
  84. data/sig/torque/postgresql/arel/visitors.rbs +30 -0
  85. data/sig/torque/postgresql/arel.rbs +6 -0
  86. data/sig/torque/postgresql/associations/association_scope.rbs +28 -0
  87. data/sig/torque/postgresql/associations/belongs_to_many_association.rbs +51 -0
  88. data/sig/torque/postgresql/associations/builder/belongs_to_many.rbs +24 -0
  89. data/sig/torque/postgresql/associations/builder/has_many.rbs +15 -0
  90. data/sig/torque/postgresql/associations/builder.rbs +8 -0
  91. data/sig/torque/postgresql/associations/foreign_association.rbs +16 -0
  92. data/sig/torque/postgresql/associations/join_dependency.rbs +16 -0
  93. data/sig/torque/postgresql/associations/preloader/association.rbs +32 -0
  94. data/sig/torque/postgresql/associations/preloader/loader_query.rbs +20 -0
  95. data/sig/torque/postgresql/associations/preloader.rbs +8 -0
  96. data/sig/torque/postgresql/associations.rbs +17 -0
  97. data/sig/torque/postgresql/attributes/base.rbs +37 -0
  98. data/sig/torque/postgresql/attributes/builder/enum.rbs +38 -0
  99. data/sig/torque/postgresql/attributes/builder/full_text_search.rbs +27 -0
  100. data/sig/torque/postgresql/attributes/builder/period.rbs +106 -0
  101. data/sig/torque/postgresql/attributes/builder.rbs +19 -0
  102. data/sig/torque/postgresql/attributes/composite.rbs +28 -0
  103. data/sig/torque/postgresql/attributes/enum.rbs +69 -0
  104. data/sig/torque/postgresql/attributes/enum_set.rbs +75 -0
  105. data/sig/torque/postgresql/attributes/full_text_search.rbs +13 -0
  106. data/sig/torque/postgresql/attributes/lazy.rbs +17 -0
  107. data/sig/torque/postgresql/attributes/lquery.rbs +48 -0
  108. data/sig/torque/postgresql/attributes/ltree.rbs +46 -0
  109. data/sig/torque/postgresql/attributes/period.rbs +13 -0
  110. data/sig/torque/postgresql/attributes/simple_enum.rbs +39 -0
  111. data/sig/torque/postgresql/attributes/struct.rbs +38 -0
  112. data/sig/torque/postgresql/attributes.rbs +6 -0
  113. data/sig/torque/postgresql/autosave_association.rbs +16 -0
  114. data/sig/torque/postgresql/auxiliary_statement/recursive.rbs +22 -0
  115. data/sig/torque/postgresql/auxiliary_statement/settings.rbs +63 -0
  116. data/sig/torque/postgresql/auxiliary_statement.rbs +57 -0
  117. data/sig/torque/postgresql/base.rbs +41 -0
  118. data/sig/torque/postgresql/collector.rbs +7 -0
  119. data/sig/torque/postgresql/config.rbs +9 -0
  120. data/sig/torque/postgresql/function.rbs +21 -0
  121. data/sig/torque/postgresql/geometry_builder.rbs +20 -0
  122. data/sig/torque/postgresql/i18n.rbs +15 -0
  123. data/sig/torque/postgresql/inheritance/expander.rbs +23 -0
  124. data/sig/torque/postgresql/inheritance/record.rbs +20 -0
  125. data/sig/torque/postgresql/inheritance.rbs +47 -0
  126. data/sig/torque/postgresql/insert_all.rbs +22 -0
  127. data/sig/torque/postgresql/migration/command_recorder.rbs +30 -0
  128. data/sig/torque/postgresql/migration.rbs +6 -0
  129. data/sig/torque/postgresql/predicate_builder/arel_attribute_handler.rbs +15 -0
  130. data/sig/torque/postgresql/predicate_builder/array_handler.rbs +26 -0
  131. data/sig/torque/postgresql/predicate_builder/composite_handler.rbs +26 -0
  132. data/sig/torque/postgresql/predicate_builder/enumerator_lazy_handler.rbs +18 -0
  133. data/sig/torque/postgresql/predicate_builder/ltree_handler.rbs +36 -0
  134. data/sig/torque/postgresql/predicate_builder/regexp_handler.rbs +16 -0
  135. data/sig/torque/postgresql/predicate_builder/struct_handler.rbs +24 -0
  136. data/sig/torque/postgresql/predicate_builder.rbs +23 -0
  137. data/sig/torque/postgresql/predicate_table.rbs +24 -0
  138. data/sig/torque/postgresql/railtie.rbs +10 -0
  139. data/sig/torque/postgresql/reflection/abstract_reflection.rbs +22 -0
  140. data/sig/torque/postgresql/reflection/association_reflection.rbs +18 -0
  141. data/sig/torque/postgresql/reflection/belongs_to_many_reflection.rbs +30 -0
  142. data/sig/torque/postgresql/reflection/has_many_reflection.rbs +14 -0
  143. data/sig/torque/postgresql/reflection/runtime_reflection.rbs +18 -0
  144. data/sig/torque/postgresql/reflection/through_reflection.rbs +14 -0
  145. data/sig/torque/postgresql/reflection.rbs +11 -0
  146. data/sig/torque/postgresql/relation/auxiliary_statement.rbs +29 -0
  147. data/sig/torque/postgresql/relation/buckets.rbs +35 -0
  148. data/sig/torque/postgresql/relation/cross_join.rbs +63 -0
  149. data/sig/torque/postgresql/relation/distinct_on.rbs +16 -0
  150. data/sig/torque/postgresql/relation/inheritance.rbs +48 -0
  151. data/sig/torque/postgresql/relation/join_series.rbs +28 -0
  152. data/sig/torque/postgresql/relation/merger.rbs +23 -0
  153. data/sig/torque/postgresql/relation/segments.rbs +27 -0
  154. data/sig/torque/postgresql/relation.rbs +48 -0
  155. data/sig/torque/postgresql/schema_cache/bound_schema_reflection.rbs +14 -0
  156. data/sig/torque/postgresql/schema_cache/inheritance.rbs +12 -0
  157. data/sig/torque/postgresql/schema_cache/schema_reflection.rbs +14 -0
  158. data/sig/torque/postgresql/schema_cache.rbs +43 -0
  159. data/sig/torque/postgresql/table_name.rbs +20 -0
  160. data/sig/torque/postgresql/validations.rbs +13 -0
  161. data/sig/torque/postgresql/version.rbs +5 -0
  162. data/sig/torque/postgresql/versioned_commands/command_migration.rbs +54 -0
  163. data/sig/torque/postgresql/versioned_commands/generator.rbs +17 -0
  164. data/sig/torque/postgresql/versioned_commands/migration_context.rbs +30 -0
  165. data/sig/torque/postgresql/versioned_commands/migrator.rbs +19 -0
  166. data/sig/torque/postgresql/versioned_commands/schema_table.rbs +23 -0
  167. data/sig/torque/postgresql/versioned_commands.rbs +25 -0
  168. data/sig/torque/postgresql.rbs +26 -0
  169. data/skills/torque-postgresql/SKILL.md +70 -0
  170. data/spec/fixtures/migrations/20250101000008_create_trigger_audit_users_v1.sql +3 -0
  171. data/spec/initialize.rb +9 -0
  172. data/spec/mocks/cache_query.rb +1 -1
  173. data/spec/rbs_helper.rb +8 -0
  174. data/spec/schema.rb +2 -0
  175. data/spec/spec_helper.rb +1 -0
  176. data/spec/tests/arel_spec.rb +1 -3
  177. data/spec/tests/auxiliary_statement_spec.rb +12 -0
  178. data/spec/tests/buckets_spec.rb +122 -0
  179. data/spec/tests/composite_spec.rb +67 -0
  180. data/spec/tests/cross_join_spec.rb +185 -0
  181. data/spec/tests/distinct_on_spec.rb +5 -0
  182. data/spec/tests/enum_set_spec.rb +4 -0
  183. data/spec/tests/enum_spec.rb +20 -4
  184. data/spec/tests/geometric_builder_spec.rb +16 -5
  185. data/spec/tests/has_many_spec.rb +2 -1
  186. data/spec/tests/interval_spec.rb +6 -0
  187. data/spec/tests/ltree_spec.rb +398 -64
  188. data/spec/tests/predicate_builder_spec.rb +120 -0
  189. data/spec/tests/relation_spec.rb +25 -69
  190. data/spec/tests/segments_spec.rb +264 -0
  191. data/spec/tests/struct_spec.rb +202 -0
  192. data/spec/tests/table_inheritance_spec.rb +10 -0
  193. data/spec/tests/versioned_commands_spec.rb +145 -0
  194. metadata +153 -2
@@ -3,21 +3,17 @@
3
3
  module Torque
4
4
  module PostgreSQL
5
5
  module PredicateBuilder
6
- # A condition over an ltree column is rarely a plain equality, so the value
7
- # decides which operator to use: a plain path is compared with +=+, while
8
- # anything carrying an lquery marker is matched with +~+
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
9
  class LtreeHandler
10
10
  class << self
11
- # An Array is always a single value here, either the labels of a path
12
- # or the items of a pattern, so it never means a list of values the
13
- # way the regular handlers would take it. Objects that describe their
14
- # own path are claimed as well, otherwise a record would be reduced to
15
- # its primary key before it had the chance to describe itself
16
- def candidate?(value, type)
17
- return false unless type.is_a?(Adapter::OID::Ltree)
11
+ def candidate?(type)
12
+ column_of(type).is_a?(Adapter::OID::Ltree)
13
+ end
18
14
 
19
- value.is_a?(::Array) || value.is_a?(LQuery) ||
20
- LTree.compatible?(value) || LQuery.marker?(value)
15
+ def column_of(type)
16
+ type.is_a?(ARRAY_OID) ? type.subtype : type
21
17
  end
22
18
  end
23
19
 
@@ -26,17 +22,89 @@ module Torque
26
22
  end
27
23
 
28
24
  def call(attribute, type, value)
29
- return attribute.eq(FN.bind_with(attribute, type.cast(value))) \
30
- unless LQuery.marker?(value)
25
+ @attribute = attribute
26
+ @column = self.class.column_of(type)
27
+ @array = type.is_a?(ARRAY_OID)
28
+ @list = list?(value)
31
29
 
32
- attribute.matches_lquery(pattern_for(attribute, value))
30
+ values = list ? value : [value]
31
+ pattern = values.any? { |entry| lquery.cast(entry)&.pattern? }
32
+ pattern == lquery_column? ? equality(values) : match(values)
33
33
  end
34
34
 
35
35
  private
36
36
 
37
- def pattern_for(attribute, value)
38
- type = Adapter::OID::Lquery.new
39
- FN.bind(attribute.name, type.cast(value), type).pg_cast('lquery')
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))
40
108
  end
41
109
  end
42
110
  end
@@ -13,10 +13,12 @@ module Torque
13
13
 
14
14
  class << self
15
15
  # Only a hash describes properties, everything else is left alone so
16
- # that whole documents are still compared as documents
17
- def candidate?(value, type)
18
- value.is_a?(::Hash) && type.is_a?(Adapter::OID::Struct) &&
19
- !type.is_a?(Adapter::OID::StructList)
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)
20
22
  end
21
23
  end
22
24
 
@@ -25,21 +27,39 @@ module Torque
25
27
  end
26
28
 
27
29
  def call(attribute, struct, value)
28
- raise ArgumentError, <<~MSG.squish unless struct.type == :jsonb
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
29
41
  Unable to build a condition over "#{attribute.name}" because json
30
42
  columns cannot be queried by their properties. Use jsonb instead.
31
43
  MSG
32
44
 
33
45
  @struct = struct
34
- table = PredicateTable.new(self, attribute, Arel::Nodes::Property)
35
- nodes = predicate_builder.with(table).build_from_hash(value.stringify_keys)
46
+ PredicateTable.new(self, attribute, Arel::Nodes::Property)
47
+ end
36
48
 
37
- ::Arel::Nodes::Grouping.new(nodes.reduce(:and))
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
38
56
  end
39
57
 
40
58
  # The type of a single property, plus what it has to be casted to once
41
59
  # it is extracted from the document as text
42
60
  def type_of(name)
61
+ return [ActiveModel::Type.default_value, nil] unless @struct.is_a?(Adapter::OID::Struct)
62
+
43
63
  klass = @struct.klass
44
64
  name = name.to_s
45
65
 
@@ -60,7 +80,7 @@ module Torque
60
80
  name = type&.type
61
81
  return if name.nil? || PLAIN_TYPES.include?(name)
62
82
 
63
- ActiveRecord::Base.connection.type_to_sql(name)
83
+ ActiveRecord::Base.with_connection { |c| c.type_to_sql(name) }
64
84
  end
65
85
  end
66
86
  end
@@ -14,6 +14,20 @@ module Torque
14
14
  module PredicateBuilder
15
15
  ARRAY_OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
16
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
+
17
31
  def initialize(*)
18
32
  super
19
33
 
@@ -34,25 +48,38 @@ module Torque
34
48
 
35
49
  # Values described as a hash of columns or properties are turned into
36
50
  # conditions over the column that holds them. Whole values are left to
37
- # the regular handlers, so that they are compared as a single value
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
38
53
  def build(attribute, value, operator = nil)
39
- type = table.type(attribute.name)
40
- handler = handler_for_document(type, value)
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)
41
58
  return super if handler.nil?
42
59
 
43
60
  handler.new(self).call(attribute, type, value)
44
61
  end
45
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
+
46
75
  private
47
76
 
48
- def handler_for_document(type, value)
49
- if PostgreSQL.config.composite.enabled && CompositeHandler.candidate?(value, type)
50
- CompositeHandler
51
- elsif PostgreSQL.config.struct.enabled && StructHandler.candidate?(value, type)
52
- StructHandler
53
- elsif PostgreSQL.config.ltree.enabled && LtreeHandler.candidate?(value, type)
54
- LtreeHandler
55
- end
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)
56
83
  end
57
84
  end
58
85
 
@@ -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'
@@ -141,6 +155,9 @@ module Torque
141
155
  require_relative 'adapter/oid/ltree'
142
156
  require_relative 'adapter/oid/lquery'
143
157
 
158
+ require_relative 'attributes/ltree'
159
+ require_relative 'attributes/lquery'
160
+
144
161
  ar_type.register(:ltree, Adapter::OID::Ltree, adapter: :postgresql)
145
162
  ar_type.register(:lquery, Adapter::OID::Lquery, adapter: :postgresql)
146
163
  end
@@ -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
@@ -219,11 +219,10 @@ module Torque
219
219
  # once per explicit select that merely omits the marker
220
220
  def warn_about_missing_record_class(records)
221
221
  return unless model.physically_inheritances?
222
+ return if itself_only_value === true
222
223
  return if records.empty? || group_values.present? || distinct_value || select_values.empty?
223
224
  return unless from_clause.empty?
224
- return if itself_only_value === true
225
225
  return if select_values.any? { |value| record_class_marker?(value) }
226
- return if records.any? { |record| record.class != model }
227
226
 
228
227
  warn(<<~MSG.squish)
229
228
  #{model.name} was queried with an explicit select that omits
@@ -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
@@ -82,6 +83,14 @@ module Torque
82
83
  relation.buckets_value = other.buckets_value
83
84
  end
84
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
+
85
94
  end
86
95
 
87
96
  ActiveRecord::Relation::Merger.prepend Merger