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
@@ -7,6 +7,8 @@ RSpec.describe 'LTree' do
7
7
 
8
8
  let(:ltree) { Torque::PostgreSQL::LTree }
9
9
  let(:lquery) { Torque::PostgreSQL::LQuery }
10
+ let(:ltree_type) { Torque::PostgreSQL::Adapter::OID::Ltree.new }
11
+ let(:lquery_type) { Torque::PostgreSQL::Adapter::OID::Lquery.new }
10
12
 
11
13
  context 'on table definition' do
12
14
  subject { table_definition.new(connection, 'articles') }
@@ -50,6 +52,13 @@ RSpec.describe 'LTree' do
50
52
  expect(dump_io.string).to match(/t\.ltree +"path"/)
51
53
  end
52
54
 
55
+ it 'dumps the column as an lquery' do
56
+ dump_io = StringIO.new
57
+ ActiveRecord::SchemaDumper.dump(source, dump_io)
58
+ expect(dump_io.string).to match(/t\.lquery +"pattern"/)
59
+ expect(dump_io.string).to match(/t\.lquery +"patterns", array: true/)
60
+ end
61
+
53
62
  it 'dumps an array of paths' do
54
63
  dump_io = StringIO.new
55
64
  ActiveRecord::SchemaDumper.dump(source, dump_io)
@@ -63,9 +72,19 @@ RSpec.describe 'LTree' do
63
72
  end
64
73
 
65
74
  context 'on the path' do
66
- it 'is an array of labels' do
67
- expect(ltree.new('Top.Science')).to be_a(Array)
75
+ it 'enumerates its labels' do
76
+ subject = ltree.new('Top.Science')
77
+ expect(subject).to be_a(Enumerable)
78
+ expect(subject.items).to be_eql(%w[Top Science])
79
+ expect(subject.to_a).to be_eql(%w[Top Science])
80
+ expect(subject.map(&:downcase)).to be_eql(%w[top science])
81
+ end
82
+
83
+ it 'compares with a plain array' do
68
84
  expect(ltree.new('Top.Science')).to be_eql(%w[Top Science])
85
+ expect(ltree.new('Top.Science')).to be_eql(ltree[:Top, :Science])
86
+ expect(ltree.new('Top.Science')).not_to be_eql(%w[Top])
87
+ expect(ltree.new('Top.Science').hash).to be_eql(ltree[:Top, :Science].hash)
69
88
  end
70
89
 
71
90
  it 'accepts a string, an array or symbols' do
@@ -74,6 +93,10 @@ RSpec.describe 'LTree' do
74
93
  expect(ltree[:Top, :Science].to_s).to be_eql('Top.Science')
75
94
  end
76
95
 
96
+ it 'flattens nested arrays into labels' do
97
+ expect(ltree.new(['Top', %w[Science Astro]]).to_s).to be_eql('Top.Science.Astro')
98
+ end
99
+
77
100
  it 'knows its depth' do
78
101
  expect(ltree.new('a.b.c').depth).to be_eql(3)
79
102
  expect(ltree.new('').depth).to be_eql(0)
@@ -123,13 +146,6 @@ RSpec.describe 'LTree' do
123
146
  expect(subject.index_of('5.6', -4)).to be_eql(9)
124
147
  expect(subject.index_of('9.9')).to be_eql(-1)
125
148
  end
126
-
127
- it 'rejects anything that is not a plain sequence of labels' do
128
- expect { ltree.new('a.*') }.to raise_error(ArgumentError, /not a valid ltree label/)
129
- expect { ltree.new('a b') }.to raise_error(ArgumentError, /not a valid ltree label/)
130
- expect { ltree.new(['a', %w[b c]]) }.to raise_error(ArgumentError, /only make sense on an lquery/)
131
- expect { ltree.new(['a', 1..2]) }.to raise_error(ArgumentError, /only make sense on an lquery/)
132
- end
133
149
  end
134
150
 
135
151
  context 'on the pattern' do
@@ -162,29 +178,179 @@ RSpec.describe 'LTree' do
162
178
  expect(subject.to_s).to be_eql('Top.*{0,2}.sport*@.!football|tennis{1,}.Russ*|Spain')
163
179
  end
164
180
 
