activerecord-refined 0.9.0 → 0.10.1

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