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
@@ -4,173 +4,113 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Attributes
6
6
  # A pattern that matches label paths, as described by the +lquery+ data
7
- # type. It is only ever built from Ruby and never parsed back from its
8
- # text form, so each entry of the given list becomes one item
7
+ # type. It holds the pure items of the pattern: labels as Strings, +:any+
8
+ # for a star, a Range for a quantified star and an Array for a group of
9
+ # alternatives
9
10
  class LQuery
11
+ include Enumerable
12
+
10
13
  MARKERS = /[*|!{}@%]/
11
- QUANTIFIER = /\{(?:\d+|\d*,\d*)\}/
12
- LABEL = /[[:alnum:]_-]+/
13
- ALTERNATIVE = /#{LABEL}[@*%]{0,3}/
14
- ITEM = /\A!?#{ALTERNATIVE}(?:\|#{ALTERNATIVE})*#{QUANTIFIER}?\z/
15
- STAR_ITEM = /\A\*#{QUANTIFIER}?\z/
16
-
17
- class << self
18
- def [](*items)
19
- new(items)
20
- end
14
+ LEVEL = /\A(?<not>!)?(?<variants>.*?)(?:\{(?<min>\d*)(?<comma>,)?(?<max>\d*)\})?\z/
15
+ VARIANT = /\A(?<name>.*?)(?<modifiers>[*@%]*)\z/
21
16
 
22
- # Values coming from the database are valid by construction
23
- def load(value)
24
- new(value, normalize: false)
25
- end
17
+ attr_reader :items
26
18
 
27
- # Whether the given value asks for a pattern instead of a plain path,
28
- # which is what makes a condition use +~+ instead of +=+
29
- def marker?(value)
30
- Array.wrap(value).any? { |item| item_marker?(item) }
31
- end
19
+ delegate :inspect, to: :to_s
20
+ delegate :each, :size, :empty?, :[], to: :items
21
+
22
+ def self.[](*items)
23
+ new(items)
24
+ end
32
25
 
33
- private
26
+ def initialize(value = nil, sanitize: true)
27
+ @items = type.items_for(value, sanitize: sanitize).freeze
28
+ end
34
29
 
35
- def item_marker?(item)
36
- return marker?(LTree.compatible(item)) if LTree.compatible?(item)
30
+ def to_s
31
+ type.serialize(self)
32
+ end
37
33
 
38
- case item
39
- when ::Symbol then item == :any
40
- when ::Range, ::Array, LQuery then true
41
- when ::String then item.match?(MARKERS)
42
- else false
43
- end
44
- end
34
+ def pattern?
35
+ items.any? { |item| !item.is_a?(::String) || item.match?(MARKERS) }
45
36
  end
46
37
 
47
- attr_reader :items
38
+ def pattern
39
+ @pattern ||= compile
40
+ end
48
41
 
49
- def initialize(items, normalize: true)
50
- items = normalize ? expand(items) : Array.wrap(items)
51
- @items = normalize ? items.map { |item| compile(item) } : items.map(&:to_s)
42
+ def match?(path)
43
+ path = LTree.new(path) unless path.is_a?(LTree)
44
+ pattern.match?(path.to_s)
52
45
  end
53
46
 
54
- def to_s
55
- items.join('.')
47
+ def =~(path)
48
+ path = LTree.new(path) unless path.is_a?(LTree)
49
+ pattern =~ path.to_s
56
50
  end
57
51
 
58
52
  def ==(other)
59
- other.is_a?(LQuery) && to_s == other.to_s
53
+ other = other.items if other.is_a?(self.class)
54
+ other.is_a?(::Array) && items == other
60
55
  end
61
56
  alias eql? ==
62
57
 
63
58
  def hash
64
- to_s.hash
59
+ items.hash
65
60
  end
66
61
 
67
62
  private
68
63
 
