activerecord-refined 0.8.1 → 0.10.0

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +17 -0
  3. data/README.md +111 -815
  4. data/activerecord-refined.gemspec +36 -17
  5. data/docs/conditions.md +206 -0
  6. data/docs/ctes.md +65 -0
  7. data/docs/expressions.md +125 -0
  8. data/docs/functions.md +219 -0
  9. data/docs/grouping.md +55 -0
  10. data/docs/joins.md +73 -0
  11. data/docs/json.md +230 -0
  12. data/docs/ordering.md +55 -0
  13. data/docs/time_zones.md +30 -0
  14. data/docs/windows.md +41 -0
  15. data/docs/writing.md +33 -0
  16. data/examples/aggregations.rb +31 -11
  17. data/examples/complex_joins.rb +12 -10
  18. data/examples/ctes.rb +22 -20
  19. data/examples/expressions.rb +110 -45
  20. data/examples/json.rb +77 -38
  21. data/examples/postgresql.rb +64 -53
  22. data/examples/predicates.rb +35 -33
  23. data/examples/subqueries.rb +20 -18
  24. data/examples/windows.rb +23 -21
  25. data/examples/writes.rb +26 -24
  26. data/lib/active_record/refined/ast.rb +1117 -373
  27. data/lib/active_record/refined/dialect/mariadb.rb +25 -0
  28. data/lib/active_record/refined/dialect/mysql.rb +18 -0
  29. data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
  30. data/lib/active_record/refined/dialect/oracle.rb +110 -0
  31. data/lib/active_record/refined/dialect/postgresql.rb +120 -0
  32. data/lib/active_record/refined/dialect/sql_server.rb +115 -0
  33. data/lib/active_record/refined/dialect/sqlite.rb +57 -0
  34. data/lib/active_record/refined/dialect.rb +340 -0
  35. data/lib/active_record/refined.rb +877 -307
  36. data/lib/activerecord-refined/version.rb +3 -1
  37. data/lib/activerecord-refined.rb +9 -5
  38. metadata +186 -15
  39. data/.github/workflows/push_gem.yml +0 -45
  40. data/.github/workflows/sandbox.yml +0 -295
  41. data/.github/workflows/test.yml +0 -90
  42. data/.gitignore +0 -19
  43. data/Gemfile +0 -12
  44. data/Rakefile +0 -25
  45. data/benchmark/query_building.rb +0 -129
  46. data/test/test_block_syntax.rb +0 -2493
  47. data/test/test_helper.rb +0 -221