181
+ it 'holds the pure items' do
182
+ expect(lquery['Top', :any].items).to be_eql(['Top', :any])
183
+ expect(lquery.new('Top.*{1,2}.a|b').items).to be_eql(['Top', 1..2, %w[a b]])
184
+ expect(lquery.new('Top.*').to_a).to be_eql(['Top', :any])
185
+ end
186
+
187
+ it 'enumerates its items' do
188
+ expect(lquery['Top', :any]).to be_a(Enumerable)
189
+ expect(lquery['Top', :any].map(&:to_s)).to be_eql(%w[Top any])
190
+ end
191
+
165
192
  it 'accepts its own text form' do
193
+ expect(lquery.new('Top.*.sport*@').items).to be_eql(['Top', :any, 'sport*@'])
166
194
  expect(lquery.new('Top.*.sport*@').to_s).to be_eql('Top.*.sport*@')
167
195
  end
168
196
 
169
- it 'compares by its text form' do
197
+ it 'compares by its items' do
170
198
  expect(lquery['Top', :any]).to be_eql(lquery.new('Top.*'))
199
+ expect(lquery['Top', :any]).to be_eql(['Top', :any])
171
200
  expect(lquery['Top', :any].hash).to be_eql(lquery.new('Top.*').hash)
172
201
  end
173
202
 
174
- it 'rejects items that are not valid' do
175
- expect { lquery['a.b'] }.to raise_error(ArgumentError, /not a valid lquery item/)
176
- expect { lquery['foo{'] }.to raise_error(ArgumentError, /not a valid lquery item/)
177
- expect { lquery['a b'] }.to raise_error(ArgumentError, /not a valid lquery item/)
178
- expect { lquery[{ a: 1 }] }.to raise_error(ArgumentError, /Unable to use/)
203
+ it 'leaves the validation to the database' do
204
+ expect(lquery['a b'].to_s).to be_eql('a b')
205
+ expect(lquery[2..0].to_s).to be_eql('*{2,0}')
206
+ expect { Category.where(path: ['a b']).count }.to raise_error(ActiveRecord::StatementInvalid)
207
+ end
208
+ end
209
+
210
+ context 'on matching' do
211
+ let(:root) { Category.create!(title: 'Top') }
212
+
213
+ after { Torque::PostgreSQL.config.ltree.sanitize = nil }
214
+
215
+ it 'compiles into a regular expression over the text of a path' do
216
+ subject = lquery.new('Top.*{1,}.sport*@')
217
+ expect(subject.pattern).to be_a(Regexp)
218
+ expect(subject.pattern).to be_equal(subject.pattern)
219
+ expect(subject.pattern).to match('Top.Science.Sports')
220
+ expect(subject.pattern).not_to match('Top.Sports')
221
+ expect(subject.pattern).not_to match('Other.Top.Science.Sports')
222
+ end
223
+
224
+ it 'matches a string, an array or a path' do
225
+ subject = lquery['Top', :any]
226
+ expect(subject.match?('Top.Science')).to be_truthy
227
+ expect(subject.match?(%w[Top Science])).to be_truthy
228
+ expect(subject.match?(ltree['Top'])).to be_truthy
229
+ expect(subject.match?('Other')).to be_falsey
230
+ expect(subject.match?(nil)).to be_falsey
231
+ end
232
+
233
+ it 'works like a regular expression on the match operator' do
234
+ subject = lquery['Top', :any]
235
+ expect(subject =~ 'Top.Science').to be_eql(0)
236
+ expect(subject =~ 'Other').to be_nil
237
+ expect('Top.Science' =~ subject).to be_eql(0)
238
+ end
239
+
240
+ it 'matches a record by its primary key' do
241
+ expect(lquery[root, :any].match?([root, 'Science'])).to be_truthy
242
+ expect(lquery[root, :any].match?(root)).to be_truthy
243
+ expect(lquery['Top', :any].match?(root)).to be_falsey
179
244
  end
180
245
 