69
- # An object that describes itself as a path contributes every one of
70
- # its labels as an item, rather than a single one
71
- def expand(value)
72
- return expand(LTree.compatible(value)) if LTree.compatible?(value)
73
-
74
- value = value.split('.') if value.is_a?(::String)
75
- Array.wrap(value).flat_map { |item| expand_item(item) }
64
+ def type
65
+ Adapter::OID::Lquery.new
76
66
  end
77
67
 
78
- def expand_item(item)
79
- return expand(LTree.compatible(item)) if LTree.compatible?(item)
80
-
81
- [item]
68
+ def compile
69
+ levels = to_s.split('.', -1).map { |level| compile_level(level) }
70
+ Regexp.new("\\A#{levels.join}\\z")
82
71
  end
83
72
 
84
- def compile(item)
85
- case item
86
- when ::Range then compile_range(item)
87
- when ::Array then compile_alternatives(item)
88
- when ::Symbol then item == :any ? '*' : compile_item(item)
89
- when ::String, ::Numeric, ::ActiveRecord::Base then compile_item(item)
90
- else
91
- raise ArgumentError, <<~MSG.squish
92
- Unable to use #{item.inspect} as an lquery item. Items are labels,
93
- a Range for a quantified star, or an Array of alternatives.
94
- MSG
95
- end
73
+ def compile_level(text)
74
+ match = text.match(LEVEL)
75
+ variants = match[:variants]
76
+ label = variants == '*' ? '[^.]+' : compile_variants(variants.split('|'))
77
+ label = "(?!#{label}(?:\\.|\\z))[^.]+" if match[:not]
78
+ "(?:(?:\\A|\\.)#{label})#{quantifier(match)}"
96
79
  end
97
80
 
98
- # A Range is always a quantifier over the star, since a star on its own
99
- # already means any number of labels
100
- def compile_range(range)
101
- min = range.begin
102
- max = range.end
103
- max -= 1 if max && range.exclude_end?
104
-
105
- invalid = min&.negative? || max&.negative? || (min && max && max < min)
106
- raise ArgumentError, <<~MSG.squish if invalid
107
- #{range.inspect} is not a valid quantifier for an lquery star.
108
- MSG
109
-
110
- return '*' if min.nil? && max.nil?
111
- return "*{#{min}}" if min == max
112
- return "*{#{min},}" if max.nil?
113
- return "*{,#{max}}" if min.nil?
114
- "*{#{min},#{max}}"
81
+ def compile_variants(variants)
82
+ sources = variants.map { |variant| compile_variant(variant) }
83
+ "(?:#{sources.join('|')})"
115
84
  end
116
85
 
117
- def compile_alternatives(list)
118
- raise ArgumentError, <<~MSG.squish if list.empty?
119
- An lquery alternation needs at least one label.
120
- MSG
86
+ def compile_variant(text)
87
+ match = text.match(VARIANT)
88
+ modifiers = match[:modifiers]
89
+ prefix = modifiers.include?('*')
90
+ name = match[:name]
121
91
 
122
- alternatives = list.map { |entry| compile_alternative(entry) }
123
- alternatives.join('|')
92
+ source = modifiers.include?('%') ? words(name, prefix) : label(name, prefix)
93
+ modifiers.include?('@') ? "(?i:#{source})" : source
124
94
  end
125
95
 