@@ -1,2493 +0,0 @@
1
- require_relative 'test_helper'
2
-
3
- class TestBlockSyntax < Minitest::Test
4
- def test_equal
5
- assert_sql(/WHERE "users"."name" = 'alice'/, User.where { :name == 'alice' }.to_sql)
6
- end
7
-
8
- def test_not_equal
9
- assert_sql(/WHERE "users"."name" != 'bob'/, User.where { :name != 'bob' }.to_sql)
10
- end
11
-
12
- def test_greater_than
13
- assert_sql(/WHERE "users"."age" > 3/, User.where { :age > 3 }.to_sql)
14
- end
15
-
16
- def test_greater_than_or_equal
17
- assert_sql(/WHERE "users"."age" >= 18/, User.where { :age >= 18 }.to_sql)
18
- end
19
-
20
- def test_less_than
21
- assert_sql(/WHERE "users"."age" < 60/, User.where { :age < 60 }.to_sql)
22
- end
23
-
24
- def test_less_than_or_equal
25
- assert_sql(/WHERE "users"."age" <= 35/, User.where { :age <= 35 }.to_sql)
26
- end
27
-
28
- def test_like
29
- assert_sql(/WHERE "users"."name" LIKE 'tender%'/, User.where { :name.like?('tender%') }.to_sql)
30
- end
31
-
32
- def test_outside_of_where_block
33
- assert_raises(ArgumentError) { :omg > 1 }
34
- end
35
-
36
- def test_and
37
- assert_sql(/WHERE "users"."name" = 'alice' AND "users"."age" > 18/,
38
- User.where { (:name == 'alice') & (:age > 18) }.to_sql)
39
- end
40
-
41
- def test_or
42
- assert_sql(/WHERE \(?\"users\".\"name\" = 'alice' OR \"users\".\"name\" = 'bob'\)?/,
43
- User.where { (:name == 'alice') | (:name == 'bob') }.to_sql)
44
- end
45
-
46
- def test_not
47
- assert_sql(/WHERE NOT \(?\"users\".\"name\" = 'alice'\)?/,
48
- User.where { !(:name == 'alice') }.to_sql)
49
- end
50
-
51
- def test_complex_combination
52
- sql = User.where { ((:name == 'alice') & (:age > 18)) | !(:name == 'bob') }.to_sql
53
- assert_sql(/"users"."name" = 'alice'/, sql)
54
- assert_sql(/"users"."age" > 18/, sql)
55
- assert_sql(/NOT/, sql)
56
- assert_sql(/OR/, sql)
57
- end
58
-
59
- def test_like_qualified
60
- assert_sql(/WHERE "users"."name" LIKE 'tender%'/,
61
- User.where { :users[:name].like?('tender%') }.to_sql)
62
- end
63
-
64
- # ILIKE is PostgreSQL's; elsewhere Arel emits LIKE, which those adapters
65
- # already match case-insensitively by default.
66
- def test_ilike
67
- expected = ADAPTER == 'postgresql' ? 'ILIKE' : 'LIKE'
68
- assert_sql(/WHERE "users"."name" #{expected} 'ma%'/,
69
- User.where { :name.ilike?('ma%') }.to_sql)
70
- end
71
-
72
- def test_casecmp
73
- assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Alice'\)/,
74
- User.where { :name.casecmp?('Alice') }.to_sql)
75
- end
76
-
77
- def test_casecmp_qualified
78
- assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Alice'\)/,
79
- User.where { :users[:name].casecmp?('Alice') }.to_sql)
80
- end
81
-
82
- def test_casecmp_nil_is_rejected
83
- e = assert_raises(ArgumentError) { User.where { :name.casecmp?(nil) } }
84
- assert_match(/null\?/, e.message)
85
- end
86
-
87
- def test_casecmp_execution
88
- User.delete_all
89
- User.create!(name: 'Alice')
90
- User.create!(name: 'bob')
91
- assert_equal(['Alice'], User.where { :name.casecmp?('aLiCe') }.pluck(:name))
92
- end
93
-
94
- def test_bang_negates_like
95
- assert_sql(/WHERE NOT \("users"."name" LIKE 'tender%'\)/,
96
- User.where { !:name.like?('tender%') }.to_sql)
97
- end
98
-
99
- def test_not_like
100
- assert_sql(/WHERE "users"."name" NOT LIKE 'tender%'/,
101
- User.where { :name.not_like?('tender%') }.to_sql)
102
- end
103
-
104
- def test_not_ilike
105
- expected = ADAPTER == 'postgresql' ? 'ILIKE' : 'LIKE'
106
- assert_sql(/WHERE "users"."name" NOT #{expected} 'tender%'/,
107
- User.where { :name.not_ilike?('tender%') }.to_sql)
108
- end
109
-
110
- def test_start_with
111
- assert_sql(/WHERE "users"."name" LIKE 'tender%' ESCAPE '\\'/,
112
- User.where { :name.start_with?('tender') }.to_sql)
113
- end
114
-
115
- def test_end_with
116
- assert_sql(/WHERE "users"."name" LIKE '%love' ESCAPE '\\'/,
117
- User.where { :name.end_with?('love') }.to_sql)
118
- end
119
-
120
- def test_include
121
- assert_sql(/WHERE "users"."name" LIKE '%der%' ESCAPE '\\'/,
122
- User.where { :name.include?('der') }.to_sql)
123
- end
124
-
125
- # Like their String namesakes, start_with? and end_with? take any number
126
- # of literals; matching any one of them is enough.
127
- def test_start_with_multiple
128
- assert_sql(
129
- /WHERE \("users"."name" LIKE 'al%' ESCAPE '\\' OR "users"."name" LIKE 'bo%' ESCAPE '\\'\)/,
130
- User.where { :name.start_with?('al', 'bo') }.to_sql)
131
- end
132
-
133
- def test_end_with_multiple
134
- assert_sql(
135
- /WHERE \("users"."name" LIKE '%z' ESCAPE '\\' OR "users"."name" LIKE '%love' ESCAPE '\\'\)/,
136
- User.where { :name.end_with?('z', 'love') }.to_sql)
137
- end
138
-
139
- # The OR arrives grouped, so a following & applies to the whole list.
140
- def test_start_with_multiple_combined
141
- assert_sql(
142
- /WHERE \("users"."name" LIKE 'al%' ESCAPE '\\' OR "users"."name" LIKE 'bo%' ESCAPE '\\'\) AND "users"."age" > 18/,
143
- User.where { :name.start_with?('al', 'bo') & (:age > 18) }.to_sql)
144
- end
145
-
146
- def test_start_with_no_arguments
147
- assert_raises(ArgumentError) { User.where { :name.start_with? } }
148
- end
149
-
150
- def test_end_with_no_arguments
151
- assert_raises(ArgumentError) { User.where { :name.end_with? } }
152
- end
153
-
154
- def test_start_with_escapes_wildcards
155
- assert_sql(/WHERE "users"."name" LIKE '100\\%\\_%' ESCAPE '\\'/,
156
- User.where { :name.start_with?('100%_') }.to_sql)
157
- end
158
-
159
- def test_include_escapes_wildcards
160
- assert_sql(/WHERE "users"."name" LIKE '%100\\%%' ESCAPE '\\'/,
161
- User.where { :name.include?('100%') }.to_sql)
162
- end
163
-
164
- def test_member
165
- assert_sql(/WHERE "users"."tags" @> '\{ruby\}'/,
166
- User.where { :tags.member?('ruby') }.to_sql)
167
- end
168
-
169
- def test_member_qualified
170
- assert_sql(/WHERE "users"."tags" @> '\{ruby\}'/,
171
- User.where { :users[:tags].member?('ruby') }.to_sql)
172
- end
173
-
174
- def test_member_negated
175
- assert_sql(/WHERE NOT \("users"."tags" @> '\{ruby\}'\)/,
176
- User.where { !:tags.member?('ruby') }.to_sql)
177
- end
178
-
179
- # Ruby's [1, 2].member?([1]) is false: member? tests one element, and an
180
- # Array argument would have to mean something the namesake does not.
181
- def test_member_array_is_rejected
182
- e = assert_raises(ArgumentError) { User.where { :tags.member?(%w[ruby rails]) } }
183
- assert_match(/superset\?/, e.message)
184
- end
185
-
186
- def test_superset
187
- assert_sql(/WHERE "users"."tags" @> '\{ruby,rails\}'/,
188
- User.where { :tags.superset?(%w[ruby rails]) }.to_sql)
189
- end
190
-
191
- def test_superset_takes_a_set
192
- assert_sql(/WHERE "users"."tags" @> '\{ruby,rails\}'/,
193
- User.where { :tags.superset?(Set['ruby', 'rails']) }.to_sql)
194
- end
195
-
196
- def test_superset_rejects_a_scalar
197
- assert_raises(ArgumentError) { User.where { :tags.superset?('ruby') } }
198
- end
199
-
200
- def test_subset
201
- assert_sql(/WHERE "users"."tags" <@ '\{ruby,rails,go\}'/,
202
- User.where { :tags.subset?(%w[ruby rails go]) }.to_sql)
203
- end
204
-
205
- def test_intersect
206
- assert_sql(/WHERE "users"."tags" && '\{ruby,go\}'/,
207
- User.where { :tags.intersect?(%w[ruby go]) }.to_sql)
208
- end
209
-
210
- def test_intersect_negated
211
- assert_sql(/WHERE NOT \("users"."tags" && '\{ruby,go\}'\)/,
212
- User.where { !:tags.intersect?(%w[ruby go]) }.to_sql)
213
- end
214
-
215
- def test_array_comparisons_execution
216
- skip_without_array_columns
217
- User.delete_all
218
- User.create!(name: 'both', tags: %w[ruby rails])
219
- User.create!(name: 'one', tags: %w[ruby go])
220
- User.create!(name: 'neither', tags: %w[python])
221
- assert_equal(['both'], User.where { :tags.superset?(%w[ruby rails]) }.pluck(:name))
222
- assert_equal(['neither'], User.where { :tags.subset?(%w[python js]) }.pluck(:name))
223
- assert_equal(%w[both one],
224
- User.where { :tags.intersect?(%w[ruby js]) }.pluck(:name).sort)
225
- end
226
-
227
- # MySQL additionally escapes the double quotes inside its string literal,
228
- # so the exact spelling is only asserted where the operator is real.
229
- def test_member_quotes_special_elements
230
- skip_without_array_columns
231
- assert_sql(/WHERE "users"."tags" @> '\{"with,comma"\}'/,
232
- User.where { :tags.member?('with,comma') }.to_sql)
233
- end
234
-
235
- # include? is a substring match even on an array column; only member?
236
- # means containment.
237
- def test_include_is_like_even_on_array_columns
238
- skip_without_array_columns
239
- assert_sql(/WHERE "users"."tags" LIKE '%ruby%' ESCAPE '\\'/,
240
- User.where { :tags.include?('ruby') }.to_sql)
241
- end
242
-
243
- # Elements survive the trip through the array literal: % is an ordinary
244
- # character there, a comma stays inside its element, and quotes and
245
- # backslashes are escaped.
246
- def test_member_matches_elements_literally
247
- skip_without_array_columns
248
- User.delete_all
249
- User.create!(name: 'literal', tags: ['100%', 'with,comma', 'q"uote', 'back\\slash'])
250
- User.create!(name: 'lookalike', tags: ['100200', 'with', 'comma'])
251
- assert_equal(['literal'], User.where { :tags.member?('100%') }.pluck(:name))
252
- assert_equal(['literal'], User.where { :tags.member?('with,comma') }.pluck(:name))
253
- assert_equal(['literal'], User.where { :tags.member?('q"uote') }.pluck(:name))
254
- assert_equal(['literal'], User.where { :tags.member?('back\\slash') }.pluck(:name))
255
- end
256
-
257
- def test_regexp
258
- skip_without_regexp_support
259
- assert_sql(/WHERE "users"."name" #{regexp_operator} '\^ma'/,
260
- User.where { :name =~ '^ma' }.to_sql)
261
- end
262
-
263
- def test_not_regexp
264
- skip_without_regexp_support
265
- assert_sql(/WHERE "users"."name" #{not_regexp_operator} '\^ma'/,
266
- User.where { :name !~ '^ma' }.to_sql)
267
- end
268
-
269
- def test_regexp_qualified
270
- skip_without_regexp_support
271
- assert_sql(/WHERE "users"."name" #{regexp_operator} '\^ma'/,
272
- User.where { :users[:name] =~ '^ma' }.to_sql)
273
- end
274
-
275
- def test_regexp_on_function
276
- skip_without_regexp_support
277
- assert_sql(/WHERE UPPER\("users"."name"\) #{regexp_operator} '\^MA'/,
278
- User.where { upper(:name) =~ '^MA' }.to_sql)
279
- end
280
-
281
- def test_regexp_literal
282
- skip_without_regexp_support
283
- assert_sql(/WHERE "users"."name" #{regexp_operator} 'love\$'/,
284
- User.where { :name =~ /love$/ }.to_sql)
285
- end
286
-
287
- # Rejected while the block runs, so this holds on every adapter.
288
- def test_regexp_literal_with_options_is_rejected
289
- assert_raises(ArgumentError) { User.where { :name =~ /^ma/i } }
290
- end
291
-
292
- def test_between
293
- assert_sql(/WHERE "users"."age" BETWEEN 18 AND 65/,
294
- User.where { :age.between?(18, 65) }.to_sql)
295
- end
296
-
297
- def test_in_range
298
- assert_sql(/WHERE "users"."age" BETWEEN 18 AND 65/,
299
- User.where { :age.in?(18..65) }.to_sql)
300
- end
301
-
302
- def test_in_endless_range
303
- assert_sql(/WHERE "users"."age" >= 18/,
304
- User.where { :age.in?(18..) }.to_sql)
305
- end
306
-
307
- def test_in_exclusive_range
308
- assert_sql(/WHERE "users"."age" >= 18 AND "users"."age" < 65/,
309
- User.where { :age.in?(18...65) }.to_sql)
310
- end
311
-
312
- def test_bang_negates_between
313
- assert_sql(/WHERE NOT \("users"."age" BETWEEN 18 AND 65\)/,
314
- User.where { !:age.between?(18, 65) }.to_sql)
315
- end
316
-
317
- # Arel spells the negation as the two comparisons rather than NOT BETWEEN,
318
- # which is the same set of rows, NULLs included.
319
- def test_not_between
320
- assert_sql(/WHERE \("users"."age" < 18 OR "users"."age" > 65\)/,
321
- User.where { :age.not_between?(18, 65) }.to_sql)
322
- end
323
-
324
- def test_not_in_range
325
- assert_sql(/WHERE \("users"."age" < 18 OR "users"."age" > 65\)/,
326
- User.where { :age.not_in?(18..65) }.to_sql)
327
- end
328
-
329
- def test_is_null
330
- assert_sql(/WHERE "users"."name" IS NULL/,
331
- User.where { :name.null? }.to_sql)
332
- end
333
-
334
- def test_is_null_qualified
335
- assert_sql(/WHERE "users"."name" IS NULL/,
336
- User.where { :users[:name].null? }.to_sql)
337
- end
338
-
339
- def test_is_not_null
340
- assert_sql(/WHERE "users"."name" IS NOT NULL/,
341
- User.where { :name.not_null? }.to_sql)
342
- end
343
-
344
- def test_is_not_null_qualified
345
- assert_sql(/WHERE "users"."name" IS NOT NULL/,
346
- User.where { :users[:name].not_null? }.to_sql)
347
- end
348
-
349
- # CASE has two shapes, and so does the block: an operand to compare each
350
- # `when` against, or a condition on every `when`.
351
- def test_case_with_an_operand
352
- assert_sql(/SELECT CASE "users"."age" WHEN 10 THEN 'ten' ELSE 'other' END AS "v"/,
353
- User.select { self.case(:age).when(10).then('ten').else('other').as(:v) }.to_sql)
354
- end
355
-
356
- def test_when_on_a_column_is_the_same_case
357
- assert_equal(
358
- User.select { self.case(:age).when(10).then('ten').else('other').as(:v) }.to_sql,
359
- User.select { :age.when(10).then('ten').else('other').as(:v) }.to_sql)
360
- end
361
-
362
- def test_searched_case
363
- assert_sql(/SELECT CASE WHEN "users"."age" >= 60 THEN 'senior' ELSE 'other' END AS "v"/,
364
- User.select { case_when { :age >= 60 }.then('senior').else('other').as(:v) }.to_sql)
365
- end
366
-
367
- def test_case_when_is_the_same_as_case_with_no_operand
368
- assert_equal(
369
- User.select { self.case.when { :age >= 60 }.then(1).else(0).as(:v) }.to_sql,
370
- User.select { case_when { :age >= 60 }.then(1).else(0).as(:v) }.to_sql)
371
- end
372
-
373
- # A value and a block say the same thing; the block is there to read like
374
- # the blocks around it.
375
- def test_a_condition_reads_the_same_either_way
376
- assert_equal(
377
- User.select { case_when(:age >= 60).then(1).else(0).as(:v) }.to_sql,
378
- User.select { case_when { :age >= 60 }.then(1).else(0).as(:v) }.to_sql)
379
- end
380
-
381
- def test_case_with_several_whens
382
- assert_sql(
383
- /CASE WHEN "users"."age" < 18 THEN 'minor' WHEN "users"."age" >= 60 THEN 'senior' ELSE 'adult' END/,
384
- User.select {
385
- case_when { :age < 18 }.then('minor').
386
- when { :age >= 60 }.then('senior').
387
- else('adult').as(:v)
388
- }.to_sql)
389
- end
390
-
391
- # Leaving the ELSE off is SQL's own default rather than an omission.
392
- def test_case_without_an_else
393
- sql = User.select { case_when { :age >= 60 }.then('senior').as(:v) }.to_sql
394
- assert_sql(/CASE WHEN "users"."age" >= 60 THEN 'senior' END/, sql)
395
- refute_match(/ELSE/, sql)
396
- end
397
-
398
- def test_case_takes_expressions_and_columns
399
- assert_sql(/THEN \("users"."age" - 60\)/,
400
- User.select { case_when { :age >= 60 }.then { :age - 60 }.else(0).as(:v) }.to_sql)
401
- assert_sql(/THEN "users"."name"/,
402
- User.select { case_when { :age >= 60 }.then(:name).else('x').as(:v) }.to_sql)
403
- end
404
-
405
- def test_case_is_an_expression_like_any_other
406
- assert_sql(/SUM\(CASE WHEN/,
407
- User.select { sum(case_when { :age >= 60 }.then(1).else(0)).as(:v) }.to_sql)
408
- assert_sql(/WHERE CASE "users"."age" WHEN 10 THEN 1 ELSE 2 END = 1/,
409
- User.where { self.case(:age).when(10).then(1).else(2) == 1 }.to_sql)
410
- end
411
-
412
- def test_case_execution
413
- User.delete_all
414
- User.create!(name: 'senior', age: 70)
415
- User.create!(name: 'adult', age: 30)
416
- User.create!(name: 'minor', age: 10)
417
- assert_equal(%w[adult minor senior],
418
- User.select {
419
- case_when { :age < 18 }.then('minor').
420
- when { :age >= 60 }.then('senior').
421
- else('adult').as(:v)
422
- }.map(&:v).sort)
423
- end
424
-
425
- # One case finished two ways: the methods return new nodes rather than
426
- # adding to the one they were called on.
427
- def test_a_case_is_not_added_to_in_place
428
- sql = User.select {
429
- started = case_when { :age >= 60 }.then(1)
430
- [started.else(0).as(:a), started.else(9).as(:b)]
431
- }.to_sql
432
- assert_sql(/THEN 1 ELSE 0 END AS "a"/, sql)
433
- assert_sql(/THEN 1 ELSE 9 END AS "b"/, sql)
434
- end
435
-
436
- def test_when_needs_a_value_or_a_block
437
- assert_raises(ArgumentError) { User.select { case_when.then(1) } }
438
- e = assert_raises(ArgumentError) { User.select { case_when(1) { 2 }.then(1) } }
439
- assert_match(/not both/, e.message)
440
- end
441
-
442
- def test_when_needs_a_matching_then
443
- e = assert_raises(ArgumentError) { User.select { :age.when(10) }.to_sql }
444
- assert_match(/matching then/, e.message)
445
- end
446
-
447
- # Kernel#then would otherwise answer this one, with no block and no noise.
448
- def test_then_without_a_when_says_so
449
- e = assert_raises(ArgumentError) { User.select { self.case(:age).then(1) } }
450
- assert_match(/follows a when/, e.message)
451
- end
452
-
453
- # A window is built by chaining, the way Arel's own is.
454
- def test_over_with_no_window
455
- assert_sql(/SELECT AVG\("users"."age"\) OVER \(\) AS "v"/,
456
- User.select { avg(:age).over.as(:v) }.to_sql)
457
- end
458
-
459
- def test_over_partition_and_order
460
- assert_sql(
461
- /AVG\("users"."age"\) OVER \(PARTITION BY "users"."name" ORDER BY "users"."age" DESC\)/,
462
- User.select { avg(:age).over.partition(:name).order(:age.desc).as(:v) }.to_sql)
463
- end
464
-
465
- def test_over_takes_several_expressions
466
- assert_sql(/PARTITION BY "users"."name", "users"."age"/,
467
- User.select { count(:*).over.partition(:name, :age).as(:v) }.to_sql)
468
- end
469
-
470
- # The window-only functions, which the adapters that have them at all spell
471
- # the same way.
472
- def test_window_functions
473
- assert_sql(/ROW_NUMBER\(\) OVER \(ORDER BY "users"."age"\)/,
474
- User.select { row_number.over.order(:age).as(:v) }.to_sql)
475
- assert_sql(/RANK\(\) OVER/, User.select { rank.over.order(:age).as(:v) }.to_sql)
476
- assert_sql(/NTILE\(2\) OVER/, User.select { ntile(2).over.order(:age).as(:v) }.to_sql)
477
- assert_sql(/LAG\("users"."age", 1\) OVER/,
478
- User.select { lag(:age).over.order(:age).as(:v) }.to_sql)
479
- assert_sql(/LAG\("users"."age", 2, 0\) OVER/,
480
- User.select { lag(:age, 2, 0).over.order(:age).as(:v) }.to_sql)
481
- end
482
-
483
- # A frame is a range of rows counted from the current one: negative before
484
- # it, positive after, 0 the row itself, an open end for unbounded.
485
- def test_window_frames
486
- assert_sql(/ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW/,
487
- User.select { sum(:age).over.order(:age).rows(..0).as(:v) }.to_sql)
488
- assert_sql(/ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING/,
489
- User.select { sum(:age).over.order(:age).rows(-1..1).as(:v) }.to_sql)
490
- assert_sql(/ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING/,
491
- User.select { sum(:age).over.order(:age).rows(0..).as(:v) }.to_sql)
492
- assert_sql(/RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW/,
493
- User.select { sum(:age).over.order(:age).range(..0).as(:v) }.to_sql)
494
- end
495
-
496
- def test_over_is_an_expression_like_any_other
497
- assert_sql(/\(RANK\(\) OVER \(ORDER BY "users"."age"\) \+ 1\) AS "v"/,
498
- User.select { (rank.over.order(:age) + 1).as(:v) }.to_sql)
499
- end
500
-
501
- def test_window_execution
502
- User.delete_all
503
- User.create!(name: 'a', age: 20)
504
- User.create!(name: 'b', age: 30)
505
- User.create!(name: 'c', age: 40)
506
- assert_equal([1, 2, 3],
507
- User.select { row_number.over.order(:age).as(:v) }.map {|u| u.v.to_i })
508
- assert_equal([20, 50, 90],
509
- User.select { sum(:age).over.order(:age).rows(..0).as(:v) }.map {|u| u.v.to_i })
510
- end
511
-
512
- # One window finished two ways: the methods return new nodes.
513
- def test_a_window_is_not_added_to_in_place
514
- sql = User.select {
515
- started = sum(:age).over.order(:age)
516
- [started.partition(:name).as(:a), started.as(:b)]
517
- }.to_sql
518
- assert_sql(/PARTITION BY "users"."name" ORDER BY "users"."age"\) AS "a"/, sql)
519
- assert_sql(/SUM\("users"."age"\) OVER \(ORDER BY "users"."age"\) AS "b"/, sql)
520
- end
521
-
522
- def test_a_window_function_needs_over
523
- e = assert_raises(ArgumentError) { User.select { row_number.as(:v) }.to_sql }
524
- assert_match(/needs over/, e.message)
525
- end
526
-
527
- def test_a_window_has_one_frame
528
- assert_raises(ArgumentError) { User.select { sum(:age).over.rows(..0).range(..0) } }
529
- end
530
-
531
- def test_a_frame_is_a_range_of_rows
532
- assert_raises(ArgumentError) { User.select { sum(:age).over.rows(3) } }
533
- assert_raises(ArgumentError) { User.select { sum(:age).over.rows('a'..'b') } }
534
- e = assert_raises(ArgumentError) { User.select { sum(:age).over.rows(-2...0) } }
535
- assert_match(/ends on a row/, e.message)
536
- end
537
-
538
- def test_partition_needs_an_expression
539
- assert_raises(ArgumentError) { User.select { sum(:age).over.partition } }
540
- assert_raises(ArgumentError) { User.select { sum(:age).over.order } }
541
- end
542
-
543
- def test_case_needs_a_when
544
- e = assert_raises(ArgumentError) { User.select { self.case(:age).else(1) }.to_sql }
545
- assert_match(/needs a when/, e.message)
546
- end
547
-
548
- # The claim these methods rest on: the direct spelling is the same rows as
549
- # negating the positive one, which is where a NULL would show a difference
550
- # if there were one.
551
- def test_the_negations_match_what_bang_selects
552
- User.delete_all
553
- User.create!(name: 'alice', age: 60, active: true)
554
- User.create!(name: 'bob', age: 20, active: false)
555
- User.create!(name: nil, age: 40)
556
- [
557
- [-> { :name.not_null? }, -> { !:name.null? }],
558
- [-> { :age.not_in?([20, 30]) }, -> { !:age.in?([20, 30]) }],
559
- [-> { :age.not_between?(20, 30) }, -> { !:age.between?(20, 30) }],
560
- [-> { :name.not_like?('a%') }, -> { !:name.like?('a%') }],
561
- [-> { :active.not_true? }, -> { !:active.true? }],
562
- [-> { :active.not_false? }, -> { !:active.false? }],
563
- ].each do |direct, negated|
564
- assert_equal(User.where(&negated).pluck(:id).sort,
565
- User.where(&direct).pluck(:id).sort,
566
- "#{direct.source_location} did not match the ! form")
567
- end
568
- end
569
-
570
- def test_equal_nil_is_rejected
571
- e = assert_raises(ArgumentError) { User.where { :name == nil } }
572
- assert_match(/null\?/, e.message)
573
- end
574
-
575
- def test_not_equal_nil_is_rejected
576
- e = assert_raises(ArgumentError) { User.where { :name != nil } }
577
- assert_match(/null\?/, e.message)
578
- end
579
-
580
- def test_is_true
581
- assert_sql(/WHERE "users"."active" IS TRUE/, User.where { :active.true? }.to_sql)
582
- end
583
-
584
- def test_is_not_true
585
- assert_sql(/WHERE "users"."active" IS NOT TRUE/, User.where { :active.not_true? }.to_sql)
586
- end
587
-
588
- def test_is_false
589
- assert_sql(/WHERE "users"."active" IS FALSE/, User.where { :active.false? }.to_sql)
590
- end
591
-
592
- def test_is_not_false
593
- assert_sql(/WHERE "users"."active" IS NOT FALSE/, User.where { :active.not_false? }.to_sql)
594
- end
595
-
596
- # The four are spelled and answered the same way by every adapter, NULL
597
- # included, which is what makes them worth having over = TRUE.
598
- def test_truth_values_execution
599
- User.delete_all
600
- User.create!([{name: 'yes', active: true}, {name: 'no', active: false},
601
- {name: 'unset', active: nil}])
602
- order = ->(relation) { relation.order(:name).pluck(:name) }
603
- assert_equal(['yes'], order.(User.where { :active.true? }))
604
- assert_equal(%w[no unset], order.(User.where { :active.not_true? }))
605
- assert_equal(['no'], order.(User.where { :active.false? }))
606
- assert_equal(%w[unset yes], order.(User.where { :active.not_false? }))
607
- end
608
-
609
- # Where the difference from a comparison against the literal shows: = TRUE
610
- # is NULL for a NULL row, and negating it leaves that row out.
611
- def test_not_true_keeps_the_nulls_equality_drops
612
- User.delete_all
613
- User.create!([{name: 'no', active: false}, {name: 'unset', active: nil}])
614
- assert_equal(%w[no unset], User.where { :active.not_true? }.order(:name).pluck(:name))
615
- assert_equal(['no'], User.where { !(:active == true) }.order(:name).pluck(:name))
616
- end
617
-
618
- def test_in
619
- assert_sql(/WHERE "users"."age" IN \(1, 2, 3\)/,
620
- User.where { :age.in?([1, 2, 3]) }.to_sql)
621
- end
622
-
623
- def test_in_qualified
624
- assert_sql(/WHERE "users"."age" IN \(1, 2, 3\)/,
625
- User.where { :users[:age].in?([1, 2, 3]) }.to_sql)
626
- end
627
-
628
- def test_bang_negates_in
629
- assert_sql(/WHERE NOT \("users"."age" IN \(1, 2, 3\)\)/,
630
- User.where { !:age.in?([1, 2, 3]) }.to_sql)
631
- end
632
-
633
- def test_not_in
634
- assert_sql(/WHERE "users"."age" NOT IN \(1, 2, 3\)/,
635
- User.where { :age.not_in?([1, 2, 3]) }.to_sql)
636
- end
637
-
638
- def test_not_in_qualified
639
- assert_sql(/WHERE "users"."age" NOT IN \(1, 2, 3\)/,
640
- User.where { :users[:age].not_in?([1, 2, 3]) }.to_sql)
641
- end
642
-
643
- # Spelled IS [NOT] DISTINCT FROM on PostgreSQL, IS / IS NOT on SQLite and
644
- # <=> on MySQL, so only the resulting rows are portable.
645
- def test_not_distinct_from_execution
646
- User.delete_all
647
- User.create!(name: 'named')
648
- User.create!(name: nil)
649
- assert_equal([nil], User.where { :name.not_distinct_from?(nil) }.pluck(:name))
650
- assert_equal(['named'], User.where { :name.distinct_from?(nil) }.pluck(:name))
651
- end
652
-
653
- def test_not_distinct_from_a_value_execution
654
- User.delete_all
655
- User.create!(name: 'alice')
656
- User.create!(name: nil)
657
- assert_equal(['alice'], User.where { :name.not_distinct_from?('alice') }.pluck(:name))
658
- # Unlike !=, this keeps the NULL row.
659
- assert_equal([nil], User.where { :name.distinct_from?('alice') }.pluck(:name))
660
- end
661
-
662
- def test_distinct_from_postgresql_syntax
663
- skip "#{ADAPTER} spells it differently" unless ADAPTER == 'postgresql'
664
- assert_sql(/WHERE "users"."name" IS NOT DISTINCT FROM 'x'/,
665
- User.where { :name.not_distinct_from?('x') }.to_sql)
666
- assert_sql(/WHERE "users"."name" IS DISTINCT FROM 'x'/,
667
- User.where { :name.distinct_from?('x') }.to_sql)
668
- end
669
-
670
- def test_comparison_with_scalar_subquery
671
- assert_sql(/WHERE "users"."age" >= \(SELECT AVG\("users"."age"\) FROM "users"\)/,
672
- User.where { :age >= User.select { avg(:age) } }.to_sql)
673
- end
674
-
675
- def test_equality_with_scalar_subquery
676
- assert_sql(/WHERE "users"."age" = \(SELECT MAX\("users"."age"\) FROM "users"\)/,
677
- User.where { :age == User.select { max(:age) } }.to_sql)
678
- end
679
-
680
- # A scalar comparison has no sensible default select list, unlike in?.
681
- def test_scalar_subquery_without_select_is_rejected
682
- e = assert_raises(ArgumentError) { User.where { :age >= User.all } }
683
- assert_match(/select/, e.message)
684
- end
685
-
686
- def test_scalar_subquery_execution
687
- User.delete_all
688
- User.create!(name: 'young', age: 20)
689
- User.create!(name: 'old', age: 60)
690
- assert_equal(['old'], User.where { :age >= User.select { avg(:age) } }.pluck(:name))
691
- end
692
-
693
- def test_in_subquery
694
- assert_sql(
695
- /WHERE "authors"."id" IN \(SELECT "posts"."author_id" FROM "posts" WHERE "posts"."title" = 'pub'\)/,
696
- Author.where { :id.in?(Post.where(title: 'pub').select(:author_id)) }.to_sql)
697
- end
698
-
699
- # A relation without an explicit select list selects its primary key, the
700
- # same way Active Record's own where(id: relation) does.
701
- def test_in_subquery_selects_primary_key_by_default
702
- assert_sql(/WHERE "authors"."id" IN \(SELECT "posts"."id" FROM "posts"\)/,
703
- Author.where { :id.in?(Post.all) }.to_sql)
704
- end
705
-
706
- def test_not_in_subquery
707
- assert_sql(/WHERE NOT \("authors"."id" IN \(SELECT "posts"."author_id" FROM "posts"\)\)/,
708
- Author.where { !:id.in?(Post.select(:author_id)) }.to_sql)
709
- end
710
-
711
- def test_any_subquery
712
- skip_without_quantifiers
713
- assert_sql(
714
- /WHERE "users"."age" > ANY\(SELECT "users"."age" FROM "users" WHERE "users"."name" = 'alice'\)/,
715
- User.where { :age > any(User.where(name: 'alice').select(:age)) }.to_sql)
716
- end
717
-
718
- def test_all_subquery
719
- skip_without_quantifiers
720
- assert_sql(/WHERE "users"."age" >= ALL\(SELECT "users"."age" FROM "users"\)/,
721
- User.where { :age >= all(User.select(:age)) }.to_sql)
722
- end
723
-
724
- # The same default in? has, since both take the relation for a set of rows.
725
- def test_quantifier_selects_primary_key_by_default
726
- skip_without_quantifiers
727
- assert_sql(/WHERE "authors"."id" > ANY\(SELECT "posts"."id" FROM "posts"\)/,
728
- Author.where { :id > any(Post.all) }.to_sql)
729
- end
730
-
731
- # A list is what in? takes; ANY of one is what a plain comparison says.
732
- def test_quantifier_without_a_relation_is_rejected
733
- skip_without_quantifiers
734
- e = assert_raises(ArgumentError) { User.where { :age > any([20, 30]) } }
735
- assert_match(/relation/, e.message)
736
- end
737
-
738
- def test_quantifier_is_unsupported_on_sqlite
739
- if ADAPTER == 'sqlite3'
740
- e = assert_raises(NotImplementedError) { User.where { :age > any(User.select(:age)) } }
741
- assert_match(/ANY/, e.message)
742
- else
743
- assert_sql(/> ANY\(SELECT/, User.where { :age > any(User.select(:age)) }.to_sql)
744
- end
745
- end
746
-
747
- # ANY is satisfied by one row of the subquery and ALL by every row, so the
748
- # two pick out the ends of the range the subquery covers.
749
- def test_quantifier_execution
750
- skip_without_quantifiers
751
- User.delete_all
752
- User.create!([{name: 'young', age: 20}, {name: 'middle', age: 40},
753
- {name: 'old', age: 60}])
754
- ages = -> { User.select(:age) }
755
- assert_equal(%w[middle old], User.where { :age > any(ages.call) }.order(:age).pluck(:name))
756
- assert_equal(['old'], User.where { :age >= all(ages.call) }.pluck(:name))
757
- assert_equal(['young'], User.where { :age <= all(ages.call) }.pluck(:name))
758
- end
759
-
760
- # = ANY is IN and != ALL is NOT IN, which is worth a test because it is the
761
- # part of the quantifiers the gem already had another spelling for.
762
- def test_quantifier_equality_execution
763
- skip_without_quantifiers
764
- User.delete_all
765
- User.create!([{name: 'young', age: 20}, {name: 'old', age: 60}])
766
- young = -> { User.where(name: 'young').select(:age) }
767
- assert_equal(['young'], User.where { :age == any(young.call) }.pluck(:name))
768
- assert_equal(['old'], User.where { :age != all(young.call) }.pluck(:name))
769
- end
770
-
771
- # The subquery correlates with the outer table through qualified columns,
772
- # and its own where block goes through the DSL too.
773
- def test_exists
774
- assert_sql(
775
- /WHERE EXISTS \(SELECT "posts"\.\* FROM "posts" WHERE "posts"."author_id" = "authors"."id"\)/,
776
- Author.where { exists?(Post.where { :posts[:author_id] == :authors[:id] }) }.to_sql)
777
- end
778
-
779
- def test_not_exists
780
- assert_sql(/WHERE NOT \(EXISTS \(SELECT "posts"\.\* FROM "posts"\)\)/,
781
- Author.where { !exists?(Post.all) }.to_sql)
782
- end
783
-
784
- def test_exists_combined
785
- assert_sql(
786
- /WHERE "authors"."name" = 'alice' AND EXISTS \(SELECT "posts"\.\* FROM "posts" WHERE "posts"."title" = 'pub'\)/,
787
- Author.where { (:name == 'alice') & exists?(Post.where(title: 'pub')) }.to_sql)
788
- end
789
-
790
- def test_exists_execution
791
- Author.delete_all
792
- Post.delete_all
793
- with_post = Author.create!(name: 'with_post')
794
- Author.create!(name: 'without')
795
- Post.create!(title: 'pub', author_id: with_post.id)
796
- correlated = -> { Post.where { :posts[:author_id] == :authors[:id] } }
797
- assert_equal(['with_post'],
798
- Author.where { exists?(correlated.call) }.pluck(:name))
799
- assert_equal(['without'],
800
- Author.where { !exists?(correlated.call) }.pluck(:name))
801
- end
802
-
803
- def test_in_subquery_execution
804
- Author.delete_all
805
- Post.delete_all
806
- published = Author.create!(name: 'published')
807
- drafting = Author.create!(name: 'drafting')
808
- Post.create!(title: 'pub', author_id: published.id)
809
- Post.create!(title: 'draft', author_id: drafting.id)
810
- subquery = -> { Post.where(title: 'pub').select(:author_id) }
811
- assert_equal(['published'],
812
- Author.where { :id.in?(subquery.call) }.pluck(:name))
813
- assert_equal(['drafting'],
814
- Author.where { !:id.in?(subquery.call) }.pluck(:name))
815
- end
816
-
817
- # == passes a Range or an Array through as a value rather than expanding it,
818
- # so that it compares against a PostgreSQL range or array column. The SQL
819
- # literal depends on the column type, so assert on the Arel node instead.
820
- def test_equal_range_is_an_equality
821
- node = ActiveRecord::Refined::AST::Comparison.new(:period, :==, 18..65).
822
- to_arel(User.arel_table, User)
823
- assert_instance_of(Arel::Nodes::Equality, node)
824
- assert_equal(18..65, node.right.value)
825
- end
826
-
827
- def test_equal_array_is_an_equality
828
- node = ActiveRecord::Refined::AST::Comparison.new(:tags, :==, [1, 2, 3]).
829
- to_arel(User.arel_table, User)
830
- assert_instance_of(Arel::Nodes::Equality, node)
831
- assert_equal([1, 2, 3], node.right.value)
832
- end
833
-
834
- def test_not_equal_array_is_an_inequality
835
- node = ActiveRecord::Refined::AST::Comparison.new(:tags, :!=, [1, 2, 3]).
836
- to_arel(User.arel_table, User)
837
- assert_instance_of(Arel::Nodes::NotEqual, node)
838
- assert_equal([1, 2, 3], node.right.value)
839
- end
840
-
841
- def test_qualified_column
842
- assert_sql(/WHERE "users"."name" = 'alice'/,
843
- User.where { :users[:name] == 'alice' }.to_sql)
844
- end
845
-
846
- def test_column_to_column_comparison
847
- sql = User.where { :users[:name] == :users[:age] }.to_sql
848
- assert_sql(/"users"."name" = "users"."age"/, sql)
849
- end
850
-
851
- def test_joins_with_block
852
- sql = Author.joins(:posts) { :posts[:author_id] == :authors[:id] }.to_sql
853
- assert_sql(/INNER JOIN "posts" ON "posts"."author_id" = "authors"."id"/, sql)
854
- end
855
-
856
- def test_left_outer_joins_with_block
857
- sql = Author.left_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }.to_sql
858
- assert_sql(/LEFT OUTER JOIN "posts" ON "posts"."author_id" = "authors"."id"/, sql)
859
- end
860
-
861
- # The alias is what the block's qualified columns name, which is what makes
862
- # a self join expressible at all. Adapters differ on writing the AS
863
- # keyword, so the assertions allow either.
864
- def test_joins_with_alias
865
- assert_sql(
866
- /INNER JOIN "authors" (?:AS )?"mentors" ON "mentors"."id" = "authors"."id"/,
867
- Author.joins(:authors, as: :mentors) { :mentors[:id] == :authors[:id] }.to_sql)
868
- end
869
-
870
- def test_left_outer_joins_with_alias
871
- assert_sql(
872
- /LEFT OUTER JOIN "authors" (?:AS )?"mentors" ON "mentors"."id" = "authors"."id"/,
873
- Author.left_outer_joins(:authors, as: :mentors) { :mentors[:id] == :authors[:id] }.to_sql)
874
- end
875
-
876
- def test_joins_alias_needs_a_block
877
- assert_raises(ArgumentError) { Author.joins(:posts, as: :p) }
878
- assert_raises(ArgumentError) { Author.left_outer_joins(:posts, as: :p) }
879
- end
880
-
881
- # The other two outer joins, which Active Record has no method for. MySQL
882
- # has no FULL OUTER JOIN either, and says so before the SQL is built.
883
- def test_right_outer_joins_with_block
884
- assert_sql(/RIGHT OUTER JOIN "posts" ON "posts"."author_id" = "authors"."id"/,
885
- Author.right_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }.to_sql)
886
- end
887
-
888
- def test_full_outer_joins_with_block
889
- skip_without_full_outer_joins
890
- assert_sql(/FULL OUTER JOIN "posts" ON "posts"."author_id" = "authors"."id"/,
891
- Author.full_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }.to_sql)
892
- end
893
-
894
- def test_full_outer_joins_says_where_it_cannot_go
895
- skip "#{ADAPTER} has FULL OUTER JOIN" unless ADAPTER == 'mysql2'
896
- e = assert_raises(NotImplementedError) do
897
- Author.full_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }
898
- end
899
- assert_match(/no equivalent on MySQL/, e.message)
900
- end
901
-
902
- def test_right_outer_joins_with_alias
903
- assert_sql(
904
- /RIGHT OUTER JOIN "authors" (?:AS )?"mentors" ON "mentors"."id" = "authors"."id"/,
905
- Author.right_outer_joins(:authors, as: :mentors) { :mentors[:id] == :authors[:id] }.to_sql)
906
- end
907
-
908
- # An association is what joins and left_outer_joins read; there is nothing
909
- # for these two to read one as.
910
- def test_the_other_outer_joins_need_a_block
911
- e = assert_raises(ArgumentError) { Author.right_outer_joins(:posts) }
912
- assert_match(/takes a table and the block/, e.message)
913
- unless ADAPTER == 'mysql2'
914
- assert_raises(ArgumentError) { Author.full_outer_joins(:posts) }
915
- end
916
- end
917
-
918
- # CROSS JOIN: every row against every row, so there is no condition to give.
919
- def test_cross_joins
920
- assert_sql(/FROM "authors" CROSS JOIN "posts"/, Author.cross_joins(:posts).to_sql)
921
- end
922
-
923
- def test_cross_joins_with_alias
924
- assert_sql(/CROSS JOIN "authors" "others"/,
925
- Author.cross_joins(:authors, as: :others).to_sql)
926
- end
927
-
928
- def test_cross_joins_takes_no_block
929
- e = assert_raises(ArgumentError) { Author.cross_joins(:posts) { :id == 1 } }
930
- assert_match(/no condition/, e.message)
931
- end
932
-
933
- def test_cross_joins_execution
934
- Author.delete_all
935
- Post.delete_all
936
- Author.create!(name: 'a')
937
- Author.create!(name: 'b')
938
- Post.create!(title: 'one')
939
- Post.create!(title: 'two')
940
- Post.create!(title: 'three')
941
- assert_equal(6, Author.cross_joins(:posts).count)
942
- end
943
-
944
- def test_right_outer_joins_execution
945
- Author.delete_all
946
- Post.delete_all
947
- author = Author.create!(name: 'a')
948
- Post.create!(title: 'hers', author_id: author.id)
949
- Post.create!(title: 'nobody\'s', author_id: nil)
950
- assert_equal(['a', nil],
951
- Author.right_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }.
952
- order { :posts[:title] }.pluck(:'authors.name'))
953
- end
954
-
955
- def test_joins_without_alias_still_delegates
956
- assert_sql(/INNER JOIN "posts" ON "posts"."author_id" = "authors"."id"/,
957
- Author.joins(:posts).to_sql)
958
- end
959
-
960
- def test_self_join_execution
961
- Author.delete_all
962
- Author.create!(name: 'shared')
963
- Author.create!(name: 'other')
964
- assert_equal(%w[other shared],
965
- Author.joins(:authors, as: :mentors) { :mentors[:name] == :authors[:name] }.
966
- pluck(:name).sort)
967
- end
968
-
969
- # Active Record's from only takes a table name as a string.
970
- def test_from_symbol
971
- assert_sql(/FROM "tree"/, Node.from(:tree).to_sql)
972
- end
973
-
974
- def test_from_symbol_with_alias
975
- assert_sql(/FROM "tree" (?:AS )?"nodes"/, Node.from(:tree, as: :nodes).to_sql)
976
- end
977
-
978
- def test_from_string_still_delegates
979
- assert_sql(/FROM subq/, Node.from('subq').to_sql)
980
- end
981
-
982
- def test_from_alias_needs_a_symbol
983
- assert_raises(ArgumentError) { Node.from('tree', as: :nodes) }
984
- end
985
-
986
- def test_from_cte_takes_the_alias_from_the_model
987
- declared = Node.with(tree: Node.all)
988
- assert_sql(/FROM "tree" (?:AS )?"nodes"/, declared.from_cte(:tree).to_sql)
989
- assert_equal(declared.from(:tree, as: :nodes).to_sql,
990
- declared.from_cte(:tree).to_sql)
991
- end
992
-
993
- def test_from_cte_needs_a_symbol
994
- assert_raises(ArgumentError) { Node.from_cte('tree') }
995
- end
996
-
997
- # The name has to be one `with` declares, or the query is against a table
998
- # nobody has -- which the database would say much later and less clearly.
999
- def test_from_cte_needs_a_cte_of_that_name
1000
- e = assert_raises(ArgumentError) do
1001
- Node.with(tree: Node.all).from_cte(:tre).to_sql
1002
- end
1003
- assert_match(/names no CTE/, e.message)
1004
- assert_match(/:tree/, e.message)
1005
-
1006
- e = assert_raises(ArgumentError) { Node.from_cte(:tree).to_sql }
1007
- assert_match(/declares none/, e.message)
1008
- end
1009
-
1010
- # Checked when the SQL is built, so where the CTE is declared in the chain
1011
- # does not matter.
1012
- def test_from_cte_takes_a_cte_declared_later
1013
- assert_sql(/FROM "tree" (?:AS )?"nodes"/,
1014
- Node.from_cte(:tree).with(tree: Node.all).to_sql)
1015
- assert_sql(/FROM "tree" (?:AS )?"nodes"/,
1016
- Node.from_cte(:tree).merge(Node.with(tree: Node.all)).to_sql)
1017
- end
1018
-
1019
- # from itself says nothing about CTEs and goes on taking any table.
1020
- def test_from_with_an_alias_is_not_checked
1021
- assert_sql(/FROM "tree" (?:AS )?"nodes"/, Node.from(:tree, as: :nodes).to_sql)
1022
- end
1023
-
1024
- # The alias is what lets a where find its column, which is the whole reason
1025
- # from_cte exists; without it the SQL names a table the query does not have.
1026
- def test_from_cte_leaves_where_able_to_qualify
1027
- Node.delete_all
1028
- root = Node.create!(name: 'root')
1029
- Node.create!(name: 'child', parent_id: root.id)
1030
- other = Node.create!(name: 'other root')
1031
- Node.create!(name: 'other child', parent_id: other.id)
1032
- forest = Node.with_recursive(
1033
- tree: [
1034
- Node.where { :parent_id.null? }.
1035
- select { [:id, :name, :parent_id, :id.as(:root_id)] },
1036
- Node.joins(:tree) { :nodes[:parent_id] == :tree[:id] }.
1037
- select { [:nodes[:id], :nodes[:name], :nodes[:parent_id],
1038
- :tree[:root_id]] },
1039
- ]
1040
- ).from_cte(:tree)
1041
- assert_equal(%w[child root],
1042
- forest.where { :root_id == root.id }.pluck(:name).sort)
1043
- end
1044
-
1045
- # A CTE is joined by name like any other table, so the recursive member's
1046
- # ON clause is a block rather than the string join Rails' own docs use.
1047
- def test_recursive_cte
1048
- Node.delete_all
1049
- root = Node.create!(name: 'root')
1050
- child = Node.create!(name: 'child', parent_id: root.id)
1051
- Node.create!(name: 'grandchild', parent_id: child.id)
1052
- Node.create!(name: 'unrelated', parent_id: nil)
1053
- descendants = Node.with_recursive(
1054
- tree: [
1055
- Node.where { :id == root.id },
1056
- Node.joins(:tree) { :nodes[:parent_id] == :tree[:id] },
1057
- ]
1058
- ).from(:tree, as: :nodes)
1059
- assert_equal(%w[child grandchild root], descendants.pluck(:name).sort)
1060
- end
1061
-
1062
- def test_cte_joined_by_name
1063
- Node.delete_all
1064
- root = Node.create!(name: 'root')
1065
- Node.create!(name: 'child', parent_id: root.id)
1066
- Node.create!(name: 'orphan', parent_id: nil)
1067
- q = Node.with(roots: Node.where { :parent_id.null? }).
1068
- joins(:roots) { :roots[:id] == :nodes[:parent_id] }
1069
- assert_equal(['child'], q.pluck(:name))
1070
- end
1071
-
1072
- def test_select_sum
1073
- assert_sql(/SELECT SUM\("users"."age"\)/, User.select { sum(:age) }.to_sql)
1074
- end
1075
-
1076
- def test_select_aggregate_of_qualified_column
1077
- assert_sql(/SELECT COUNT\("users"."id"\)/,
1078
- User.select { count(:users[:id]) }.to_sql)
1079
- end
1080
-
1081
- def test_select_max_and_min
1082
- assert_sql(/SELECT MAX\("users"."age"\)/, User.select { max(:age) }.to_sql)
1083
- assert_sql(/SELECT MIN\("users"."age"\)/, User.select { min(:age) }.to_sql)
1084
- end
1085
-
1086
- # An aggregate is written as a call, the way SQL writes it; a column has no
1087
- # method of its own for one.
1088
- def test_aggregates_have_no_postfix_form
1089
- assert_raises(NoMethodError) { User.select { :age.sum } }
1090
- assert_raises(NoMethodError) { User.select { :age.average } }
1091
- end
1092
-
1093
- def test_having_aggregate
1094
- sql = User.group(:name).having { sum(:age) > 100 }.to_sql
1095
- assert_sql(/GROUP BY "users"."name"/, sql)
1096
- assert_sql(/HAVING SUM\("users"."age"\) > 100/, sql)
1097
- end
1098
-
1099
- def test_select_avg_function
1100
- assert_sql(/SELECT AVG\("users"."age"\)/,
1101
- User.select { avg(:age) }.to_sql)
1102
- end
1103
-
1104
- def test_select_count_function
1105
- assert_sql(/SELECT COUNT\("users"."id"\)/,
1106
- User.select { count(:id) }.to_sql)
1107
- end
1108
-
1109
- def test_select_count_star
1110
- assert_sql(/SELECT COUNT\(\*\)/,
1111
- User.select { count(:*) }.to_sql)
1112
- end
1113
-
1114
- def test_select_count_star_alias
1115
- assert_sql(/SELECT COUNT\(\*\) AS "cnt"/,
1116
- User.select { count(:*).as(:cnt) }.to_sql)
1117
- end
1118
-
1119
- def test_count_distinct
1120
- assert_sql(/SELECT COUNT\(DISTINCT "users"."name"\)/,
1121
- User.select { count(:name, distinct: true) }.to_sql)
1122
- end
1123
-
1124
- def test_count_distinct_aliased
1125
- assert_sql(/SELECT COUNT\(DISTINCT "users"."name"\) AS "n"/,
1126
- User.select { count(:name, distinct: true).as(:n) }.to_sql)
1127
- end
1128
-
1129
- def test_count_distinct_in_having
1130
- assert_sql(/HAVING COUNT\(DISTINCT "users"."name"\) > 1/,
1131
- User.group(:age).having { count(:name, distinct: true) > 1 }.to_sql)
1132
- end
1133
-
1134
- # DISTINCT is Arel's only aggregate modifier, and COUNT(DISTINCT *) is not
1135
- # valid SQL.
1136
- def test_distinct_is_rejected_for_other_aggregates
1137
- assert_raises(ArgumentError) do
1138
- ActiveRecord::Refined::AST::Aggregate.new(:age, :sum, distinct: true)
1139
- end
1140
- end
1141
-
1142
- def test_count_star_distinct_is_rejected
1143
- assert_raises(ArgumentError) { User.select { count(:*, distinct: true) } }
1144
- end
1145
-
1146
- def test_having_count_star
1147
- sql = Author.joins(:posts) { :posts[:author_id] == :authors[:id] }.
1148
- group { :authors[:id] }.
1149
- having { count(:*) > 1 }.to_sql
1150
- assert_sql(/HAVING COUNT\(\*\) > 1/, sql)
1151
- end
1152
-
1153
- def test_order_count_star
1154
- assert_sql(/ORDER BY COUNT\(\*\) DESC/,
1155
- User.group(:name).order { count(:*).desc }.to_sql)
1156
- end
1157
-
1158
- def test_select_sum_function
1159
- assert_sql(/SELECT SUM\("users"."age"\)/,
1160
- User.select { sum(:age) }.to_sql)
1161
- end
1162
-
1163
- def test_select_min_function
1164
- assert_sql(/SELECT MIN\("users"."age"\)/,
1165
- User.select { min(:age) }.to_sql)
1166
- end
1167
-
1168
- def test_select_max_function
1169
- assert_sql(/SELECT MAX\("users"."age"\)/,
1170
- User.select { max(:age) }.to_sql)
1171
- end
1172
-
1173
- def test_function_qualified_column
1174
- assert_sql(/SELECT AVG\("users"."age"\)/,
1175
- User.select { avg(:users[:age]) }.to_sql)
1176
- end
1177
-
1178
- def test_having_function
1179
- sql = User.group(:name).having { sum(:age) > 100 }.to_sql
1180
- assert_sql(/GROUP BY "users"."name"/, sql)
1181
- assert_sql(/HAVING SUM\("users"."age"\) > 100/, sql)
1182
- end
1183
-
1184
- def test_upper_function
1185
- assert_sql(/SELECT UPPER\("users"."name"\)/,
1186
- User.select { upper(:name) }.to_sql)
1187
- end
1188
-
1189
- def test_lower_function
1190
- assert_sql(/SELECT LOWER\("users"."name"\)/,
1191
- User.select { lower(:name) }.to_sql)
1192
- end
1193
-
1194
- def test_length_function_in_where
1195
- assert_sql(/WHERE LENGTH\("users"."name"\) > 3/,
1196
- User.where { length(:name) > 3 }.to_sql)
1197
- end
1198
-
1199
- # fn emits the name as written, so a case-sensitive one can be spelled
1200
- # exactly.
1201
- def test_fn
1202
- assert_sql(/SELECT date_trunc\('day', "users"."name"\)/,
1203
- User.select { fn(:date_trunc, 'day', :name) }.to_sql)
1204
- end
1205
-
1206
- def test_fn_is_comparable
1207
- assert_sql(/WHERE char_length\("users"."name"\) > 3/,
1208
- User.where { fn(:char_length, :name) > 3 }.to_sql)
1209
- end
1210
-
1211
- def test_fn_alias
1212
- assert_sql(/SELECT date_trunc\('day', "users"."name"\) AS "d"/,
1213
- User.select { fn(:date_trunc, 'day', :name).as(:d) }.to_sql)
1214
- end
1215
-
1216
- # Aliases and function names are written into the SQL where a value would
1217
- # have been quoted, so a name that is not plain is refused rather than
1218
- # given the chance to close the identifier and carry on.
1219
- INJECTION = %q{a" AS x, (SELECT 1) AS "y}
1220
-
1221
- # An alias that is not a plain name is quoted by the adapter rather than
1222
- # refused, so an injected one becomes an alias with a strange name and
1223
- # nothing else. Each spells the quoting its own way, so what is asserted is
1224
- # that the payload arrived as the name of the column it labelled.
1225
- def test_an_injected_alias_is_quoted_rather_than_refused
1226
- User.delete_all
1227
- User.create!(name: 'alice')
1228
- payload = 'a" FROM users; --'
1229
- row = User.select { :name.as(payload.to_sym) }.first
1230
- assert_equal('alice', row[payload])
1231
- assert_equal(1, User.count)
1232
- end
1233
-
1234
- def test_an_alias_that_needs_quoting_gets_it
1235
- assert_sql(/AS "total sales"/, User.select { :name.as(:'total sales') }.to_sql)
1236
- assert_sql(/AS "select"/, User.select { :name.as(:select, quote: true) }.to_sql)
1237
- assert_sql(/AS "up per"/, User.select { upper(:name).as(:'up per') }.to_sql)
1238
- assert_sql(/AS "d epth"/, User.select { 0.as(:'d epth') }.to_sql)
1239
- end
1240
-
1241
- # Quoted, the name asked for is the name that comes back. Unquoted,
1242
- # PostgreSQL would fold the capital away and the other two would keep it.
1243
- def test_an_alias_keeps_the_name_as_written
1244
- assert_sql(/AS "postCount"/, User.select { :name.as(:postCount) }.to_sql)
1245
- User.delete_all
1246
- User.create!(name: 'alice')
1247
- assert_equal('alice', User.select { :name.as(:postCount) }.first['postCount'])
1248
- end
1249
-
1250
- def test_quote_false_asks_for_the_name_as_it_is
1251
- assert_sql(/AS post_count/, User.select { :name.as(:post_count, quote: false) }.to_sql)
1252
- refute_match(/"post_count"/,
1253
- normalize_sql(User.select { :name.as(:post_count, quote: false) }.to_sql))
1254
- end
1255
-
1256
- # Nothing quotes it, so a name that would be SQL has to be refused.
1257
- def test_quote_false_refuses_a_name_that_is_not_plain
1258
- e = assert_raises(ArgumentError) { User.select { :name.as(:'total sales', quote: false) } }
1259
- assert_match(/plain column alias/, e.message)
1260
- assert_raises(ArgumentError) { User.select { :name.as(INJECTION.to_sym, quote: false) } }
1261
- end
1262
-
1263
- def test_fn_rejects_an_injected_name
1264
- assert_raises(ArgumentError) { User.select { fn(INJECTION.to_sym, :name) } }
1265
- end
1266
-
1267
- def test_plain_names_are_still_accepted
1268
- assert_sql(/AS "post_count"/, User.select { :name.as(:post_count) }.to_sql)
1269
- assert_sql(/AS "名前"/, User.select { :name.as(:名前) }.to_sql)
1270
- assert_sql(/SELECT myFunc\(/, User.select { fn(:myFunc, :name) }.to_sql)
1271
- assert_sql(/SELECT pg_catalog.upper\(/,
1272
- User.select { fn(:'pg_catalog.upper', :name) }.to_sql)
1273
- end
1274
-
1275
- # Values go through the adapter's quoting, which each spells its own way,
1276
- # so what is asserted is that the payload stays a value: it matches no row
1277
- # rather than opening the condition up.
1278
- def test_values_are_quoted
1279
- User.delete_all
1280
- User.create!(name: 'alice')
1281
- User.create!(name: 'bob')
1282
- payload = "x' OR 1=1 --"
1283
- assert_empty(User.where { :name == payload }.pluck(:name))
1284
- assert_empty(User.where { :name.like?(payload) }.pluck(:name))
1285
- assert_empty(User.where { :name.in?([payload]) }.pluck(:name))
1286
- assert_empty(User.where { :name.include?(payload) }.pluck(:name))
1287
- end
1288
-
1289
- # Likewise for column names: the payload becomes one identifier, so the
1290
- # database rejects it as an unknown column instead of running it.
1291
- def test_column_names_are_quoted
1292
- assert_raises(ActiveRecord::StatementInvalid) do
1293
- User.where { :users[INJECTION.to_sym] == 1 }.to_a
1294
- end
1295
- end
1296
-
1297
- def test_scalar_functions_shared_by_every_adapter
1298
- assert_sql(/SELECT CONCAT\(UPPER\("users"."name"\), 'x'\)/,
1299
- User.select { concat(upper(:name), 'x') }.to_sql)
1300
- assert_sql(/WHERE MOD\("users"."age", 7\) = 0/,
1301
- User.where { mod(:age, 7) == 0 }.to_sql)
1302
- end
1303
-
1304
- # SQLite has no CHAR_LENGTH, GREATEST or LEAST, but LENGTH, MAX and MIN
1305
- # mean the same thing there.
1306
- def test_scalar_functions_spelled_differently_on_sqlite
1307
- expected = ADAPTER == 'sqlite3' ? %w[LENGTH MAX MIN] : %w[CHAR_LENGTH GREATEST LEAST]
1308
- assert_sql(/SELECT #{expected[0]}\("users"."name"\)/,
1309
- User.select { char_length(:name) }.to_sql)
1310
- assert_sql(/SELECT #{expected[1]}\("users"."age", 18\)/,
1311
- User.select { greatest(:age, 18) }.to_sql)
1312
- assert_sql(/SELECT #{expected[2]}\("users"."age", 99\)/,
1313
- User.select { least(:age, 99) }.to_sql)
1314
- end
1315
-
1316
- def test_scalar_functions_run
1317
- User.delete_all
1318
- User.create!(name: 'alice', age: 60)
1319
- assert_equal(['ALICE-x'], User.select { concat(upper(:name), '-x').as(:v) }.map(&:v))
1320
- assert_equal([5], User.select { char_length(:name).as(:v) }.map(&:v))
1321
- assert_equal([60], User.select { greatest(:age, 18).as(:v) }.map(&:v))
1322
- end
1323
-
1324
- # rand takes the name back from Kernel#rand, which would otherwise answer
1325
- # inside the block and never reach the database.
1326
- def test_rand
1327
- expected = ADAPTER == 'mysql2' ? 'RAND' : 'RANDOM'
1328
- assert_sql(/ORDER BY #{expected}\(\)/, User.order { rand }.to_sql)
1329
- end
1330
-
1331
- # Where an adapter has no equivalent, the block raises instead of leaving
1332
- # the database to reject the SQL.
1333
- def test_unsupported_function_raises
1334
- if ADAPTER == 'postgresql'
1335
- assert_sql(/SELECT DATE_TRUNC\('day', "users"."name"\)/,
1336
- User.select { date_trunc('day', :name) }.to_sql)
1337
- else
1338
- e = assert_raises(NotImplementedError) { User.select { date_trunc('day', :name) } }
1339
- assert_match(/date_trunc/, e.message)
1340
- end
1341
- end
1342
-
1343
- # MySQL's FORMAT is a different function that happens to share the name,
1344
- # and reads a printf template as the number zero rather than complaining,
1345
- # so the name carries the printf one and MySQL raises.
1346
- def test_format_is_printf_and_unsupported_on_mysql
1347
- if ADAPTER == 'mysql2'
1348
- assert_raises(NotImplementedError) { User.select { format('%s!', :name) } }
1349
- else
1350
- User.delete_all
1351
- User.create!(name: 'alice')
1352
- assert_equal(['alice!'], User.select { format('%s!', :name).as(:v) }.map(&:v))
1353
- end
1354
- end
1355
-
1356
- # MySQL's own is still reachable, spelled as the different thing it is.
1357
- def test_mysql_format_through_fn
1358
- assert_sql(/SELECT format\(1234.5678, 2\)/,
1359
- User.select { fn(:format, 1234.5678, 2) }.to_sql)
1360
- end
1361
-
1362
- def test_now_is_unsupported_on_sqlite
1363
- if ADAPTER == 'sqlite3'
1364
- assert_raises(NotImplementedError) { User.select { now } }
1365
- else
1366
- assert_sql(/SELECT NOW\(\)/, User.select { now }.to_sql)
1367
- end
1368
- end
1369
-
1370
- # CURRENT_TIMESTAMP and its relatives are grammar rather than calls, so
1371
- # they come out without the parentheses PostgreSQL and SQLite reject.
1372
- def test_datetime_value_functions_are_emitted_bare
1373
- assert_sql(/SELECT CURRENT_TIMESTAMP FROM/,
1374
- User.select { current_timestamp }.to_sql)
1375
- assert_sql(/SELECT CURRENT_DATE FROM/, User.select { current_date }.to_sql)
1376
- assert_sql(/SELECT CURRENT_TIME FROM/, User.select { current_time }.to_sql)
1377
- end
1378
-
1379
- def test_datetime_value_function_in_comparison_and_alias
1380
- assert_sql(/WHERE "users"."name" < CURRENT_TIMESTAMP/,
1381
- User.where { :name < current_timestamp }.to_sql)
1382
- assert_sql(/SELECT CURRENT_TIMESTAMP AS "ts" FROM/,
1383
- User.select { current_timestamp.as(:ts) }.to_sql)
1384
- end
1385
-
1386
- def test_current_timestamp_runs
1387
- User.delete_all
1388
- User.create!(name: 'alice')
1389
- refute_nil(User.select { current_timestamp.as(:v) }.sole.v)
1390
- end
1391
-
1392
- # The one thing that does go into the parentheses is a precision, which
1393
- # current_date never takes and SQLite never accepts.
1394
- def test_datetime_value_function_with_precision
1395
- if ADAPTER == 'sqlite3'
1396
- e = assert_raises(NotImplementedError) { User.select { current_timestamp(3) } }
1397
- assert_match(/precision/, e.message)
1398
- else
1399
- assert_sql(/SELECT CURRENT_TIMESTAMP\(3\) FROM/,
1400
- User.select { current_timestamp(3) }.to_sql)
1401
- assert_sql(/SELECT CURRENT_TIME\(0\) FROM/,
1402
- User.select { current_time(0) }.to_sql)
1403
- User.delete_all
1404
- User.create!(name: 'alice')
1405
- refute_nil(User.select { current_timestamp(0).as(:v) }.sole.v)
1406
- end
1407
- end
1408
-
1409
- def test_current_date_takes_no_precision
1410
- assert_raises(ArgumentError) { User.select { current_date(0) } }
1411
- end
1412
-
1413
- # The precision is written into the SQL as given, so only an Integer is
1414
- # accepted there.
1415
- def test_precision_must_be_an_integer
1416
- assert_raises(ArgumentError) do
1417
- User.select { current_timestamp(:'3); DROP TABLE users --') }
1418
- end
1419
- end
1420
-
1421
- def test_localtime_is_unsupported_on_sqlite
1422
- if ADAPTER == 'sqlite3'
1423
- assert_raises(NotImplementedError) { User.select { localtime } }
1424
- assert_raises(NotImplementedError) { User.select { localtimestamp } }
1425
- else
1426
- assert_sql(/SELECT LOCALTIME FROM/, User.select { localtime }.to_sql)
1427
- assert_sql(/SELECT LOCALTIMESTAMP FROM/,
1428
- User.select { localtimestamp }.to_sql)
1429
- end
1430
- end
1431
-
1432
- def test_math_functions
1433
- assert_sql(/SELECT SIGN\("users"."age"\)/, User.select { sign(:age) }.to_sql)
1434
- assert_sql(/SELECT ATAN2\("users"."age", 2\)/,
1435
- User.select { atan2(:age, 2) }.to_sql)
1436
- assert_sql(/SELECT PI\(\)/, User.select { pi }.to_sql)
1437
- assert_sql(/SELECT DEGREES\(RADIANS\("users"."age"\)\)/,
1438
- User.select { degrees(radians(:age)) }.to_sql)
1439
- end
1440
-
1441
- def test_math_functions_run
1442
- User.delete_all
1443
- User.create!(name: 'alice', age: 60)
1444
- assert_equal(1, User.select { sign(:age).as(:v) }.sole.v.to_i)
1445
- assert_equal(60,
1446
- User.select { round(degrees(radians(:age))).as(:v) }.sole.v.to_i)
1447
- end
1448
-
1449
- # PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
1450
- def test_log2_is_unsupported_on_postgresql
1451
- if ADAPTER == 'postgresql'
1452
- assert_raises(NotImplementedError) { User.select { log2(:age) } }
1453
- else
1454
- assert_sql(/SELECT LOG2\("users"."age"\)/, User.select { log2(:age) }.to_sql)
1455
- end
1456
- end
1457
-
1458
- # MySQL spells trunc TRUNCATE, and insists on the second argument the
1459
- # others default to zero.
1460
- def test_trunc
1461
- expected = ADAPTER == 'mysql2' ? 'TRUNCATE' : 'TRUNC'
1462
- assert_sql(/SELECT #{expected}\("users"."age", 0\)/,
1463
- User.select { trunc(:age, 0) }.to_sql)
1464
- end
1465
-
1466
- # EXTRACT(field FROM expr): the field is a keyword rather than a value.
1467
- # SQLite spells all of this as strftime formats, which no renaming
1468
- # carries.
1469
- def test_extract
1470
- if ADAPTER == 'sqlite3'
1471
- e = assert_raises(NotImplementedError) { User.select { extract(:year, :name) } }
1472
- assert_match(/extract/, e.message)
1473
- else
1474
- assert_sql(/SELECT EXTRACT\(YEAR FROM "users"."name"\)/,
1475
- User.select { extract(:year, :name) }.to_sql)
1476
- assert_sql(/WHERE EXTRACT\(YEAR FROM "users"."name"\) = 2026/,
1477
- User.where { extract(:year, :name) == 2026 }.to_sql)
1478
- end
1479
- end
1480
-
1481
- # A bad field is an ArgumentError on every adapter, before SQLite gets to
1482
- # say it has no extract at all.
1483
- def test_extract_rejects_an_injected_field
1484
- assert_raises(ArgumentError) { User.select { extract(INJECTION.to_sym, :name) } }
1485
- end
1486
-
1487
- def test_extract_runs
1488
- skip "#{ADAPTER} has no extract" if ADAPTER == 'sqlite3'
1489
- User.delete_all
1490
- User.create!(name: 'alice')
1491
- assert_equal(2026,
1492
- User.select { extract(:year, cast('2026-01-05', :date)).as(:v) }.sole.v.to_i)
1493
- end
1494
-
1495
- def test_cast
1496
- assert_sql(/SELECT CAST\("users"."age" AS text\)/,
1497
- User.select { cast(:age, :text) }.to_sql)
1498
- end
1499
-
1500
- def test_cast_runs
1501
- User.delete_all
1502
- User.create!(name: 'alice')
1503
- assert_equal(12.5,
1504
- User.select { cast('12.5', 'decimal(10,2)').as(:v) }.sole.v.to_f)
1505
- end
1506
-
1507
- # The type is written into the SQL as given, so it has to look like one:
1508
- # a plain name, at most parenthesized with lengths. The adapters' own
1509
- # spellings with a space in them pass too.
1510
- def test_cast_type_names
1511
- assert_sql(/AS double precision\)/,
1512
- User.select { cast(:age, 'double precision') }.to_sql)
1513
- assert_sql(/AS decimal\(10,2\)\)/,
1514
- User.select { cast(:age, 'decimal(10,2)') }.to_sql)
1515
- assert_raises(ArgumentError) { User.select { cast(:age, INJECTION.to_sym) } }
1516
- assert_raises(ArgumentError) do
1517
- User.select { cast(:age, 'integer); DROP TABLE users --') }
1518
- end
1519
- end
1520
-
1521
- # A name with no method of its own is still a NoMethodError, not a
1522
- # function call the database has to reject.
1523
- def test_unknown_function_is_a_no_method_error
1524
- assert_raises(NoMethodError) { User.select { uppr(:name) } }
1525
- end
1526
-
1527
- def test_arithmetic_multiplication
1528
- assert_sql(/SELECT "users"."age" \* 2 AS "dbl"/,
1529
- User.select { (:age * 2).as(:dbl) }.to_sql)
1530
- end
1531
-
1532
- # Ruby puts * above >, so the expression groups the way it reads.
1533
- def test_arithmetic_in_where_without_parentheses
1534
- assert_sql(/WHERE "users"."age" \* 2 > 100/,
1535
- User.where { :age * 2 > 100 }.to_sql)
1536
- end
1537
-
1538
- def test_arithmetic_between_columns
1539
- assert_sql(/WHERE \("users"."age" \+ "users"."id"\) \/ 2 <= 30/,
1540
- User.where { (:age + :id) / 2 <= 30 }.to_sql)
1541
- end
1542
-
1543
- def test_arithmetic_inside_aggregate
1544
- assert_sql(/SELECT SUM\("users"."age" \* 2\)/,
1545
- User.select { sum(:age * 2) }.to_sql)
1546
- end
1547
-
1548
- # Arel groups + and - but not * and /, which is how SQL precedence works
1549
- # out anyway.
1550
- def test_arithmetic_on_qualified_column
1551
- assert_sql(/SELECT \("users"."age" - 1\)/,
1552
- User.select { :users[:age] - 1 }.to_sql)
1553
- end
1554
-
1555
- def test_bitwise_and_or
1556
- assert_sql(/SELECT \("users"."flags" & 4\) AS "masked"/,
1557
- User.select { (:flags & 4).as(:masked) }.to_sql)
1558
- assert_sql(/SELECT \("users"."flags" \| 4\) AS "set"/,
1559
- User.select { (:flags | 4).as(:set) }.to_sql)
1560
- end
1561
-
1562
- # Ruby puts & above >, so this groups the way it reads, and the node
1563
- # parenthesises itself so that the adapter's own precedence cannot regroup
1564
- # it -- PostgreSQL gives & and | the same one.
1565
- def test_bitwise_in_where_without_parentheses
1566
- assert_sql(/WHERE \("users"."flags" & 4\) > 0/,
1567
- User.where { :flags & 4 > 0 }.to_sql)
1568
- end
1569
-
1570
- def test_bitwise_shifts
1571
- assert_sql(/SELECT \("users"."flags" << 2\)/, User.select { :flags << 2 }.to_sql)
1572
- assert_sql(/SELECT \("users"."flags" >> 1\)/, User.select { :flags >> 1 }.to_sql)
1573
- end
1574
-
1575
- def test_bitwise_not
1576
- assert_sql(/SELECT \( ~ "users"."flags"\)/, User.select { ~:flags }.to_sql)
1577
- end
1578
-
1579
- # The one operator the three do not share: PostgreSQL's # is where a comment
1580
- # starts on MySQL, MySQL's ^ is exponentiation to PostgreSQL, and SQLite has
1581
- # neither, so it gets the two operations XOR is made of.
1582
- def test_bitwise_xor_is_spelled_per_adapter
1583
- sql = User.select { :flags ^ 10 }.to_sql
1584
- case ADAPTER
1585
- when 'postgresql' then assert_sql(/SELECT \("users"."flags" # 10\)/, sql)
1586
- when 'mysql2' then assert_sql(/SELECT \("users"."flags" \^ 10\)/, sql)
1587
- else assert_sql(
1588
- /SELECT \(\("users"."flags" \| 10\) - \("users"."flags" & 10\)\)/, sql)
1589
- end
1590
- end
1591
-
1592
- # Whatever the spelling, the answers agree.
1593
- def test_bitwise_execution
1594
- User.delete_all
1595
- User.create!(name: 'a', flags: 12)
1596
- assert_equal(8, User.select { (:flags & 10).as(:v) }.take.v.to_i)
1597
- assert_equal(14, User.select { (:flags | 10).as(:v) }.take.v.to_i)
1598
- assert_equal(6, User.select { (:flags ^ 10).as(:v) }.take.v.to_i)
1599
- assert_equal(48, User.select { (:flags << 2).as(:v) }.take.v.to_i)
1600
- assert_equal(6, User.select { (:flags >> 1).as(:v) }.take.v.to_i)
1601
- # MariaDB reads ~ back as the unsigned 64-bit number where the others give
1602
- # a negative one, so the assertion is on the bits rather than the value.
1603
- assert_equal(243, User.select { (~:flags & 255).as(:v) }.take.v.to_i)
1604
- end
1605
-
1606
- # AND and OR are the conditions' own & and |, and an operand that is a
1607
- # condition means one of the two was meant.
1608
- def test_bitwise_refuses_a_condition
1609
- e = assert_raises(ArgumentError) { User.where { :flags & (:age == 1) } }
1610
- assert_match(/AND and OR/, e.message)
1611
- end
1612
-
1613
- # MySQL and SQLite would take a boolean for the bit it is stored as and
1614
- # quietly answer as AND would; PostgreSQL has no such operator.
1615
- def test_bitwise_refuses_a_boolean_column
1616
- e = assert_raises(ArgumentError) { User.where { (:active & :active) > 0 }.to_sql }
1617
- assert_match(/true\?/, e.message)
1618
- assert_raises(ArgumentError) { User.select { ~:active }.to_sql }
1619
- end
1620
-
1621
- def test_conditions_still_and_with_the_same_operators
1622
- assert_sql(/WHERE "users"."age" = 1 AND "users"."name" = 'a'/,
1623
- User.where { (:age == 1) & (:name == 'a') }.to_sql)
1624
- end
1625
-
1626
- def test_bit_aggregates
1627
- skip_without_bit_aggregates
1628
- User.delete_all
1629
- User.create!([{name: 'a', flags: 12}, {name: 'b', flags: 10}, {name: 'c', flags: 3}])
1630
- assert_equal(0, User.select { bit_and(:flags).as(:v) }.take.v.to_i)
1631
- assert_equal(15, User.select { bit_or(:flags).as(:v) }.take.v.to_i)
1632
- assert_equal(5, User.select { bit_xor(:flags).as(:v) }.take.v.to_i)
1633
- end
1634
-
1635
- # PostgreSQL counts the bits of a bit string rather than of a number, so the
1636
- # argument is cast there; bit(64) is what makes a negative answer alike.
1637
- def test_bit_count
1638
- if ADAPTER == 'sqlite3'
1639
- assert_raises(NotImplementedError) { User.select { bit_count(:flags) } }
1640
- return
1641
- end
1642
- User.delete_all
1643
- User.create!(name: 'a', flags: 12)
1644
- User.create!(name: 'b', flags: -1)
1645
- assert_equal([2, 64], User.select { bit_count(:flags).as(:v) }.order(:name).map {|u| u.v.to_i })
1646
- assert_sql(/BIT_COUNT\(CAST\("users"."flags" AS bit\(64\)\)\)/,
1647
- User.select { bit_count(:flags) }.to_sql) if ADAPTER == 'postgresql'
1648
- end
1649
-
1650
- def test_bit_aggregates_are_unsupported_on_sqlite
1651
- if ADAPTER == 'sqlite3'
1652
- e = assert_raises(NotImplementedError) { User.select { bit_or(:flags) } }
1653
- assert_match(/bit_or/, e.message)
1654
- else
1655
- assert_sql(/BIT_OR\("users"."flags"\)/, User.select { bit_or(:flags) }.to_sql)
1656
- end
1657
- end
1658
-
1659
- def test_coalesce_function_with_literal
1660
- assert_sql(/SELECT COALESCE\("users"."name", 'unknown'\)/,
1661
- User.select { coalesce(:name, 'unknown') }.to_sql)
1662
- end
1663
-
1664
- def test_function_comparison
1665
- assert_sql(/WHERE UPPER\("users"."name"\) = 'MATZ'/,
1666
- User.where { upper(:name) == 'MATZ' }.to_sql)
1667
- end
1668
-
1669
- def test_function_like
1670
- assert_sql(/WHERE UPPER\("users"."name"\) LIKE 'MA%'/,
1671
- User.where { upper(:name).like?('MA%') }.to_sql)
1672
- end
1673
-
1674
- def test_function_in
1675
- assert_sql(/WHERE UPPER\("users"."name"\) IN \('MATZ', 'NOBU'\)/,
1676
- User.where { upper(:name).in?(%w[MATZ NOBU]) }.to_sql)
1677
- end
1678
-
1679
- def test_aggregate_in
1680
- assert_sql(/HAVING SUM\("users"."age"\) BETWEEN 1 AND 10/,
1681
- User.group(:name).having { sum(:age).in?(1..10) }.to_sql)
1682
- end
1683
-
1684
- def test_nested_function
1685
- assert_sql(/SELECT UPPER\(COALESCE\("users"."name", 'x'\)\)/,
1686
- User.select { upper(coalesce(:name, 'x')) }.to_sql)
1687
- end
1688
-
1689
- def test_function_qualified_column_arg
1690
- assert_sql(/SELECT UPPER\("users"."name"\)/,
1691
- User.select { upper(:users[:name]) }.to_sql)
1692
- end
1693
-
1694
- def test_select_multiple_fields
1695
- assert_sql(/SELECT UPPER\("users"."name"\), "users"."age"/,
1696
- User.select { [upper(:name), :age] }.to_sql)
1697
- end
1698
-
1699
- def test_select_multiple_columns
1700
- assert_sql(/SELECT "users"."name", "users"."age"/,
1701
- User.select { [:name, :age] }.to_sql)
1702
- end
1703
-
1704
- def test_select_multiple_with_aggregate
1705
- assert_sql(/SELECT "users"."name", SUM\("users"."age"\)/,
1706
- User.select { [:name, sum(:age)] }.to_sql)
1707
- end
1708
-
1709
- def test_select_function_with_alias
1710
- assert_sql(/SELECT UPPER\("users"."name"\) AS "upper_name", "users"."age"/,
1711
- User.select { [upper(:name).as(:upper_name), :age] }.to_sql)
1712
- end
1713
-
1714
- def test_select_column_alias
1715
- assert_sql(/SELECT "users"."name" AS "n"/,
1716
- User.select { :name.as(:n) }.to_sql)
1717
- end
1718
-
1719
- def test_select_qualified_column_alias
1720
- assert_sql(/SELECT "users"."name" AS "n"/,
1721
- User.select { :users[:name].as(:n) }.to_sql)
1722
- end
1723
-
1724
- def test_select_aggregate_alias
1725
- assert_sql(/SELECT COUNT\("users"."id"\) AS "cnt"/,
1726
- User.select { count(:id).as(:cnt) }.to_sql)
1727
- end
1728
-
1729
- def test_order_default_asc
1730
- assert_sql(/ORDER BY "users"."age"/,
1731
- User.order { :age }.to_sql)
1732
- end
1733
-
1734
- def test_order_desc
1735
- assert_sql(/ORDER BY "users"."age" DESC/,
1736
- User.order { :age.desc }.to_sql)
1737
- end
1738
-
1739
- def test_order_asc
1740
- assert_sql(/ORDER BY "users"."age" ASC/,
1741
- User.order { :age.asc }.to_sql)
1742
- end
1743
-
1744
- def test_order_multiple
1745
- assert_sql(/ORDER BY "users"."age" DESC, "users"."name" ASC/,
1746
- User.order { [:age.desc, :name.asc] }.to_sql)
1747
- end
1748
-
1749
- def test_order_nulls_first
1750
- skip_without_nulls_ordering_syntax
1751
- assert_sql(/ORDER BY "users"."age" ASC NULLS FIRST/,
1752
- User.order { :age.asc.nulls_first }.to_sql)
1753
- end
1754
-
1755
- def test_order_nulls_last
1756
- skip_without_nulls_ordering_syntax
1757
- assert_sql(/ORDER BY "users"."age" DESC NULLS LAST, "users"."name" ASC/,
1758
- User.order { [:age.desc.nulls_last, :name.asc] }.to_sql)
1759
- end
1760
-
1761
- # The order itself is portable even where the syntax is not.
1762
- def test_order_nulls_execution
1763
- User.delete_all
1764
- User.create!(name: 'null_age', age: nil)
1765
- User.create!(name: 'young', age: 20)
1766
- User.create!(name: 'old', age: 60)
1767
- assert_equal(%w[null_age young old],
1768
- User.order { :age.asc.nulls_first }.pluck(:name))
1769
- assert_equal(%w[young old null_age],
1770
- User.order { :age.asc.nulls_last }.pluck(:name))
1771
- end
1772
-
1773
- def test_order_qualified_column
1774
- assert_sql(/ORDER BY "users"."name" DESC/,
1775
- User.order { :users[:name].desc }.to_sql)
1776
- end
1777
-
1778
- def test_group_single
1779
- assert_sql(/GROUP BY "users"."name"/,
1780
- User.group { :name }.to_sql)
1781
- end
1782
-
1783
- def test_group_multiple
1784
- assert_sql(/GROUP BY "users"."name", "users"."age"/,
1785
- User.group { [:name, :age] }.to_sql)
1786
- end
1787
-
1788
- def test_group_qualified_column
1789
- assert_sql(/GROUP BY "users"."name"/,
1790
- User.group { :users[:name] }.to_sql)
1791
- end
1792
-
1793
- def test_group_with_having
1794
- sql = User.group { :name }.having { sum(:age) > 100 }.to_sql
1795
- assert_sql(/GROUP BY "users"."name"/, sql)
1796
- assert_sql(/HAVING SUM\("users"."age"\) > 100/, sql)
1797
- end
1798
-
1799
- # update_all's hash reads a symbol as the value it is; the block reads it as
1800
- # the column it names, which is what lets the new value be built from the old.
1801
- def test_update_all_from_the_column
1802
- Tally.delete_all
1803
- Tally.create!(page: '/a', hits: 1)
1804
- Tally.create!(page: '/b', hits: 2)
1805
- Tally.update_all { { hits: :hits + 1 } }
1806
- assert_equal([2, 3], Tally.order(:page).pluck(:hits))
1807
- end
1808
-
1809
- def test_update_all_takes_any_expression
1810
- Tally.delete_all
1811
- Tally.create!(page: '/a', hits: 5)
1812
- Tally.update_all { { hits: case_when { :hits > 4 }.then(0).else(:hits), page: upper(:page) } }
1813
- assert_equal([['/A', 0]], Tally.pluck(:page, :hits))
1814
- end
1815
-
1816
- def test_update_all_within_a_scope
1817
- Tally.delete_all
1818
- Tally.create!(page: '/a', hits: 1)
1819
- Tally.create!(page: '/b', hits: 1)
1820
- Tally.where { :page == '/a' }.update_all { { hits: 9 } }
1821
- assert_equal([9, 1], Tally.order(:page).pluck(:hits))
1822
- end
1823
-
1824
- def test_update_all_without_a_block_is_unchanged
1825
- Tally.delete_all
1826
- Tally.create!(page: '/a', hits: 1)
1827
- Tally.update_all(hits: 4)
1828
- assert_equal([4], Tally.pluck(:hits))
1829
- end
1830
-
1831
- def test_update_all_takes_updates_or_a_block
1832
- assert_raises(ArgumentError) { Tally.update_all({hits: 1}) { { hits: 2 } } }
1833
- e = assert_raises(ArgumentError) { Tally.update_all { :hits + 1 } }
1834
- assert_match(/hash of column/, e.message)
1835
- end
1836
-
1837
- # on_duplicate takes SQL text and nothing else, so the block is compiled to
1838
- # some. `excluded` is the row that could not be inserted.
1839
- def test_upsert_all_adds_to_what_is_there
1840
- Tally.delete_all
1841
- Tally.upsert_all([{page: '/a', hits: 1}], **upsert_target)
1842
- Tally.upsert_all([{page: '/a', hits: 10}], **upsert_target) {
1843
- { hits: :hits + excluded(:hits) }
1844
- }
1845
- assert_equal([11], Tally.pluck(:hits))
1846
- end
1847
-
1848
- def test_upsert_all_inserts_when_there_is_no_conflict
1849
- Tally.delete_all
1850
- Tally.upsert_all([{page: '/new', hits: 3}], **upsert_target) {
1851
- { hits: :hits + excluded(:hits) }
1852
- }
1853
- assert_equal([3], Tally.pluck(:hits))
1854
- end
1855
-
1856
- def test_upsert_all_takes_any_expression
1857
- Tally.delete_all
1858
- Tally.upsert_all([{page: '/a', hits: 7}], **upsert_target)
1859
- Tally.upsert_all([{page: '/a', hits: 2}], **upsert_target) {
1860
- { hits: greatest(:hits, excluded(:hits)) }
1861
- }
1862
- assert_equal([7], Tally.pluck(:hits))
1863
- end
1864
-
1865
- def test_upsert_all_without_a_block_is_unchanged
1866
- Tally.delete_all
1867
- Tally.upsert_all([{page: '/a', hits: 1}], **upsert_target)
1868
- Tally.upsert_all([{page: '/a', hits: 6}], **upsert_target)
1869
- assert_equal([6], Tally.pluck(:hits))
1870
- end
1871
-
1872
- def test_upsert_all_takes_on_duplicate_or_a_block
1873
- assert_raises(ArgumentError) do
1874
- Tally.upsert_all([{page: '/a', hits: 1}],
1875
- on_duplicate: Arel.sql('hits = 1'), **upsert_target) { { hits: 2 } }
1876
- end
1877
- e = assert_raises(ArgumentError) do
1878
- Tally.upsert_all([{page: '/a', hits: 1}], **upsert_target) { {} }
1879
- end
1880
- assert_match(/at least one column/, e.message)
1881
- end
1882
-
1883
- # Reading inside a JSON document, by the name of what Hash does. No two
1884
- # adapters spell it alike, so what the tests assert is the value that comes
1885
- # back rather than the SQL.
1886
- def seed_docs
1887
- Doc.delete_all
1888
- Doc.create!(name: 'one',
1889
- meta: json_document({ 'a' => { 'b' => 'deep' }, 'n' => 5,
1890
- 'tags' => %w[x y], 'odd key' => 1 }))
1891
- Doc.create!(name: 'two', meta: json_document({ 'n' => 9 }))
1892
- end
1893
-
1894
- def test_dig_text_a_key
1895
- seed_docs
1896
- assert_equal(%w[5 9], Doc.order(:name).select { :meta.dig_text(:n).as(:v) }.map(&:v))
1897
- end
1898
-
1899
- def test_dig_text_a_path
1900
- seed_docs
1901
- assert_equal(['deep', nil],
1902
- Doc.order(:name).select { :meta.dig_text(:a, :b).as(:v) }.map(&:v))
1903
- end
1904
-
1905
- def test_dig_text_an_array_index
1906
- seed_docs
1907
- assert_equal(['x', nil],
1908
- Doc.order(:name).select { :meta.dig_text(:tags, 0).as(:v) }.map(&:v))
1909
- end
1910
-
1911
- # A key that is not a plain name travels as itself rather than being refused.
1912
- def test_dig_text_a_key_that_needs_quoting
1913
- seed_docs
1914
- assert_equal(['1', nil],
1915
- Doc.order(:name).select { :meta.dig_text(:'odd key').as(:v) }.map(&:v))
1916
- end
1917
-
1918
- # dig_text gives text on every adapter -- SQLite's ->> would otherwise give the
1919
- # value with its type -- so a number is compared through a cast.
1920
- def test_dig_text_is_text_everywhere
1921
- seed_docs
1922
- assert_equal(['one'], Doc.where { :meta.dig_text(:n) == '5' }.pluck(:name))
1923
- type = integer_type
1924
- assert_equal(['two'], Doc.where { cast(:meta.dig_text(:n), type) > 6 }.pluck(:name))
1925
- end
1926
-
1927
- def test_dig_keeps_the_json
1928
- seed_docs
1929
- value = Doc.where { :name == 'one' }.select { :meta.dig(:tags).as(:v) }.first.v
1930
- assert_equal(%w[x y], value.is_a?(String) ? JSON.parse(value) : value)
1931
- end
1932
-
1933
- # What text compared with a number means is a question the three adapters
1934
- # answer three ways -- `dig_text(:n) == 5` is true on SQLite, an error on
1935
- # PostgreSQL and true on MySQL, and `dig_text(:flag) == true` is true, an error
1936
- # and false -- so the comparison is refused rather than left to them.
1937
- def test_dig_text_refuses_a_comparison_with_anything_but_text
1938
- e = assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n) == 5 } }
1939
- assert_match(/cast/, e.message)
1940
- assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n) != 5 } }
1941
- assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n) > 6 } }
1942
- assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:flag) == true } }
1943
- assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n).in?([1, 2]) } }
1944
- assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n).between?(1, 9) } }
1945
- end
1946
-
1947
- # A string is what a dug value compares to; so is anything the block built
1948
- # rather than wrote as a literal, since that is nobody's guess to make.
1949
- def test_dig_text_compares_with_text_and_with_expressions
1950
- seed_docs
1951
- assert_equal(['one'], Doc.where { :meta.dig_text(:n) == '5' }.pluck(:name))
1952
- assert_equal([], Doc.where { :meta.dig_text(:n) == :name }.pluck(:name))
1953
- assert_equal(['one'], Doc.where { :meta.dig_text(:n) == upper('5') }.pluck(:name))
1954
- type = integer_type
1955
- assert_equal(['one'], Doc.where { cast(:meta.dig_text(:n), type) == 5 }.pluck(:name))
1956
- end
1957
-
1958
- # The JSON for a string carries its quotes, so the same comparison is
1959
- # refused the other way about: false on SQLite, an error on PostgreSQL and
1960
- # true on MySQL.
1961
- def test_dig_refuses_a_comparison_with_a_ruby_value
1962
- e = assert_raises(ArgumentError) { Doc.where { :meta.dig(:a) == 'deep' } }
1963
- assert_match(/dig_text gives the value/, e.message)
1964
- assert_raises(ArgumentError) { Doc.where { :meta.dig(:n) == 5 } }
1965
- end
1966
-
1967
- # What dig gives is a document, so the JSON operations read it: the
1968
- # same question asked of a part rather than of the whole.
1969
- def test_the_json_operations_read_what_dig_kept
1970
- seed_docs
1971
- assert_equal(['one'], Doc.where { :meta.dig(:a).key?(:b) }.pluck(:name))
1972
- assert_equal(%w[one two], Doc.where { :meta.dig(:n).not_null? }.order(:name).pluck(:name))
1973
- assert_equal(['one'],
1974
- Doc.where { :meta.dig(:a).dig_text(:b) == 'deep' }.pluck(:name))
1975
- value = Doc.where { :name == 'one' }.
1976
- select { :meta.dig(:a).bury(:b, 'x').as(:v) }.first.v
1977
- assert_equal('x', (value.is_a?(String) ? JSON.parse(value) : value)['b'])
1978
- end
1979
-
1980
- def test_containment_reads_what_dig_kept
1981
- skip_without_json_containment
1982
- seed_docs
1983
- assert_equal(['one'], Doc.where { :meta.dig(:tags).contains?(['x']) }.pluck(:name))
1984
- assert_equal([], Doc.where { :meta.dig(:tags).contains?(['z']) }.pluck(:name))
1985
- end
1986
-
1987
- # Reading text back as a document is where the adapters part company:
1988
- # SQLite parses it, MySQL takes it as written, PostgreSQL has no such
1989
- # function for text at all.
1990
- def test_the_json_operations_are_refused_on_a_dug_value
1991
- e = assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:a).key?(:b) } }
1992
- assert_match(/dig keeps it/, e.message)
1993
- assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:a).contains?(b: 1) } }
1994
- assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).dig_text(:b) } }
1995
- assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).dig(:b) } }
1996
- assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).bury(:b, 'x') } }
1997
- assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).except(:b) } }
1998
- end
1999
-
2000
- # What bury and except give back is JSON as dig's is, so the same guard
2001
- # covers them; an expression on the right still goes through.
2002
- def test_bury_and_except_refuse_a_comparison_with_a_ruby_value
2003
- e = assert_raises(ArgumentError) { Doc.where { :meta.bury(:a, 1) == '{"a": 1}' } }
2004
- assert_match(/bury gives JSON/, e.message)
2005
- e = assert_raises(ArgumentError) { Doc.where { :meta.except(:a) == '{"b": 2}' } }
2006
- assert_match(/except gives JSON/, e.message)
2007
- assert_raises(ArgumentError) { Doc.where { :meta.except(:a).in?(['{}']) } }
2008
- assert_sql(/ = /, Doc.where { :meta.except(:a) == :meta.except(:b) })
2009
- end
2010
-
2011
- # Arithmetic is refused like a literal comparison is: text plus one is 6
2012
- # on SQLite, an error on PostgreSQL and 6.0 on MariaDB. cast settles it,
2013
- # and an expression on the right changes nothing about the dug side.
2014
- def test_arithmetic_is_refused_on_a_dug_value
2015
- e = assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:n) + 1 } }
2016
- assert_match(/cast it to the type meant/, e.message)
2017
- e = assert_raises(ArgumentError) { Doc.select { :meta.dig(:n) * 2 } }
2018
- assert_match(/dig gives JSON/, e.message)
2019
- assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:n) + :name } }
2020
- assert_raises(ArgumentError) { Doc.select { :meta.bury(:a, 1) - 1 } }
2021
- assert_raises(ArgumentError) { Doc.select { ~:meta.dig(:n) } }
2022
- seed_docs
2023
- type = integer_type
2024
- assert_equal(6,
2025
- Doc.where { :name == 'one' }.
2026
- select { (cast(:meta.dig_text(:n), type) + 1).as(:v) }.first.v.to_i)
2027
- end
2028
-
2029
- def test_dig_text_from_a_qualified_column
2030
- seed_docs
2031
- assert_equal(['one'], Doc.where { :docs[:meta].dig_text(:a, :b) == 'deep' }.pluck(:name))
2032
- end
2033
-
2034
- def test_key
2035
- seed_docs
2036
- assert_equal(['one'], Doc.where { :meta.key?(:tags) }.pluck(:name))
2037
- assert_equal(%w[one two], Doc.where { :meta.key?(:n) }.order(:name).pluck(:name))
2038
- end
2039
-
2040
- def test_contains
2041
- skip_without_json_containment
2042
- seed_docs
2043
- assert_equal(['one'], Doc.where { :meta.contains?(n: 5) }.pluck(:name))
2044
- assert_equal([], Doc.where { :meta.contains?(n: 1) }.pluck(:name))
2045
- end
2046
-
2047
- def test_contains_says_where_it_cannot_go
2048
- skip "#{ADAPTER} has JSON containment" unless ADAPTER == 'sqlite3'
2049
- assert_raises(NotImplementedError) { Doc.where { :meta.contains?(n: 5) }.to_sql }
2050
- end
2051
-
2052
- def test_dig_needs_a_path
2053
- assert_raises(ArgumentError) { Doc.select { :meta.dig } }
2054
- e = assert_raises(ArgumentError) { Doc.select { :meta.dig_text(1.5) } }
2055
- assert_match(/key or an array index/, e.message)
2056
- end
2057
-
2058
- # FILTER takes the aggregate over the rows a condition holds for. MySQL has
2059
- # no such clause, so what is asserted across adapters is the number that
2060
- # comes back rather than the SQL.
2061
- def seed_for_filter
2062
- User.delete_all
2063
- User.create!(name: 'a', age: 10)
2064
- User.create!(name: 'a', age: 20)
2065
- User.create!(name: 'b', age: 100)
2066
- end
2067
-
2068
- def aggregate(&block)
2069
- User.select(&block).to_a.first.v
2070
- end
2071
-
2072
- def test_filter_a_count
2073
- seed_for_filter
2074
- assert_equal(2, aggregate { count(:*).filter { :age < 50 }.as(:v) }.to_i)
2075
- end
2076
-
2077
- def test_filter_takes_a_value_as_well_as_a_block
2078
- seed_for_filter
2079
- assert_equal(2, aggregate { count(:*).filter(:age < 50).as(:v) }.to_i)
2080
- end
2081
-
2082
- def test_filter_a_sum_and_an_average
2083
- seed_for_filter
2084
- assert_equal(30, aggregate { sum(:age).filter { :age < 50 }.as(:v) }.to_i)
2085
- assert_equal(15, aggregate { avg(:age).filter { :age < 50 }.as(:v) }.to_i)
2086
- end
2087
-
2088
- def test_filter_a_distinct_count
2089
- seed_for_filter
2090
- assert_equal(1, aggregate { count(:name, distinct: true).filter { :age < 50 }.as(:v) }.to_i)
2091
- end
2092
-
2093
- def test_filter_is_a_clause_where_there_is_one
2094
- skip "#{ADAPTER} has no FILTER" if ADAPTER == 'mysql2'
2095
- assert_sql(/COUNT\(\*\) FILTER \(WHERE "users"."age" < 50\)/,
2096
- User.select { count(:*).filter { :age < 50 } }.to_sql)
2097
- end
2098
-
2099
- # Where there is not, the same rows are reached through a case: an aggregate
2100
- # passes over a NULL, so a row the condition misses is a row it does not see.
2101
- def test_filter_becomes_a_case_where_there_is_no_clause
2102
- skip "#{ADAPTER} has FILTER" unless ADAPTER == 'mysql2'
2103
- assert_sql(/COUNT\(CASE WHEN "users"."age" < 50 THEN 1 END\)/,
2104
- User.select { count(:*).filter { :age < 50 } }.to_sql)
2105
- assert_sql(/SUM\(CASE WHEN "users"."age" < 50 THEN "users"."age" END\)/,
2106
- User.select { sum(:age).filter { :age < 50 } }.to_sql)
2107
- end
2108
-
2109
- def test_filter_needs_a_value_or_a_block
2110
- assert_raises(ArgumentError) { User.select { count(:*).filter } }
2111
- e = assert_raises(ArgumentError) { User.select { count(:*).filter(1) { 2 } } }
2112
- assert_match(/not both/, e.message)
2113
- end
2114
-
2115
- # DISTINCT ON keeps the first row of each group the order brings up.
2116
- def seed_for_distinct_on
2117
- Author.delete_all
2118
- Author.create!(name: 'a')
2119
- Author.create!(name: 'a')
2120
- Author.create!(name: 'b')
2121
- end
2122
-
2123
- def test_distinct_on
2124
- skip_without_distinct_on
2125
- seed_for_distinct_on
2126
- assert_sql(/SELECT DISTINCT ON \( "authors"."name" \)/,
2127
- Author.distinct_on { :name }.to_sql)
2128
- assert_equal(%w[a b], Author.distinct_on { :name }.order { :name }.pluck(:name))
2129
- end
2130
-
2131
- def test_distinct_on_takes_columns_as_well_as_a_block
2132
- skip_without_distinct_on
2133
- assert_equal(Author.distinct_on { :name }.to_sql, Author.distinct_on(:name).to_sql)
2134
- end
2135
-
2136
- def test_distinct_on_takes_several
2137
- skip_without_distinct_on
2138
- assert_sql(/DISTINCT ON \( "authors"."id", "authors"."name" \)/,
2139
- Author.distinct_on { [:id, :name] }.to_sql)
2140
- end
2141
-
2142
- def test_distinct_on_takes_an_expression
2143
- skip_without_distinct_on
2144
- assert_sql(/DISTINCT ON \( UPPER\("authors"."name"\) \)/,
2145
- Author.distinct_on { upper(:name) }.to_sql)
2146
- end
2147
-
2148
- # Arel carries the node and refuses to write it elsewhere, as it does a
2149
- # regexp, so the gem has nothing of its own to say.
2150
- def test_distinct_on_says_where_it_cannot_go
2151
- skip "#{ADAPTER} has DISTINCT ON" if ADAPTER == 'postgresql'
2152
- assert_raises(NotImplementedError) { Author.distinct_on { :name }.to_sql }
2153
- end
2154
-
2155
- def test_distinct_on_needs_a_column
2156
- assert_raises(ArgumentError) { Author.distinct_on }
2157
- end
2158
-
2159
- def test_distinct_on_spawns
2160
- refute_match(/DISTINCT ON/, Author.all.to_sql)
2161
- Author.distinct_on { :name }
2162
- refute_match(/DISTINCT ON/, Author.all.to_sql)
2163
- end
2164
-
2165
- # A lateral join lets the relation joined see the row being joined to, which
2166
- # is what makes the top row of each group reachable in one query.
2167
- def top_post
2168
- Post.select { :title }.
2169
- where { :posts[:author_id] == :authors[:id] }.
2170
- order { :title.desc }.limit(1)
2171
- end
2172
-
2173
- def seed_for_lateral
2174
- Author.delete_all
2175
- Post.delete_all
2176
- author = Author.create!(name: 'writes')
2177
- Author.create!(name: 'does not')
2178
- Post.create!(author_id: author.id, title: 'a')
2179
- Post.create!(author_id: author.id, title: 'b')
2180
- end
2181
-
2182
- def test_lateral_join
2183
- skip_without_lateral
2184
- seed_for_lateral
2185
- rows = Author.joins(top_post.lateral, as: :top).
2186
- select { [:name, :top[:title].as(:v)] }.map {|r| [r.name, r.v] }
2187
- assert_equal([['writes', 'b']], rows)
2188
- end
2189
-
2190
- # Left, so that a row with nothing to join to is kept.
2191
- def test_left_outer_lateral_join
2192
- skip_without_lateral
2193
- seed_for_lateral
2194
- rows = Author.left_outer_joins(top_post.lateral, as: :top).
2195
- select { [:name, :top[:title].as(:v)] }.order { :name }.map {|r| [r.name, r.v] }
2196
- assert_equal([['does not', nil], ['writes', 'b']], rows)
2197
- end
2198
-
2199
- # Without a block the join is ON TRUE; what the subquery may see is said
2200
- # inside it.
2201
- def test_lateral_join_takes_an_on_clause
2202
- skip_without_lateral
2203
- seed_for_lateral
2204
- assert_equal(0, Author.joins(top_post.lateral, as: :top) {
2205
- :top[:title] == 'nothing'
2206
- }.count)
2207
- assert_sql(/ON TRUE/, Author.joins(top_post.lateral, as: :top).to_sql)
2208
- end
2209
-
2210
- def test_lateral_join_needs_the_mark_and_a_name
2211
- e = assert_raises(ArgumentError) { Author.joins(top_post, as: :top) }
2212
- assert_match(/mark it/, e.message)
2213
- e = assert_raises(ArgumentError) { Author.joins(top_post.lateral) }
2214
- assert_match(/needs a name/, e.message)
2215
- end
2216
-
2217
- def test_lateral_spawns
2218
- relation = top_post
2219
- assert(relation.lateral.lateral_value)
2220
- refute(relation.lateral_value)
2221
- end
2222
-
2223
- def test_lateral_join_says_where_it_cannot_go
2224
- skip 'this one has LATERAL' if ADAPTER == 'postgresql' || (ADAPTER == 'mysql2' && !mariadb?)
2225
- e = assert_raises(NotImplementedError) { Author.joins(top_post.lateral, as: :top) }
2226
- assert_match(/lateral join has no equivalent/, e.message)
2227
- end
2228
-
2229
- # Several groupings asked for at once, the totals of each coming back beside
2230
- # the rows. What is asserted is the rows, since the point is which totals
2231
- # arrive rather than how the clause is spelled.
2232
- def seed_for_grouping
2233
- Post.delete_all
2234
- Author.delete_all
2235
- a = Author.create!(name: 'a')
2236
- b = Author.create!(name: 'b')
2237
- Post.create!(author_id: a.id, title: 'x')
2238
- Post.create!(author_id: a.id, title: 'y')
2239
- Post.create!(author_id: b.id, title: 'x')
2240
- end
2241
-
2242
- def grouped(&block)
2243
- Post.group(&block).select { [:author_id, :title, count(:*).as(:n)] }.
2244
- map {|r| [r.author_id, r.title, r.n.to_i] }.sort_by(&:to_s)
2245
- end
2246
-
2247
- def test_grouping_sets
2248
- skip_without_grouping_sets
2249
- seed_for_grouping
2250
- rows = grouped { grouping_sets([:author_id], [:title], []) }
2251
- assert_equal(3, rows.count {|_, title, _| title.nil? }) # by author
2252
- assert_includes(rows, [nil, 'x', 2]) # by title
2253
- assert_includes(rows, [nil, nil, 3]) # the whole
2254
- end
2255
-
2256
- def test_rollup
2257
- skip_without_grouping_sets
2258
- seed_for_grouping
2259
- rows = grouped { rollup(:author_id, :title) }
2260
- assert_includes(rows, [nil, nil, 3])
2261
- assert_sql(/GROUP BY ROLLUP\( "posts"."author_id", "posts"."title" \)/,
2262
- Post.group { rollup(:author_id, :title) }.to_sql)
2263
- end
2264
-
2265
- def test_cube
2266
- skip_without_grouping_sets
2267
- seed_for_grouping
2268
- assert_sql(/GROUP BY CUBE\( "posts"."author_id", "posts"."title" \)/,
2269
- Post.group { cube(:author_id, :title) }.to_sql)
2270
- # Every combination: by both, by each, and the whole.
2271
- assert_equal(8, Post.group { cube(:author_id, :title) }.select { count(:*).as(:n) }.to_a.size)
2272
- end
2273
-
2274
- def test_grouping_sets_say_where_they_cannot_go
2275
- skip 'PostgreSQL has them' if ADAPTER == 'postgresql'
2276
- assert_raises(NotImplementedError) { Post.group { grouping_sets([:title]) } }
2277
- assert_raises(NotImplementedError) { Post.group { rollup(:title) } }
2278
- assert_raises(NotImplementedError) { Post.group { cube(:title) } }
2279
- end
2280
-
2281
- def test_grouping_sets_need_something_to_group_by
2282
- assert_raises(ArgumentError) { Post.group { rollup } }
2283
- assert_raises(ArgumentError) { Post.group { grouping_sets } }
2284
- end
2285
-
2286
- # bury sets what dig reads. The document comes back changed rather than
2287
- # being written anywhere, so update_all is what makes it stick.
2288
- def buried(&block)
2289
- seed_docs
2290
- Doc.where { :name == 'one' }.update_all(&block)
2291
- value = Doc.find_by(name: 'one').meta
2292
- value.is_a?(String) ? JSON.parse(value) : value
2293
- end
2294
-
2295
- def test_bury_a_nested_key
2296
- assert_equal('new', buried { { meta: :meta.bury(:a, :b, 'new') } }.dig('a', 'b'))
2297
- end
2298
-
2299
- def test_bury_a_key_that_is_not_there_yet
2300
- assert_equal(9, buried { { meta: :meta.bury(:fresh, 9) } }['fresh'])
2301
- end
2302
-
2303
- # A whole document, which each adapter takes its own way round.
2304
- def test_bury_an_object_and_an_array
2305
- assert_equal({ 'x' => 1 }, buried { { meta: :meta.bury(:obj, { 'x' => 1 }) } }['obj'])
2306
- assert_equal([1, 2], buried { { meta: :meta.bury(:arr, [1, 2]) } }['arr'])
2307
- end
2308
-
2309
- # A boolean goes in as JSON too: taken as it is, SQLite would write its 1.
2310
- def test_bury_a_boolean
2311
- assert_equal(true, buried { { meta: :meta.bury(:flag, true) } }['flag'])
2312
- assert_equal(false, buried { { meta: :meta.bury(:flag, false) } }['flag'])
2313
- end
2314
-
2315
- def test_bury_a_null
2316
- document = buried { { meta: :meta.bury(:gone, nil) } }
2317
- assert(document.key?('gone'))
2318
- assert_nil(document['gone'])
2319
- end
2320
-
2321
- def test_bury_an_array_index
2322
- assert_equal(%w[7 y], buried { { meta: :meta.bury(:tags, 0, '7') } }['tags'])
2323
- end
2324
-
2325
- # The value can be read out of the document it is going into: dig keeps
2326
- # the number a number, dig_text makes it the text of one.
2327
- def test_bury_an_expression
2328
- assert_equal(5, buried { { meta: :meta.bury(:copy, :meta.dig(:n)) } }['copy'])
2329
- assert_equal('5', buried { { meta: :meta.bury(:copy, :meta.dig_text(:n)) } }['copy'])
2330
- end
2331
-
2332
- # It is an expression, so it does not have to be written anywhere.
2333
- def test_bury_in_a_select
2334
- seed_docs
2335
- value = Doc.where { :name == 'one' }.select { :meta.bury(:a, :b, 'x').as(:v) }.first.v
2336
- assert_equal('x', (value.is_a?(String) ? JSON.parse(value) : value).dig('a', 'b'))
2337
- end
2338
-
2339
- def test_bury_needs_a_path
2340
- assert_raises(ArgumentError) { Doc.select { :meta.bury('v') } }
2341
- assert_raises(ArgumentError) { Doc.select { :meta.bury(1.5, 'v') } }
2342
- end
2343
-
2344
- # except takes keys out, by the name of what Hash does. PostgreSQL
2345
- # subtracts them where the others remove a path apiece, and what comes back
2346
- # is the same document on all three.
2347
- def test_except_a_key
2348
- assert_equal({ 'a' => { 'b' => 'deep' }, 'tags' => %w[x y], 'odd key' => 1 },
2349
- buried { { meta: :meta.except(:n) } })
2350
- end
2351
-
2352
- # A key deeper in is reached through the chain: dig reads the part out,
2353
- # except takes the key from it, and bury puts it back. The dug document
2354
- # needs its parentheses on PostgreSQL, where - binds tighter than #>.
2355
- def test_except_a_nested_key_through_the_chain
2356
- assert_equal({}, buried { { meta: :meta.bury(:a, :meta.dig(:a).except(:b)) } }['a'])
2357
- end
2358
-
2359
- def test_except_several_keys
2360
- assert_equal({ 'a' => { 'b' => 'deep' } },
2361
- buried { { meta: :meta.except(:n, :tags, :'odd key') } })
2362
- end
2363
-
2364
- # A key that is not there is not an error, as Hash#except has none for it.
2365
- def test_except_a_key_that_is_not_there
2366
- assert_equal(5, buried { { meta: :meta.except(:nothing) } }['n'])
2367
- end
2368
-
2369
- # The document a bury gives back is one to take keys out of.
2370
- def test_except_after_bury
2371
- document = buried { { meta: :meta.bury(:fresh, 9).except(:n) } }
2372
- assert_equal(9, document['fresh'])
2373
- assert_nil(document['n'])
2374
- end
2375
-
2376
- def test_except_in_a_select
2377
- seed_docs
2378
- value = Doc.where { :name == 'one' }.select { :meta.except(:n).as(:v) }.first.v
2379
- assert_nil((value.is_a?(String) ? JSON.parse(value) : value)['n'])
2380
- end
2381
-
2382
- # An index is not what the name says anywhere, and a path is bury's.
2383
- def test_except_takes_keys
2384
- assert_raises(ArgumentError) { Doc.select { :meta.except } }
2385
- e = assert_raises(ArgumentError) { Doc.select { :meta.except(0) } }
2386
- assert_match(/keys of the document/, e.message)
2387
- end
2388
-
2389
- def test_default_where_syntax
2390
- assert_sql(/WHERE "users"."name" = 'Ruby' AND "users"."age" = 19/,
2391
- User.where(name: 'Ruby', age: 19).to_sql)
2392
- end
2393
-
2394
- def test_value_in_a_select_list
2395
- assert_sql(/SELECT "users"."name", 0 AS "depth"/,
2396
- User.select { [:name, value(0).as(:depth)] }.to_sql)
2397
- end
2398
-
2399
- # Each adapter escapes the apostrophe its own way, so what is asserted is
2400
- # that the string stays a value rather than reaching the SQL as written.
2401
- def test_value_is_quoted
2402
- User.delete_all
2403
- User.create!(name: 'alice')
2404
- payload = "it's a value"
2405
- assert_sql(/SELECT 'draft' AS "state"/,
2406
- User.select { value('draft').as(:state) }.to_sql)
2407
- assert_equal([payload],
2408
- User.select { value(payload).as(:note) }.map(&:note))
2409
- end
2410
-
2411
- def test_a_bare_string_is_still_sql
2412
- assert_sql(/SELECT "users"."name", 1 \+ 1 AS two/,
2413
- User.select { [:name, '1 + 1 AS two'] }.to_sql)
2414
- end
2415
-
2416
- def test_value_takes_the_predications
2417
- assert_sql(/WHERE 1 = "users"."age"/, User.where { value(1) == :users[:age] }.to_sql)
2418
- assert_sql(/WHERE 1 IS NULL/, User.where { value(1).null? }.to_sql)
2419
- end
2420
-
2421
- def test_value_takes_the_arithmetics
2422
- assert_sql(/SELECT \(1 \+ "users"."age"\) AS "next_year"/,
2423
- User.select { (value(1) + :age).as(:next_year) }.to_sql)
2424
- end
2425
-
2426
- def test_value_as_a_function_argument
2427
- assert_sql(/SELECT COALESCE\("users"."age", 0\)/,
2428
- User.select { coalesce(:age, value(0)) }.to_sql)
2429
- end
2430
-
2431
- def test_integer_shorthand_for_value
2432
- assert_sql(/SELECT "users"."name", 0 AS "depth"/,
2433
- User.select { [:name, 0.as(:depth)] }.to_sql)
2434
- end
2435
-
2436
- def test_float_shorthand_for_value
2437
- assert_sql(/SELECT 1\.5 AS "rate"/, User.select { 1.5.as(:rate) }.to_sql)
2438
- end
2439
-
2440
- def test_numeric_shorthand_has_no_orderings
2441
- assert_raises(NoMethodError) { User.order { 1.asc } }
2442
- assert_raises(NoMethodError) { User.order { 1.desc } }
2443
- end
2444
-
2445
- # The alias on a literal is quoted like any other, so a name that is not a
2446
- # plain one arrives as itself rather than as SQL.
2447
- def test_a_value_alias_is_quoted_rather_than_refused
2448
- User.delete_all
2449
- User.create!(name: 'alice')
2450
- payload = 'a" FROM users; --'
2451
- assert_equal(0, User.select { value(0).as(payload.to_sym) }.first[payload].to_i)
2452
- assert_equal(0, User.select { 0.as(payload.to_sym) }.first[payload].to_i)
2453
- assert_equal(1, User.count)
2454
- end
2455
-
2456
- def test_a_value_selected_reaches_the_row
2457
- User.delete_all
2458
- User.create!(name: 'alice', age: 60)
2459
- assert_equal([['alice', 0]],
2460
- User.select { [:name, 0.as(:depth)] }.map {|u| [u.name, u.depth] })
2461
- end
2462
-
2463
- def test_numeric_shorthand_is_confined_to_the_block
2464
- assert_raises(NoMethodError) { 0.as(:depth) }
2465
- end
2466
-
2467
- # pglite is PostgreSQL compiled to WebAssembly, which the sandbox runs in the
2468
- # browser. Its adapter answers to a name of its own, so without this the
2469
- # spellings would fall back to the standard ones and the browser would be
2470
- # told PostgreSQL's JSON operators do not exist.
2471
- def test_adapter_families
2472
- model = Class.new do
2473
- def self.with_adapter(name)
2474
- config = Struct.new(:adapter).new(name)
2475
- Class.new { define_singleton_method(:connection_db_config) { config } }
2476
- end
2477
- end
2478
-
2479
- {
2480
- 'sqlite3' => :sqlite,
2481
- 'postgresql' => :postgresql,
2482
- 'postgis' => :postgresql,
2483
- 'pglite' => :postgresql,
2484
- 'mysql2' => :mysql,
2485
- 'trilogy' => :mysql,
2486
- 'nothing_of_the_sort' => :unknown,
2487
- }.each do |adapter, family|
2488
- assert_equal(family,
2489
- ActiveRecord::Refined::AST.adapter_family(model.with_adapter(adapter)),
2490
- adapter)
2491
- end
2492
- end
2493
- end