181
- it 'rejects quantifiers that make no sense' do
182
- expect { lquery[2..0] }.to raise_error(ArgumentError, /not a valid quantifier/)
183
- expect { lquery[-1..2] }.to raise_error(ArgumentError, /not a valid quantifier/)
246
+ it 'sanitizes the path like a condition would' do
247
+ Torque::PostgreSQL.config.ltree.sanitize = { '-' => '_' }
248
+ expect(lquery.new('top.my-slug').match?('top.my_slug')).to be_truthy
249
+ expect(lquery.new('top.my_slug').match?('top.my-slug')).to be_truthy
250
+ end
251
+
252
+ samples = [
253
+ ['a.b.c.d.e', 'a.b.c.d.e', true],
254
+ ['a.b.c.d.e', 'A.b.c.d.e', false],
255
+ ['a.b.c.d.e', 'A@.b.c.d.e', true],
256
+ ['aa.b.c.d.e', 'A@.b.c.d.e', false],
257
+ ['aa.b.c.d.e', 'A*.b.c.d.e', false],
258
+ ['aa.b.c.d.e', 'A*@.b.c.d.e', true],
259
+ ['aa.b.c.d.e', 'A*@|g.b.c.d.e', true],
260
+ ['g.b.c.d.e', 'A*@|g.b.c.d.e', true],
261
+ ['a.b.c.d.e', 'a.*.e', true],
262
+ ['a.b.c.d.e', 'a.*{3}.e', true],
263
+ ['a.b.c.d.e', 'a.*{2}.e', false],
264
+ ['a.b.c.d.e', 'a.*{4}.e', false],
265
+ ['a.b.c.d.e', 'a.*{,4}.e', true],
266
+ ['a.b.c.d.e', 'a.*{2,}.e', true],
267
+ ['a.b.c.d.e', 'a.*{2,4}.e', true],
268
+ ['a.b.c.d.e', 'a.*{2,3}.e', true],
269
+ ['a.b.c.d.e', 'a.*{2,3}', false],
270
+ ['a.b.c.d.e', 'a.*{2,4}', true],
271
+ ['a.b.c.d.e', 'a.*{2,5}', true],
272
+ ['a.b.c.d.e', '*{2,3}.e', false],
273
+ ['a.b.c.d.e', '*{2,4}.e', true],
274
+ ['a.b.c.d.e', '*{2,5}.e', true],
275
+ ['a.b.c.d.e', '*.e', true],
276
+ ['a.b.c.d.e', '*.e.*', true],
277
+ ['a.b.c.d.e', '*.d.*', true],
278
+ ['a.b.c.d.e', '*.a.*.d.*', true],
279
+ ['a.b.c.d.e', '*.!d.*', true],
280
+ ['a.b.c.d.e', '*.!d', true],
281
+ ['a.b.c.d.e', '!d.*', true],
282
+ ['a.b.c.d.e', '!a.*', false],
283
+ ['a.b.c.d.e', '*.!e', false],
284
+ ['a.b.c.d.e', '*.!e.*', true],
285
+ ['a.b.c.d.e', 'a.*.!e', false],
286
+ ['a.b.c.d.e', 'a.*.!d', true],
287
+ ['a.b.c.d.e', 'a.*.!d.*', true],
288
+ ['a.b.c.d.e', 'a.*.!f.*', true],
289
+ ['a.b.c.d.e', '*.a.*.!f.*', true],
290
+ ['a.b.c.d.e', '*.a.*.!d.*', true],
291
+ ['a.b.c.d.e', '*.a.!d.*', true],
292
+ ['a.b.c.d.e', '*.a.!d', false],
293
+ ['a.b.c.d.e', 'a.!d.*', true],
294
+ ['a.b.c.d.e', '*.!b.*', true],
295
+ ['a.b.c.d.e', '*.!b.c.*', false],
296
+ ['a.b.c.d.e', '*.!b.*.c.*', true],
297
+ ['a.b.c.d.e', '!b.*.c.*', true],
298
+ ['a.b.c.d.e', '!b.b.*', true],
299
+ ['a.b.c.d.e', '!b.*.e', true],
300
+ ['a.b.c.d.e', '!b.!c.*.e', true],
301
+ ['a.b.c.d.e', '!b.*.!c.*.e', true],
302
+ ['a.b.c.d.e', '*{2}.!b.*.!c.*.e', true],
303
+ ['a.b.c.d.e', '*{1}.!b.*.!c.*.e', false],
304
+ ['a.b.c.d.e', '*{1}.!b.*{1}.!c.*.e', false],
305
+ ['a.b.c.d.e', 'a.!b.*{1}.!c.*.e', false],
306
+ ['a.b.c.d.e', '!b.*{1}.!c.*.e', false],
307
+ ['a.b.c.d.e', '*.!b.*{1}.!c.*.e', false],
308
+ ['a.b.c.d.e', '*.!b.*.!c.*.e', true],
309
+ ['a.b.c.d.e', '!b.!c.*', true],
310
+ ['a.b.c.d.e', '!b.*.!c.*', true],
311
+ ['a.b.c.d.e', '*{2}.!b.*.!c.*', true],
312
+ ['a.b.c.d.e', '*{1}.!b.*.!c.*', false],
313
+ ['a.b.c.d.e', '*{1}.!b.*{1}.!c.*', false],
314
+ ['a.b.c.d.e', 'a.!b.*{1}.!c.*', false],
315
+ ['a.b.c.d.e', '!b.*{1}.!c.*', false],
316
+ ['a.b.c.d.e', '*.!b.*{1}.!c.*', true],
317
+ ['a.b.c.d.e', '*.!b.*.!c.*', true],
318
+ ['a.b.c.d.e', 'a.*{2}.*{2}', true],
319
+ ['a.b.c.d.e', 'a.*{1}.*{2}.e', true],
320
+ ['a.b.c.d.e', 'a.*{1}.*{4}', false],
321
+ ['a.b.c.d.e', 'a.*{5}.*', false],
322
+ ['5.0.1.0', '5.!0.!0.0', false],
323
+ ['a.b', '!a.!a', false],
324
+ ['a.b.c.d.e', 'a{,}', false],
325
+ ['a.b.c.d.e', 'a{1,}.*', true],
326
+ ['a.b.c.d.e', 'a{,}.!a{,}', true],
327
+ ['a.b.c.d.a', 'a{,}.!a{,}', false],
328
+ ['a.b.c.d.a', 'a{,2}.!a{1,}', false],
329
+ ['a.b.c.d.e', 'a{,2}.!a{1,}', true],
330
+ ['a.b.c.d.e', '!x{,}', true],
331
+ ['a.b.c.d.e', '!c{,}', false],
332
+ ['a.b.c.d.e', '!c{0,3}.!a{2,}', true],
333
+ ['a.b.c.d.e', '!c{0,3}.!d{2,}.*', true],
334
+ ['QWER_TY', 'q%@*', true],
335
+ ['QWER_TY', 'q%@*%@*', true],
336
+ ['QWER_TY', 'Q_t%@*', true],
337
+ ['QWER_GY', 'q_t%@*', false],
338
+ ]
339
+
340
+ samples.each do |path, query, expected|
341
+ it "agrees with PostgreSQL that '#{path}' ~ '#{query}' is #{expected}" do
342
+ expect(lquery.new(query).match?(path)).to be(expected)
343
+ end
184
344
  end