126
- def compile_alternative(entry)
127
- alternative = sanitize_labels(assert_plain!(entry))
128
-
129
- raise ArgumentError, <<~MSG.squish unless alternative.match?(/\A#{ALTERNATIVE}\z/)
130
- #{entry.inspect} is not a valid lquery alternative. Alternatives are
131
- single labels, optionally followed by the #{'@*%'.inspect} modifiers.
132
- MSG
133
-
134
- alternative
96
+ def label(name, prefix)
97
+ "#{Regexp.escape(name)}#{'[^.]*' if prefix}"
135
98
  end
136
99
 
137
- def compile_item(entry)
138
- text = assert_plain!(entry)
139
- return text if text.match?(STAR_ITEM)
140
-
141
- head, quantifier = split_quantifier(text)
142
- item = sanitize_labels(head) + quantifier
143
-
144
- raise ArgumentError, <<~MSG.squish unless item.match?(ITEM)
145
- #{entry.inspect} is not a valid lquery item. An item is a label with
146
- the optional #{'@*%'.inspect} modifiers, optionally negated with "!",
147
- alternated with "|" and quantified with "{n,m}".
148
- MSG
149
-
150
- item
151
- end
152
-
153
- def assert_plain!(entry)
154
- entry = LTree.resolve_record(entry)
155
- plain = entry.is_a?(::String) || entry.is_a?(::Symbol) || entry.is_a?(::Numeric)
156
- raise ArgumentError, <<~MSG.squish unless plain
157
- Unable to use #{entry.inspect} as part of an lquery item.
158
- MSG
159
-
160
- entry.to_s
161
- end
100
+ def words(name, prefix)
101
+ lookaheads = name.split('_').reject(&:empty?).map do |word|
102
+ "(?=(?:[^.]*_)?#{Regexp.escape(word)}#{'[^._]*' if prefix}(?:[_.]|\\z))"
103
+ end
162
104
 
163
- # Only the labels are normalized, so that a replacement never rewrites
164
- # the structure of the pattern nor the digits of a quantifier
165
- def sanitize_labels(text)
166
- text.gsub(LABEL) { |label| LTree.sanitize(label) }
105
+ "#{lookaheads.join}[^.]+"
167
106
  end
168
107
 
169
- def split_quantifier(text)
170
- match = text.match(/#{QUANTIFIER}\z/)
171
- return [text, ''] if match.nil?
108
+ def quantifier(match)
109
+ return match[:variants] == '*' ? '*' : '' if match[:min].nil?
172
110
 
173
- [match.pre_match, match[0]]
111
+ min = match[:min].presence || 0
112
+ max = match[:comma] ? match[:max] : min
113
+ "{#{min},#{max}}"
174
114
  end
175
115
  end
176
116
  end
@@ -3,23 +3,21 @@
3
3
  module Torque
4
4
  module PostgreSQL
5
5
  module Attributes
6
- # A label path, as stored by the +ltree+ data type. It is an Array of
7
- # labels, so it flows through Ruby like any other list, which also means
8
- # that an +ltree[]+ column simply becomes an Array of these
9
- class LTree < Array
10
- LABEL = /\A[[:alnum:]_-]+\z/
6
+ # A label path, as stored by the +ltree+ data type. It holds the plain
7
+ # list of labels, so it enumerates like any other list, and an +ltree[]+
8
+ # column simply becomes an Array of these
9
+ class LTree
10
+ include Enumerable
11
11
 
12
+ attr_reader :items
13
+
14
+ delegate :inspect, to: :to_s
15
+ delegate :each, :size, :empty?, :[], to: :items
12
16
  alias depth size
13
17
 
14
18
  class << self
15
- def [](*labels)
16
- new(labels)
17
- end
18
-
19
- # Values coming from the database are valid by construction, so they
20
- # skip both the normalization and the validation
21
- def load(value)
22
- new(value.to_s.split('.'), normalize: false)
19
+ def [](*items)
20
+ new(items)
23
21
  end
24
22
 
25
23
  # Whether the object knows how to describe itself as a path, which is
@@ -34,44 +32,24 @@ module Torque
34
32
  method = PostgreSQL.config.ltree.compatible_method
35
33
  value.public_send(method) if compatible?(value)
36
34
  end
35
+ end
37
36
 
38
- # A record stands for its primary key, which is what makes a path
39
- # built out of other records work the same way as one built out of
40
- # labels
41
- def resolve_record(value)
42
- return value unless value.is_a?(::ActiveRecord::Base)
43
-
44
- id = value.id
45
- raise ArgumentError, <<~MSG.squish if id.nil?
46
- Unable to use #{value.class.name} as a label because its
47
- #{value.class.primary_key} is still empty.
48
- MSG
49
-
50
- raise ArgumentError, <<~MSG.squish if id.is_a?(::Array)
51
- Unable to use #{value.class.name} as a label because it has a
52
- composite primary key, which cannot be a single label.
53
- MSG
54
-
55
- id
56
- end
57
-
58
- # Apply the configured replacements, so callers can feed a source that
59
- # does not satisfy PostgreSQL's rules for a label on its own
60
- def sanitize(value)
61
- replacements = PostgreSQL.config.ltree.sanitize
62
- return value if replacements.blank?
37
+ def initialize(value = nil, sanitize: true)
38
+ @items = type.items_for(value, sanitize: sanitize).freeze
39
+ end
63
40
 
64
- value.gsub(Regexp.union(replacements.keys), replacements)
65
- end
41
+ def to_s
42
+ type.serialize(self)
66
43
  end
67
44
 
68
- def initialize(labels = nil, normalize: true)
69
- super()
70
- concat(normalize ? normalized(labels) : Array.wrap(labels))
45
+ def ==(other)
46
+ other = other.items if other.is_a?(self.class)
47
+ other.is_a?(::Array) && items == other
71
48
  end
49
+ alias eql? ==
72
50
 
73
- def to_s
74
- join('.')
51
+ def hash
52
+ items.hash
75
53
  end
76
54
 
77
55
  def root?
@@ -79,24 +57,23 @@ module Torque
79
57
  end
80
58
 
81
59
  def root
82
- self.class.new(first, normalize: false)
60
+ self.class.new(items.first(1), sanitize: false)
83
61
  end
84
62
 
85
- # A path with a single label has no parent, and neither does an empty one
86
63
  def parent
87
- self.class.new(self[0..-2], normalize: false) unless root?
64
+ self.class.new(items[0..-2], sanitize: false) unless root?
88
65
  end
89
66
 
90
67
  def /(other)
91
- self.class.new(to_a + self.class.new(other), normalize: false)
68
+ self.class.new(items + self.class.new(other).items, sanitize: false)
92
69
  end
93
70
 
94
71
  alias_method :+, :/
95
72
 
96
73
  # Same as the +@>+ operator, which includes the path itself
97
74
  def ancestor_of?(other)
98
- other = self.class.new(other)
99
- size <= other.size && other.first(size) == to_a
75
+ other = self.class.new(other).items
76
+ size <= other.size && other.first(size) == items
100
77
  end
101
78
  alias covers? ancestor_of?
102
79
 
@@ -109,51 +86,26 @@ module Torque
109
86
  # The longest common ancestor, which never includes the last label of
110
87
  # any of the paths, exactly like PostgreSQL's own +lca+
111
88
  def lca(*others)
112
- paths = [self, *others].map { |path| self.class.new(path)[0..-2].to_a }
113
- result = paths.shift || []
89
+ paths = [self, *others].map { |path| self.class.new(path).items[0..-2] }
90
+ result = paths.shift
114
91
  paths.each { |path| result = common_prefix(result, path) }
115
- self.class.new(result, normalize: false)
92
+ self.class.new(result, sanitize: false)
116
93
  end
117
94
 
118
- # The position where the given subpath starts, or -1 when it is not
119
- # present. Named apart from +index+ so that Array's own contract, of
120
- # returning +nil+ when the element is missing, stays intact
121
95
  def index_of(subpath, offset = 0)
122
- subpath = self.class.new(subpath)
96
+ subpath = self.class.new(subpath).items
123
97
  offset += size if offset.negative?
124
98
  return -1 if subpath.empty? || offset.negative?
125
99
 
126
100
  range = offset..(size - subpath.size)
127
- position = range.find { |i| self[i, subpath.size] == subpath.to_a }
101
+ position = range.find { |i| items[i, subpath.size] == subpath }
128
102
  position || -1
129
103
  end
130
104
 
131
105
  private
132
106
 
133
- def normalized(labels)
134
- entries = Array.wrap(labels).flat_map { |value| split_labels(value) }
135
- entries.each { |label| assert_valid_label!(label) }
136
- end
137
-
138
- def split_labels(value)
139
- return normalized(self.class.compatible(value)) if self.class.compatible?(value)
140
-
141
- value = self.class.resolve_record(value)
142
- plain = value.is_a?(::String) || value.is_a?(::Symbol) || value.is_a?(::Numeric)
143
- raise ArgumentError, <<~MSG.squish unless plain
144
- Unable to use #{value.inspect} as part of an ltree path. A path is a
145
- plain sequence of labels, so it accepts neither the alternatives nor
146
- the quantifiers that only make sense on an lquery.
147
- MSG
148
-
149
- self.class.sanitize(value.to_s).split('.')
150
- end
151
-
152
- def assert_valid_label!(label)
153
- raise ArgumentError, <<~MSG.squish unless label.match?(LABEL)
154
- #{label.inspect} is not a valid ltree label. Labels are limited to
155
- letters, numbers, underscores and dashes.
156
- MSG
107
+ def type
108
+ Adapter::OID::Ltree.new
157
109
  end
158
110
 
159
111
  def common_prefix(one, other)
@@ -18,7 +18,15 @@ module Torque
18
18
 
19
19
  class_methods do
20
20
  delegate :distinct_on, :with, :itself_only, :expand_records, :join_series,
21
- :buckets, to: :all
21
+ :buckets, :segments, to: :all
22
+
23
+ # The Arel node for a path into a document column, typed by the struct
24
+ # class that backs the column when there is one
25
+ def arel_property_of(column, *path)
26
+ type = Adapter::OID.unwrap(attribute_types[column.to_s])
27
+ handler = PredicateBuilder::StructHandler.new(predicate_builder)
28
+ handler.property_for(arel_table[column], type, path)
29
+ end
22
30
 
23
31
  # Composite values are objects that can be invalid on their own, so the
24
32
  # attributes backed by one are validated alongside the record
@@ -38,6 +38,9 @@ module Torque
38
38
  # Add support for querying and calculating histogram buckets
39
39
  config.buckets = true
40
40
 
41
+ # Add support for segmenting calculations by named conditions
42
+ config.segments = true
43
+
41
44
  # Set a list of irregular model name when associated with table names
42
45
  config.irregular_models = {}
43
46
  def config.irregular_models=(hash)
@@ -309,8 +312,8 @@ module Torque
309
312
  ltree.enabled = true
310
313
 
311
314
  # A hash of replacements applied to every label provided by the
312
- # application before it gets validated, so that a source that does not
313
- # satisfy PostgreSQL's rules on its own can still be used. Dashes, for
315
+ # application before it is sent to the database, so that a source that
316
+ # does not satisfy PostgreSQL's rules on its own can still be used. Dashes, for
314
317
  # example, are only accepted as of PostgreSQL 16
315
318
  # { '-' => '_' } turns dashes into underscores
316
319
  # { '-' => '' } drops dashes
@@ -411,7 +414,7 @@ module Torque
411
414
 
412
415
  # Define the list of commands that are going to be versioned by this
413
416
  # method
414
- vs.types = %i[function type view]
417
+ vs.types = %i[function type view trigger]
415
418
 
416
419
  # The name of the table that will inherit from +schema_migrations+ and
417
420
  # store the list of versioned commands that have been executed
@@ -419,5 +422,20 @@ module Torque
419
422
 
420
423
  end
421
424
 
425
+ # Configure cross join features
426
+ config.nested(:cross_join) do |cross_join|
427
+
428
+ # This is an experimental feature that developers must explicitly opt-in.
429
+ # TODO: hook into how conditions and columns are resolved, so that the
430
+ # name of a joined set can shadow a real column
431
+ cross_join.enabled = false
432
+
433
+ # The name of the column added by +with_ordinality+ when it is given
434
+ # +true+ instead of a name. It matches what PostgreSQL calls the column
435
+ # when it is not named at all
436
+ cross_join.ordinality_column = :ordinality
437
+
438
+ end
439
+
422
440
  end
423
441
  end
@@ -24,8 +24,7 @@ module Torque
24
24
  case value
25
25
  when ::String
26
26
  return if value.blank?
27
- value.gsub!(DESTRUCTOR, '')
28
- build_klass(*value.split(','))
27
+ build_klass(*value.gsub(DESTRUCTOR, '').split(','))
29
28
  when ::Hash
30
29
  build_klass(*value.symbolize_keys.slice(*pieces).values)
31
30
  when ::Array
@@ -49,8 +48,8 @@ module Torque
49
48
  parts = parts&.compact&.flatten
50
49
  return if parts.blank?
51
50
 
52
- raise 'Invalid format' if parts.size < pieces.size
53
- format(formation, *parts.first(pieces.size).map(&number_serializer))
51
+ parts = ::Array.new(pieces.size) { |index| parts[index] }
52
+ format(formation, *parts.map(&number_serializer))
54
53
  end
55
54
 
56
55
  def deserialize(value)
@@ -81,13 +80,8 @@ module Torque
81
80
 
82
81
  def build_klass(*args)
83
82
  return nil if args.empty?
84
- check_invalid_format!(args)
85
83
 
86
- config_class.new(*args.try(:first, pieces.size)&.map(&:to_f))
87
- end
88
-
89
- def check_invalid_format!(args)
90
- raise 'Invalid format' if args.size < pieces.size
84
+ config_class.new(*args.first(pieces.size).map(&:to_f))
91
85
  end
92
86
  end
93
87
  end
@@ -45,7 +45,7 @@ module Torque
45
45
  # Check if the model's table depends on any inheritance
46
46
  def physically_inherited?
47
47
  inheritance_cache(:@physically_inherited) do
48
- connection.schema_cache.dependencies(
48
+ connection_pool.schema_cache.dependencies(
49
49
  defined?(@table_name) ? @table_name : decorated_table_name,
50
50
  ).present?
51
51
  end
@@ -56,7 +56,7 @@ module Torque
56
56
  # Get the list of all tables directly or indirectly dependent of the
57
57
  # current one
58
58
  def inheritance_dependents
59
- connection.schema_cache.associations(table_name) || []
59
+ connection_pool.schema_cache.associations(table_name) || []
60
60
  end
61
61
 
62
62
  # Check whether the model's table has directly or indirectly dependents
@@ -100,7 +100,7 @@ module Torque
100
100
  def casted_dependents
101
101
  inheritance_cache(:@casted_dependents) do
102
102
  inheritance_dependents.map do |table_name|
103
- [table_name, connection.schema_cache.lookup_model(table_name)]
103
+ [table_name, connection_pool.schema_cache.lookup_model(table_name)]
104
104
  end.to_h.freeze
105
105
  end
106
106
  end
@@ -121,8 +121,8 @@ module Torque
121
121
  table = super
122
122
 
123
123
  adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
124
- if Torque::PostgreSQL.config.eager_load && connection.is_a?(adapter)
125
- connection.schema_cache.add_model_name(table, self)
124
+ if Torque::PostgreSQL.config.eager_load && connection_pool.db_config.adapter_class <= adapter
125
+ connection_pool.schema_cache.add_model_name(table, self)
126
126
  end
127
127
 
128
128
  table
@@ -25,7 +25,7 @@ module Torque
25
25
  private
26
26
 
27
27
  def array_typed?(attribute)
28
- attribute.able_to_type_cast? && attribute.type_caster.is_a?(ARRAY_OID)
28
+ attribute.able_to_type_cast? && Adapter::OID.unwrap(attribute.type_caster).is_a?(ARRAY_OID)
29
29
  end
30
30
  end
31
31
  end
@@ -12,13 +12,17 @@ module Torque
12
12
  end
13
13
 
14
14
  def call_for_array(attribute, value)
15
- if !value.is_a?(::Array)
16
- call_with_value(attribute, value)
17
- elsif value.any?
18
- call_with_array(attribute, value)
19
- else
20
- call_with_empty(attribute)
21
- end
15
+ return attribute.eq(nil) if value.nil?
16
+
17
+ value = value.to_a if value.is_a?(::Set)
18
+ return call_with_value(attribute, value) unless value.is_a?(::Array)
19
+ return call_with_empty(attribute) if value.empty?
20
+
21
+ nils, values = value.map { |entry| id_of(entry) }.partition(&:nil?)
22
+ return attribute.eq(nil) if values.empty?
23
+
24
+ condition = call_with_array(attribute, values)
25
+ nils.empty? ? condition : condition.or(attribute.eq(nil))
22
26
  end
23
27
 
24
28
  private
@@ -35,8 +39,12 @@ module Torque
35
39
  FN.cardinality(attribute).eq(0)
36
40
  end
37
41
 
42
+ def id_of(entry)
43
+ entry.is_a?(::ActiveRecord::Base) ? entry.id : entry
44
+ end
45
+
38
46
  def array_attribute?(attribute)
39
- attribute.type_caster.is_a?(ARRAY_OID)
47
+ Adapter::OID.unwrap(attribute.type_caster).is_a?(ARRAY_OID)
40
48
  end
41
49
  end
42
50
 
@@ -8,14 +8,15 @@ module Torque
8
8
  # whole +where+ vocabulary works inside a composite
9
9
  class CompositeHandler
10
10
  class << self
11
- # Values that this handler knows how to deal with, checked before the
12
- # attribute itself because it is way cheaper
13
- def candidate?(value, type)
11
+ # Values that this handler knows how to deal with. Without a value,
12
+ # only a single composite has parts that can be reached on their own
13
+ def candidate?(type, value = nil)
14
14
  return false unless Adapter::OID::Composite.from(type)
15
+ return !type.is_a?(ARRAY_OID) if value.nil?
15
16
 
16
17
  case value
17
18
  when ::Hash, Attributes::Composite then true
18
- when ::Array then value.any? { |entry| candidate?(entry, type) }
19
+ when ::Array then value.compact.any? { |entry| candidate?(type, entry) }
19
20
  else false
20
21
  end
21
22
  end
@@ -41,6 +42,13 @@ module Torque
41
42
  conditions_for(attribute, value)
42
43
  end
43
44
 
45
+ # The columns of the composite value as the columns of a table, so that
46
+ # any of them can be resolved by name
47
+ def table_for(source, type)
48
+ @composite = Adapter::OID::Composite.from(type)
49
+ PredicateTable.new(self, source, Arel::Nodes::Column)
50
+ end
51
+
44
52
  # The type of a single column of the composite type
45
53
  def type_of(name)
46
54
  columns = @composite.columns
@@ -65,6 +73,11 @@ module Torque
65
73
  name = attribute.name
66
74
  cast = composite.name
67
75
 
76
+ if !array && value.is_a?(::Array)
77
+ records = value.map { |entry| FN.bind(name, entry, composite).pg_cast(cast) }
78
+ return attribute.in(records)
79
+ end
80
+
68
81
  return attribute.eq(FN.bind(name, value, composite).pg_cast(cast)) unless array
69
82
 
70
83
  if value.is_a?(::Array)
@@ -99,7 +112,7 @@ module Torque
99
112
 
100
113
  def group_for(source, entry)
101
114
  entry = entry.to_h unless entry.is_a?(::Hash)
102
- table = PredicateTable.new(self, source, Arel::Nodes::Column)
115
+ table = table_for(source, composite)
103
116
  nodes = predicate_builder.with(table).build_from_hash(entry.stringify_keys)
104
117
  ::Arel::Nodes::Grouping.new(nodes.reduce(:and))
105
118
  end