185
345
 
186
- it 'rejects an empty alternation' do
187
- expect { lquery[[]] }.to raise_error(ArgumentError, /at least one label/)
346
+ it 'holds the same outcomes as the database for every sample' do
347
+ rows = samples.map { |path, query, _| "('#{path}', '#{query}')" }
348
+ result = connection.select_values(<<~SQL)
349
+ SELECT path::ltree ~ query::lquery
350
+ FROM (VALUES #{rows.join(', ')}) AS samples(path, query)
351
+ SQL
352
+
353
+ expect(result).to be_eql(samples.map(&:last))
188
354
  end
189
355
  end
190
356
 
@@ -291,8 +457,9 @@ RSpec.describe 'LTree' do
291
457
  expect((ltree.new('top') / path_like.new('app.users')).to_s).to be_eql('top.app.users')
292
458
  end
293
459
 
294
- it 'contributes every label of a pattern' do
460
+ it 'contributes every item of a pattern' do
295
461
  expect(lquery[path_like.new('app.users'), :any].to_s).to be_eql('app.users.*')
462
+ expect(lquery[path_like.new(['app', :any])].items).to be_eql(['app', :any])
296
463
  end
297
464
 
298
465
  it 'turns the condition into a match when it describes a pattern' do
@@ -317,25 +484,24 @@ RSpec.describe 'LTree' do
317
484
 
318
485
  it 'is ignored when the config has no method' do
319
486
  Torque::PostgreSQL.config.ltree.compatible_method = nil
320
- expect { ltree.new(path_like.new('app.users')) }
321
- .to raise_error(ArgumentError, /only make sense on an lquery/)
487
+ expect(ltree.new(path_like.new('app.users'))).not_to be_eql(%w[app users])
322
488
  end
323
489
  end
324
490
 
325
491
  context 'on markers' do
326
492
  it 'treats a plain path as a path' do
327
- expect(lquery.marker?('Top.Science')).to be_falsey
328
- expect(lquery.marker?(%w[Top Science])).to be_falsey
329
- expect(lquery.marker?(%i[Top Science])).to be_falsey
493
+ expect(lquery.new('Top.Science')).not_to be_pattern
494
+ expect(lquery.new(%w[Top Science])).not_to be_pattern
495
+ expect(lquery.new(%i[Top Science])).not_to be_pattern
330
496
  end
331
497
 
332
498
  it 'treats anything with a query feature as a pattern' do
333
- expect(lquery.marker?(['Top', :any])).to be_truthy
334
- expect(lquery.marker?(['Top', 1..])).to be_truthy
335
- expect(lquery.marker?(['Top', %w[a b]])).to be_truthy
336
- expect(lquery.marker?('Top.*')).to be_truthy
337
- expect(lquery.marker?('!Top')).to be_truthy
338
- expect(lquery.marker?('Top@')).to be_truthy
499
+ expect(lquery.new(['Top', :any])).to be_pattern
500
+ expect(lquery.new(['Top', 1..])).to be_pattern
501
+ expect(lquery.new(['Top', %w[a b]])).to be_pattern
502
+ expect(lquery.new('Top.*')).to be_pattern
503
+ expect(lquery.new('!Top')).to be_pattern
504
+ expect(lquery.new('Top@')).to be_pattern
339
505
  end
340
506
  end
341
507
 
@@ -361,39 +527,59 @@ RSpec.describe 'LTree' do
361
527
  expect(lquery['a-b*', %w[c-d e]].to_s).to be_eql('a_b*.c_d|e')
362
528
  end
363
529
 
364
- it 'never rewrites a quantifier' do
365
- Torque::PostgreSQL.config.ltree.sanitize = { '1' => '9' }
366
- expect(lquery['foo{1,2}'].to_s).to be_eql('foo{1,2}')
367
- end
368
-
369
530
  it 'does not apply to values read from the database' do
370
531
  Torque::PostgreSQL.config.ltree.sanitize = { 'a' => 'z' }
371
- expect(ltree.load('a.b').to_s).to be_eql('a.b')
532
+ expect(ltree_type.deserialize('a.b').to_s).to be_eql('a.b')
533
+ expect(lquery_type.deserialize('a.*').to_s).to be_eql('a.*')
372
534
  end
373
535
  end
374
536
 
375
537
  context 'on OID' do
376
- subject { Torque::PostgreSQL::Adapter::OID::Ltree.new }
377
-
378
- it 'has the right type' do
379
- expect(subject.type).to be_eql(:ltree)
538
+ it 'has the right types' do
539
+ expect(ltree_type.type).to be_eql(:ltree)
540
+ expect(lquery_type.type).to be_eql(:lquery)
380
541
  end
381
542
 
382
543
  it 'deserializes into a path' do
383
- expect(subject.deserialize('Top.Science')).to be_a(Torque::PostgreSQL::LTree)
384
- expect(subject.deserialize('Top.Science')).to be_eql(%w[Top Science])
385
- expect(subject.deserialize(nil)).to be_nil
544
+ expect(ltree_type.deserialize('Top.Science')).to be_a(ltree)
545
+ expect(ltree_type.deserialize('Top.Science')).to be_eql(%w[Top Science])
546
+ expect(ltree_type.deserialize(nil)).to be_nil
547
+ end
548
+
549
+ it 'serializes a path back into text' do
550
+ expect(ltree_type.serialize(%w[Top Science])).to be_eql('Top.Science')
551
+ expect(ltree_type.serialize('Top.Science')).to be_eql('Top.Science')
552
+ expect(ltree_type.serialize(nil)).to be_nil
553
+ expect(ltree_type.serialize('')).to be_nil
554
+ end
555
+
556
+ it 'deserializes a pattern into its items' do
557
+ expect(lquery_type.deserialize('users.*')).to be_a(lquery)
558
+ expect(lquery_type.deserialize('users.*')).to be_eql(['users', :any])
559
+ expect(lquery_type.deserialize('a.*{2}')).to be_eql(['a', 2..2])
560
+ expect(lquery_type.deserialize('a.*{1,}')).to be_eql(['a', 1..])
561
+ expect(lquery_type.deserialize('a.*{,3}')).to be_eql(['a', ..3])
562
+ expect(lquery_type.deserialize('a.*{0,2}')).to be_eql(['a', 0..2])
563
+ expect(lquery_type.deserialize('a|b.c')).to be_eql([%w[a b], 'c'])
564
+ expect(lquery_type.deserialize(nil)).to be_nil
565
+ end
566
+
567
+ it 'keeps negated and quantified groups as text' do
568
+ expect(lquery_type.deserialize('!a|b.c{1,}')).to be_eql(['!a|b', 'c{1,}'])
386
569
  end
387
570
 
388
- it 'serializes back into text' do
389
- expect(subject.serialize(%w[Top Science])).to be_eql('Top.Science')
390
- expect(subject.serialize('Top.Science')).to be_eql('Top.Science')
391
- expect(subject.serialize(nil)).to be_nil
392
- expect(subject.serialize('')).to be_nil
571
+ it 'round trips the example from the PostgreSQL manual' do
572
+ text = 'Top.*{0,2}.sport*@.!football|tennis{1,}.Russ*|Spain'
573
+ items = ['Top', 0..2, 'sport*@', '!football|tennis{1,}', %w[Russ* Spain]]
574
+
575
+ expect(lquery_type.deserialize(text)).to be_eql(items)
576
+ expect(lquery_type.serialize(lquery_type.deserialize(text))).to be_eql(text)
393
577
  end
394
578
 
395
- it 'validates on the way in' do
396
- expect { subject.serialize('Top.*') }.to raise_error(ArgumentError)
579
+ it 'serializes a pattern back into text' do
580
+ expect(lquery_type.serialize(['Top', :any])).to be_eql('Top.*')
581
+ expect(lquery_type.serialize(lquery['Top', 1..])).to be_eql('Top.*{1,}')
582
+ expect(lquery_type.serialize(nil)).to be_nil
397
583
  end
398
584
  end
399
585
 
@@ -402,7 +588,7 @@ RSpec.describe 'LTree' do
402
588
 
403
589
  it 'reads and writes a path' do
404
590
  subject.path = 'Top.Science'
405
- expect(subject.path).to be_a(Torque::PostgreSQL::LTree)
591
+ expect(subject.path).to be_a(ltree)
406
592
  expect(subject.path).to be_eql(%w[Top Science])
407
593
  end
408
594
 
@@ -416,17 +602,47 @@ RSpec.describe 'LTree' do
416
602
  expect(Category.find(subject.id).path).to be_eql(%w[Top Science])
417
603
  end
418
604
 
419
- it 'round trips an array of paths as an array of arrays' do
605
+ it 'round trips an array of paths' do
420
606
  user = User.create!(name: 'Rick', permissions: ['app.users.write', %w[app posts]])
421
607
  reloaded = User.find(user.id)
422
608
 
423
609
  expect(reloaded.permissions).to be_eql([%w[app users write], %w[app posts]])
424
- expect(reloaded.permissions.first).to be_a(Torque::PostgreSQL::LTree)
610
+ expect(reloaded.permissions.first).to be_a(ltree)
611
+ end
612
+
613
+ it 'reads and writes a pattern' do
614
+ subject.pattern = ['users', :any]
615
+ expect(subject.pattern).to be_a(lquery)
616
+ expect(subject.pattern.items).to be_eql(['users', :any])
617
+ end
618
+
619
+ it 'round trips a pattern as its items' do
620
+ subject.update!(title: 'Rule', pattern: ['users', :any, 1..2])
621
+ expect(Category.find(subject.id).pattern.items).to be_eql(['users', :any, 1..2])
622
+ end
623
+
624
+ it 'round trips an array of patterns' do
625
+ subject.update!(title: 'Rule', patterns: [['users', :any], 'posts.*{1,}'])
626
+ reloaded = Category.find(subject.id)
627
+
628
+ expect(reloaded.patterns).to be_eql([['users', :any], ['posts', 1..]])
629
+ expect(reloaded.patterns.first).to be_a(lquery)
630
+ end
631
+
632
+ it 'tracks changes by the compiled form' do
633
+ subject.update!(title: 'Rule', path: 'Top.Science', pattern: ['Top', :any])
634
+ subject.path = %w[Top Science]
635
+ subject.pattern = 'Top.*'
636
+ expect(subject).not_to be_changed
637
+
638
+ subject.pattern = ['Top', 1..]
639
+ expect(subject).to be_changed
425
640
  end
426
641
 
427
- it 'refuses a pattern where a path is expected' do
642
+ it 'leaves the validation to the database' do
428
643
  subject.path = 'Top.*'
429
- expect { subject.path }.to raise_error(ArgumentError, /not a valid ltree label/)
644
+ expect(subject.path).to be_eql(['Top', '*'])
645
+ expect { subject.save! }.to raise_error(ActiveRecord::StatementInvalid)
430
646
  end
431
647
  end
432
648
 
@@ -468,6 +684,27 @@ RSpec.describe 'LTree' do
468
684
  .to include(%[WHERE "categories"."path" ~ 'Top.*'::lquery])
469
685
  end
470
686
 
687
+ it 'takes a plain array as one path' do
688
+ expect(subject.where(path: %w[Top Science]).to_sql).not_to include('IN')
689
+ end
690
+
691
+ it 'lists paths when the first entry is a whole one' do
692
+ expect(subject.where(path: [%w[Top a], %w[Top b]]).to_sql)
693
+ .to include(%[WHERE "categories"."path" IN ('Top.a', 'Top.b')])
694
+ expect(subject.where(path: [ltree['Top'], 'Other']).to_sql)
695
+ .to include(%[WHERE "categories"."path" IN ('Top', 'Other')])
696
+ end
697
+
698
+ it 'tests a list of patterns at once' do
699
+ expect(subject.where(path: [['Top', :any], lquery['Other', 1..]]).to_sql)
700
+ .to include(%[WHERE "categories"."path" ? '{Top.*,"Other.*{1,}"}'::lquery[]])
701
+ end
702
+
703
+ it 'handles nil and an empty list' do
704
+ expect(subject.where(path: nil).to_sql).to include(%[WHERE "categories"."path" IS NULL])
705
+ expect(subject.where(path: []).to_sql).to include('WHERE 1=0')
706
+ end
707
+
471
708
  it 'finds the records that match' do
472
709
  Category.create!(title: 'Science', path: 'Top.Science')
473
710
  Category.create!(title: 'Astronomy', path: 'Top.Science.Astronomy')
@@ -476,15 +713,113 @@ RSpec.describe 'LTree' do
476
713
  expect(Category.where(path: ['Top', :any]).count).to be_eql(3)
477
714
  expect(Category.where(path: %w[Top Science]).count).to be_eql(1)
478
715
  expect(Category.where(path: ['Top', 'Science', :any]).count).to be_eql(2)
716
+ expect(Category.where(path: [%w[Top Science], %w[Top Hobbies]]).count).to be_eql(2)
717
+ expect(Category.where(path: [['Top', 'Sci*'], ['Nope', :any]]).count).to be_eql(1)
718
+ end
719
+
720
+ context 'on an array of paths' do
721
+ subject { User.all }
722
+
723
+ it 'asks whether any entry is the path' do
724
+ expect(subject.where(permissions: 'app.users').to_sql)
725
+ .to include(%[WHERE 'app.users' = ANY("users"."permissions")])
726
+ end
727
+
728
+ it 'matches the entries against a pattern' do
729
+ expect(subject.where(permissions: ['app', :any]).to_sql)
730
+ .to include(%[WHERE "users"."permissions" ~ 'app.*'::lquery])
731
+ end
732
+
733
+ it 'overlaps with a list of paths' do
734
+ expect(subject.where(permissions: [%w[app users], %w[app posts]]).to_sql)
735
+ .to include(%[WHERE "users"."permissions" && '{app.users,app.posts}'])
736
+ end
737
+
738
+ it 'tests the entries against a list of patterns' do
739
+ expect(subject.where(permissions: [['app', :any], ['other', :any]]).to_sql)
740
+ .to include(%[WHERE "users"."permissions" ? '{app.*,other.*}'::lquery[]])
741
+ end
742
+
743
+ it 'handles nil and an empty list' do
744
+ expect(subject.where(permissions: nil).to_sql)
745
+ .to include(%[WHERE "users"."permissions" IS NULL])
746
+ expect(subject.where(permissions: []).to_sql)
747
+ .to include(%[WHERE CARDINALITY("users"."permissions") = 0])
748
+ end
749
+
750
+ it 'finds the records that match' do
751
+ User.create!(name: 'Editor', permissions: ['app.posts', 'app.users'])
752
+ User.create!(name: 'Guest', permissions: ['app.posts'])
753
+
754
+ expect(User.where(permissions: 'app.users').pluck(:name)).to be_eql(%w[Editor])
755
+ expect(User.where(permissions: ['app', 'u*']).pluck(:name)).to be_eql(%w[Editor])
756
+ expect(User.where(permissions: [%w[app users], %w[nope]]).pluck(:name)).to be_eql(%w[Editor])
757
+ expect(User.where(permissions: [['app', :any]]).count).to be_eql(2)
758
+ end
479
759
  end
480
760
 
481
- it 'leaves an array column to the default behavior' do
482
- expect(User.where(permissions: ['app.users']).to_sql)
483
- .to include(%[WHERE "users"."permissions" = ])
761
+ context 'on a pattern column' do
762
+ it 'compares a pattern by its text' do
763
+ expect(subject.where(pattern: ['users', :any]).to_sql)
764
+ .to include(%[WHERE "categories"."pattern"::text = 'users.*'])
765
+ end
766
+
767
+ it 'matches a path against the stored pattern' do
768
+ expect(subject.where(pattern: 'users.admin').to_sql)
769
+ .to include(%[WHERE "categories"."pattern" ~ 'users.admin'::ltree])
770
+ end
771
+
772
+ it 'lists patterns by their text' do
773
+ expect(subject.where(pattern: [['users', :any], ['posts', :any]]).to_sql)
774
+ .to include(%[WHERE "categories"."pattern"::text IN ('users.*', 'posts.*')])
775
+ end
776
+
777
+ it 'matches any of the paths' do
778
+ expect(subject.where(pattern: [%w[users admin], %w[posts new]]).to_sql)
779
+ .to include(%[WHERE "categories"."pattern" ~ ANY('{users.admin,posts.new}'::ltree[])])
780
+ end
781
+
782
+ it 'finds the patterns that match' do
783
+ Category.create!(title: 'Users', pattern: ['users', :any])
784
+ Category.create!(title: 'Posts', pattern: ['posts', 1..])
785
+
786
+ expect(Category.where(pattern: 'users.admin').pluck(:title)).to be_eql(%w[Users])
787
+ expect(Category.where(pattern: ['users', :any]).pluck(:title)).to be_eql(%w[Users])
788
+ expect(Category.where(pattern: [%w[users admin], %w[posts new]]).pluck(:title))
789
+ .to match_array(%w[Users Posts])
790
+ end
484
791
  end
485
792
 
486
- it 'never takes an array as a list of values' do
487
- expect(subject.where(path: %w[Top Science]).to_sql).not_to include('IN')
793
+ context 'on an array of patterns' do
794
+ it 'asks whether any entry is the pattern' do
795
+ expect(subject.where(patterns: ['users', :any]).to_sql)
796
+ .to include(%[WHERE 'users.*' = ANY("categories"."patterns"::text[])])
797
+ end
798
+
799
+ it 'matches a path against any entry' do
800
+ expect(subject.where(patterns: 'users.admin').to_sql)
801
+ .to include(%[WHERE 'users.admin'::ltree ~ ANY("categories"."patterns")])
802
+ end
803
+
804
+ it 'overlaps with a list of patterns by their text' do
805
+ expect(subject.where(patterns: [['users', :any], ['posts', :any]]).to_sql)
806
+ .to include(%[WHERE "categories"."patterns"::text[] && '{users.*,posts.*}'])
807
+ end
808
+
809
+ it 'tests the entries against a list of paths' do
810
+ expect(subject.where(patterns: [%w[users admin], %w[posts new]]).to_sql)
811
+ .to include(%[WHERE "categories"."patterns" ? '{users.admin,posts.new}'::ltree[]])
812
+ end
813
+
814
+ it 'finds the records whose patterns match' do
815
+ Category.create!(title: 'Rule', patterns: [['users', :any], ['posts', 1..]])
816
+
817
+ expect(Category.where(patterns: 'users.admin').count).to be_eql(1)
818
+ expect(Category.where(patterns: 'nope.x').count).to be_eql(0)
819
+ expect(Category.where(patterns: ['users', :any]).count).to be_eql(1)
820
+ expect(Category.where(patterns: [%w[posts new], %w[nope]]).count).to be_eql(1)
821
+ expect(Category.where(patterns: [['users', :any], ['x', :any]]).count).to be_eql(1)
822
+ end
488
823
  end
489
824
  end
490
825
 
@@ -545,8 +880,7 @@ RSpec.describe 'LTree' do
545
880
  end
546
881
 
547
882
  it 'matches grants against a pattern' do
548
- condition = grants.matches_lquery(::Arel.sql("'app.users.*'").pg_cast('lquery'))
549
- expect(User.where(condition).pluck(:name)).to match_array(%w[Editor])
883
+ expect(User.where(permissions: ['app', 'users', :any]).pluck(:name)).to match_array(%w[Editor])
550
884
  end
551
885
  end
552
886
  end