activerecord-refined 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,14 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
2
4
 
3
5
  class TestBlockSyntax < Minitest::Test
4
6
  def test_equal
5
- assert_sql(/WHERE "users"."name" = 'alice'/, User.where { :name == 'alice' }.to_sql)
7
+ assert_sql(/WHERE "users"."name" = 'alice'/, User.where { :name == "alice" }.to_sql)
6
8
  end
7
9
 
8
10
  def test_not_equal
9
- assert_sql(/WHERE "users"."name" != 'bob'/, User.where { :name != 'bob' }.to_sql)
11
+ assert_sql(/WHERE "users"."name" != 'bob'/, User.where { :name != "bob" }.to_sql)
10
12
  end
11
13
 
12
14
  def test_greater_than
@@ -26,7 +28,7 @@ class TestBlockSyntax < Minitest::Test
26
28
  end
27
29
 
28
30
  def test_like
29
- assert_sql(/WHERE "users"."name" LIKE 'tender%'/, User.where { :name.like?('tender%') }.to_sql)
31
+ assert_sql(/WHERE "users"."name" LIKE 'tender%'/, User.where { :name.like?("tender%") }.to_sql)
30
32
  end
31
33
 
32
34
  def test_outside_of_where_block
@@ -35,21 +37,21 @@ class TestBlockSyntax < Minitest::Test
35
37
 
36
38
  def test_and
37
39
  assert_sql(/WHERE "users"."name" = 'alice' AND "users"."age" > 18/,
38
- User.where { (:name == 'alice') & (:age > 18) }.to_sql)
40
+ User.where { (:name == "alice") & (:age > 18) }.to_sql)
39
41
  end
40
42
 
41
43
  def test_or
42
- assert_sql(/WHERE \(?\"users\".\"name\" = 'alice' OR \"users\".\"name\" = 'bob'\)?/,
43
- User.where { (:name == 'alice') | (:name == 'bob') }.to_sql)
44
+ assert_sql(/WHERE \(?"users"."name" = 'alice' OR "users"."name" = 'bob'\)?/,
45
+ User.where { (:name == "alice") | (:name == "bob") }.to_sql)
44
46
  end
45
47
 
46
48
  def test_not
47
- assert_sql(/WHERE NOT \(?\"users\".\"name\" = 'alice'\)?/,
48
- User.where { !(:name == 'alice') }.to_sql)
49
+ assert_sql(/WHERE NOT \(?"users"."name" = 'alice'\)?/,
50
+ User.where { !(:name == "alice") }.to_sql)
49
51
  end
50
52
 
51
53
  def test_complex_combination
52
- sql = User.where { ((:name == 'alice') & (:age > 18)) | !(:name == 'bob') }.to_sql
54
+ sql = User.where { ((:name == "alice") & (:age > 18)) | !(:name == "bob") }.to_sql
53
55
  assert_sql(/"users"."name" = 'alice'/, sql)
54
56
  assert_sql(/"users"."age" > 18/, sql)
55
57
  assert_sql(/NOT/, sql)
@@ -58,25 +60,25 @@ class TestBlockSyntax < Minitest::Test
58
60
 
59
61
  def test_like_qualified
60
62
  assert_sql(/WHERE "users"."name" LIKE 'tender%'/,
61
- User.where { :users[:name].like?('tender%') }.to_sql)
63
+ User.where { :users[:name].like?("tender%") }.to_sql)
62
64
  end
63
65
 
64
66
  # ILIKE is PostgreSQL's; elsewhere Arel emits LIKE, which those adapters
65
67
  # already match case-insensitively by default.
66
68
  def test_ilike
67
- expected = ADAPTER == 'postgresql' ? 'ILIKE' : 'LIKE'
69
+ expected = ADAPTER == "postgresql" ? "ILIKE" : "LIKE"
68
70
  assert_sql(/WHERE "users"."name" #{expected} 'ma%'/,
69
- User.where { :name.ilike?('ma%') }.to_sql)
71
+ User.where { :name.ilike?("ma%") }.to_sql)
70
72
  end
71
73
 
72
74
  def test_casecmp
73
75
  assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Alice'\)/,
74
- User.where { :name.casecmp?('Alice') }.to_sql)
76
+ User.where { :name.casecmp?("Alice") }.to_sql)
75
77
  end
76
78
 
77
79
  def test_casecmp_qualified
78
80
  assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Alice'\)/,
79
- User.where { :users[:name].casecmp?('Alice') }.to_sql)
81
+ User.where { :users[:name].casecmp?("Alice") }.to_sql)
80
82
  end
81
83
 
82
84
  def test_casecmp_nil_is_rejected
@@ -86,40 +88,40 @@ class TestBlockSyntax < Minitest::Test
86
88
 
87
89
  def test_casecmp_execution
88
90
  User.delete_all
89
- User.create!(name: 'Alice')
90
- User.create!(name: 'bob')
91
- assert_equal(['Alice'], User.where { :name.casecmp?('aLiCe') }.pluck(:name))
91
+ User.create!(name: "Alice")
92
+ User.create!(name: "bob")
93
+ assert_equal(["Alice"], User.where { :name.casecmp?("aLiCe") }.pluck(:name))
92
94
  end
93
95
 
94
96
  def test_bang_negates_like
95
97
  assert_sql(/WHERE NOT \("users"."name" LIKE 'tender%'\)/,
96
- User.where { !:name.like?('tender%') }.to_sql)
98
+ User.where { !:name.like?("tender%") }.to_sql)
97
99
  end
98
100
 
99
101
  def test_not_like
100
102
  assert_sql(/WHERE "users"."name" NOT LIKE 'tender%'/,
101
- User.where { :name.not_like?('tender%') }.to_sql)
103
+ User.where { :name.not_like?("tender%") }.to_sql)
102
104
  end
103
105
 
104
106
  def test_not_ilike
105
- expected = ADAPTER == 'postgresql' ? 'ILIKE' : 'LIKE'
107
+ expected = ADAPTER == "postgresql" ? "ILIKE" : "LIKE"
106
108
  assert_sql(/WHERE "users"."name" NOT #{expected} 'tender%'/,
107
- User.where { :name.not_ilike?('tender%') }.to_sql)
109
+ User.where { :name.not_ilike?("tender%") }.to_sql)
108
110
  end
109
111
 
110
112
  def test_start_with
111
113
  assert_sql(/WHERE "users"."name" LIKE 'tender%' ESCAPE '\\'/,
112
- User.where { :name.start_with?('tender') }.to_sql)
114
+ User.where { :name.start_with?("tender") }.to_sql)
113
115
  end
114
116
 
115
117
  def test_end_with
116
118
  assert_sql(/WHERE "users"."name" LIKE '%love' ESCAPE '\\'/,
117
- User.where { :name.end_with?('love') }.to_sql)
119
+ User.where { :name.end_with?("love") }.to_sql)
118
120
  end
119
121
 
120
122
  def test_include
121
123
  assert_sql(/WHERE "users"."name" LIKE '%der%' ESCAPE '\\'/,
122
- User.where { :name.include?('der') }.to_sql)
124
+ User.where { :name.include?("der") }.to_sql)
123
125
  end
124
126
 
125
127
  # Like their String namesakes, start_with? and end_with? take any number
@@ -127,20 +129,20 @@ class TestBlockSyntax < Minitest::Test
127
129
  def test_start_with_multiple
128
130
  assert_sql(
129
131
  /WHERE \("users"."name" LIKE 'al%' ESCAPE '\\' OR "users"."name" LIKE 'bo%' ESCAPE '\\'\)/,
130
- User.where { :name.start_with?('al', 'bo') }.to_sql)
132
+ User.where { :name.start_with?("al", "bo") }.to_sql)
131
133
  end
132
134
 
133
135
  def test_end_with_multiple
134
136
  assert_sql(
135
137
  /WHERE \("users"."name" LIKE '%z' ESCAPE '\\' OR "users"."name" LIKE '%love' ESCAPE '\\'\)/,
136
- User.where { :name.end_with?('z', 'love') }.to_sql)
138
+ User.where { :name.end_with?("z", "love") }.to_sql)
137
139
  end
138
140
 
139
141
  # The OR arrives grouped, so a following & applies to the whole list.
140
142
  def test_start_with_multiple_combined
141
143
  assert_sql(
142
144
  /WHERE \("users"."name" LIKE 'al%' ESCAPE '\\' OR "users"."name" LIKE 'bo%' ESCAPE '\\'\) AND "users"."age" > 18/,
143
- User.where { :name.start_with?('al', 'bo') & (:age > 18) }.to_sql)
145
+ User.where { :name.start_with?("al", "bo") & (:age > 18) }.to_sql)
144
146
  end
145
147
 
146
148
  def test_start_with_no_arguments
@@ -153,27 +155,27 @@ class TestBlockSyntax < Minitest::Test
153
155
 
154
156
  def test_start_with_escapes_wildcards
155
157
  assert_sql(/WHERE "users"."name" LIKE '100\\%\\_%' ESCAPE '\\'/,
156
- User.where { :name.start_with?('100%_') }.to_sql)
158
+ User.where { :name.start_with?("100%_") }.to_sql)
157
159
  end
158
160
 
159
161
  def test_include_escapes_wildcards
160
162
  assert_sql(/WHERE "users"."name" LIKE '%100\\%%' ESCAPE '\\'/,
161
- User.where { :name.include?('100%') }.to_sql)
163
+ User.where { :name.include?("100%") }.to_sql)
162
164
  end
163
165
 
164
166
  def test_member
165
167
  assert_sql(/WHERE "users"."tags" @> '\{ruby\}'/,
166
- User.where { :tags.member?('ruby') }.to_sql)
168
+ User.where { :tags.member?("ruby") }.to_sql)
167
169
  end
168
170
 
169
171
  def test_member_qualified
170
172
  assert_sql(/WHERE "users"."tags" @> '\{ruby\}'/,
171
- User.where { :users[:tags].member?('ruby') }.to_sql)
173
+ User.where { :users[:tags].member?("ruby") }.to_sql)
172
174
  end
173
175
 
174
176
  def test_member_negated
175
177
  assert_sql(/WHERE NOT \("users"."tags" @> '\{ruby\}'\)/,
176
- User.where { !:tags.member?('ruby') }.to_sql)
178
+ User.where { !:tags.member?("ruby") }.to_sql)
177
179
  end
178
180
 
179
181
  # Ruby's [1, 2].member?([1]) is false: member? tests one element, and an
@@ -190,11 +192,11 @@ class TestBlockSyntax < Minitest::Test
190
192
 
191
193
  def test_superset_takes_a_set
192
194
  assert_sql(/WHERE "users"."tags" @> '\{ruby,rails\}'/,
193
- User.where { :tags.superset?(Set['ruby', 'rails']) }.to_sql)
195
+ User.where { :tags.superset?(Set["ruby", "rails"]) }.to_sql)
194
196
  end
195
197
 
196
198
  def test_superset_rejects_a_scalar
197
- assert_raises(ArgumentError) { User.where { :tags.superset?('ruby') } }
199
+ assert_raises(ArgumentError) { User.where { :tags.superset?("ruby") } }
198
200
  end
199
201
 
200
202
  def test_subset
@@ -215,11 +217,11 @@ class TestBlockSyntax < Minitest::Test
215
217
  def test_array_comparisons_execution
216
218
  skip_without_array_columns
217
219
  User.delete_all
218
- User.create!(name: 'both', tags: %w[ruby rails])
219
- User.create!(name: 'one', tags: %w[ruby go])
220
- User.create!(name: 'neither', tags: %w[python])
221
- assert_equal(['both'], User.where { :tags.superset?(%w[ruby rails]) }.pluck(:name))
222
- assert_equal(['neither'], User.where { :tags.subset?(%w[python js]) }.pluck(:name))
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))
223
225
  assert_equal(%w[both one],
224
226
  User.where { :tags.intersect?(%w[ruby js]) }.pluck(:name).sort)
225
227
  end
@@ -229,7 +231,7 @@ class TestBlockSyntax < Minitest::Test
229
231
  def test_member_quotes_special_elements
230
232
  skip_without_array_columns
231
233
  assert_sql(/WHERE "users"."tags" @> '\{"with,comma"\}'/,
232
- User.where { :tags.member?('with,comma') }.to_sql)
234
+ User.where { :tags.member?("with,comma") }.to_sql)
233
235
  end
234
236
 
235
237
  # include? is a substring match even on an array column; only member?
@@ -237,7 +239,7 @@ class TestBlockSyntax < Minitest::Test
237
239
  def test_include_is_like_even_on_array_columns
238
240
  skip_without_array_columns
239
241
  assert_sql(/WHERE "users"."tags" LIKE '%ruby%' ESCAPE '\\'/,
240
- User.where { :tags.include?('ruby') }.to_sql)
242
+ User.where { :tags.include?("ruby") }.to_sql)
241
243
  end
242
244
 
243
245
  # Elements survive the trip through the array literal: % is an ordinary
@@ -246,36 +248,36 @@ class TestBlockSyntax < Minitest::Test
246
248
  def test_member_matches_elements_literally
247
249
  skip_without_array_columns
248
250
  User.delete_all
249
- User.create!(name: 'literal', tags: ['100%', 'with,comma', 'q"uote', 'back\\slash'])
250
- User.create!(name: 'lookalike', tags: ['100200', 'with', 'comma'])
251
- assert_equal(['literal'], User.where { :tags.member?('100%') }.pluck(:name))
252
- assert_equal(['literal'], User.where { :tags.member?('with,comma') }.pluck(:name))
253
- assert_equal(['literal'], User.where { :tags.member?('q"uote') }.pluck(:name))
254
- assert_equal(['literal'], User.where { :tags.member?('back\\slash') }.pluck(:name))
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))
255
257
  end
256
258
 
257
259
  def test_regexp
258
260
  skip_without_regexp_support
259
261
  assert_sql(/WHERE "users"."name" #{regexp_operator} '\^ma'/,
260
- User.where { :name =~ '^ma' }.to_sql)
262
+ User.where { :name =~ "^ma" }.to_sql)
261
263
  end
262
264
 
263
265
  def test_not_regexp
264
266
  skip_without_regexp_support
265
267
  assert_sql(/WHERE "users"."name" #{not_regexp_operator} '\^ma'/,
266
- User.where { :name !~ '^ma' }.to_sql)
268
+ User.where { :name !~ "^ma" }.to_sql)
267
269
  end
268
270
 
269
271
  def test_regexp_qualified
270
272
  skip_without_regexp_support
271
273
  assert_sql(/WHERE "users"."name" #{regexp_operator} '\^ma'/,
272
- User.where { :users[:name] =~ '^ma' }.to_sql)
274
+ User.where { :users[:name] =~ "^ma" }.to_sql)
273
275
  end
274
276
 
275
277
  def test_regexp_on_function
276
278
  skip_without_regexp_support
277
279
  assert_sql(/WHERE UPPER\("users"."name"\) #{regexp_operator} '\^MA'/,
278
- User.where { upper(:name) =~ '^MA' }.to_sql)
280
+ User.where { upper(:name) =~ "^MA" }.to_sql)
279
281
  end
280
282
 
281
283
  def test_regexp_literal
@@ -350,18 +352,18 @@ class TestBlockSyntax < Minitest::Test
350
352
  # `when` against, or a condition on every `when`.
351
353
  def test_case_with_an_operand
352
354
  assert_sql(/SELECT CASE "users"."age" WHEN 10 THEN 'ten' ELSE 'other' END AS "v"/,
353
- User.select { self.case(:age).when(10).then('ten').else('other').as(:v) }.to_sql)
355
+ User.select { self.case(:age).when(10).then("ten").else("other").as(:v) }.to_sql)
354
356
  end
355
357
 
356
358
  def test_when_on_a_column_is_the_same_case
357
359
  assert_equal(
358
- User.select { self.case(:age).when(10).then('ten').else('other').as(:v) }.to_sql,
359
- User.select { :age.when(10).then('ten').else('other').as(:v) }.to_sql)
360
+ 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)
360
362
  end
361
363
 
362
364
  def test_searched_case
363
365
  assert_sql(/SELECT CASE WHEN "users"."age" >= 60 THEN 'senior' ELSE 'other' END AS "v"/,
364
- User.select { case_when { :age >= 60 }.then('senior').else('other').as(:v) }.to_sql)
366
+ User.select { case_when { :age >= 60 }.then("senior").else("other").as(:v) }.to_sql)
365
367
  end
366
368
 
367
369
  def test_case_when_is_the_same_as_case_with_no_operand
@@ -382,15 +384,15 @@ class TestBlockSyntax < Minitest::Test
382
384
  assert_sql(
383
385
  /CASE WHEN "users"."age" < 18 THEN 'minor' WHEN "users"."age" >= 60 THEN 'senior' ELSE 'adult' END/,
384
386
  User.select {
385
- case_when { :age < 18 }.then('minor').
386
- when { :age >= 60 }.then('senior').
387
- else('adult').as(:v)
387
+ case_when { :age < 18 }.then("minor").
388
+ when { :age >= 60 }.then("senior").
389
+ else("adult").as(:v)
388
390
  }.to_sql)
389
391
  end
390
392
 
391
393
  # Leaving the ELSE off is SQL's own default rather than an omission.
392
394
  def test_case_without_an_else
393
- sql = User.select { case_when { :age >= 60 }.then('senior').as(:v) }.to_sql
395
+ sql = User.select { case_when { :age >= 60 }.then("senior").as(:v) }.to_sql
394
396
  assert_sql(/CASE WHEN "users"."age" >= 60 THEN 'senior' END/, sql)
395
397
  refute_match(/ELSE/, sql)
396
398
  end
@@ -399,7 +401,7 @@ class TestBlockSyntax < Minitest::Test
399
401
  assert_sql(/THEN \("users"."age" - 60\)/,
400
402
  User.select { case_when { :age >= 60 }.then { :age - 60 }.else(0).as(:v) }.to_sql)
401
403
  assert_sql(/THEN "users"."name"/,
402
- User.select { case_when { :age >= 60 }.then(:name).else('x').as(:v) }.to_sql)
404
+ User.select { case_when { :age >= 60 }.then(:name).else("x").as(:v) }.to_sql)
403
405
  end
404
406
 
405
407
  def test_case_is_an_expression_like_any_other
@@ -411,14 +413,14 @@ class TestBlockSyntax < Minitest::Test
411
413
 
412
414
  def test_case_execution
413
415
  User.delete_all
414
- User.create!(name: 'senior', age: 70)
415
- User.create!(name: 'adult', age: 30)
416
- User.create!(name: 'minor', age: 10)
416
+ User.create!(name: "senior", age: 70)
417
+ User.create!(name: "adult", age: 30)
418
+ User.create!(name: "minor", age: 10)
417
419
  assert_equal(%w[adult minor senior],
418
420
  User.select {
419
- case_when { :age < 18 }.then('minor').
420
- when { :age >= 60 }.then('senior').
421
- else('adult').as(:v)
421
+ case_when { :age < 18 }.then("minor").
422
+ when { :age >= 60 }.then("senior").
423
+ else("adult").as(:v)
422
424
  }.map(&:v).sort)
423
425
  end
424
426
 
@@ -500,13 +502,13 @@ class TestBlockSyntax < Minitest::Test
500
502
 
501
503
  def test_window_execution
502
504
  User.delete_all
503
- User.create!(name: 'a', age: 20)
504
- User.create!(name: 'b', age: 30)
505
- User.create!(name: 'c', age: 40)
505
+ User.create!(name: "a", age: 20)
506
+ User.create!(name: "b", age: 30)
507
+ User.create!(name: "c", age: 40)
506
508
  assert_equal([1, 2, 3],
507
- User.select { row_number.over.order(:age).as(:v) }.map {|u| u.v.to_i })
509
+ User.select { row_number.over.order(:age).as(:v) }.map { |u| u.v.to_i })
508
510
  assert_equal([20, 50, 90],
509
- User.select { sum(:age).over.order(:age).rows(..0).as(:v) }.map {|u| u.v.to_i })
511
+ User.select { sum(:age).over.order(:age).rows(..0).as(:v) }.map { |u| u.v.to_i })
510
512
  end
511
513
 
512
514
  # One window finished two ways: the methods return new nodes.
@@ -530,7 +532,7 @@ class TestBlockSyntax < Minitest::Test
530
532
 
531
533
  def test_a_frame_is_a_range_of_rows
532
534
  assert_raises(ArgumentError) { User.select { sum(:age).over.rows(3) } }
533
- assert_raises(ArgumentError) { User.select { sum(:age).over.rows('a'..'b') } }
535
+ assert_raises(ArgumentError) { User.select { sum(:age).over.rows("a".."b") } }
534
536
  e = assert_raises(ArgumentError) { User.select { sum(:age).over.rows(-2...0) } }
535
537
  assert_match(/ends on a row/, e.message)
536
538
  end
@@ -550,14 +552,14 @@ class TestBlockSyntax < Minitest::Test
550
552
  # if there were one.
551
553
  def test_the_negations_match_what_bang_selects
552
554
  User.delete_all
553
- User.create!(name: 'alice', age: 60, active: true)
554
- User.create!(name: 'bob', age: 20, active: false)
555
+ User.create!(name: "alice", age: 60, active: true)
556
+ User.create!(name: "bob", age: 20, active: false)
555
557
  User.create!(name: nil, age: 40)
556
558
  [
557
559
  [-> { :name.not_null? }, -> { !:name.null? }],
558
560
  [-> { :age.not_in?([20, 30]) }, -> { !:age.in?([20, 30]) }],
559
561
  [-> { :age.not_between?(20, 30) }, -> { !:age.between?(20, 30) }],
560
- [-> { :name.not_like?('a%') }, -> { !:name.like?('a%') }],
562
+ [-> { :name.not_like?("a%") }, -> { !:name.like?("a%") }],
561
563
  [-> { :active.not_true? }, -> { !:active.true? }],
562
564
  [-> { :active.not_false? }, -> { !:active.false? }],
563
565
  ].each do |direct, negated|
@@ -597,12 +599,12 @@ class TestBlockSyntax < Minitest::Test
597
599
  # included, which is what makes them worth having over = TRUE.
598
600
  def test_truth_values_execution
599
601
  User.delete_all
600
- User.create!([{name: 'yes', active: true}, {name: 'no', active: false},
601
- {name: 'unset', active: nil}])
602
+ User.create!([{ name: "yes", active: true }, { name: "no", active: false },
603
+ { name: "unset", active: nil }])
602
604
  order = ->(relation) { relation.order(:name).pluck(:name) }
603
- assert_equal(['yes'], order.(User.where { :active.true? }))
605
+ assert_equal(["yes"], order.(User.where { :active.true? }))
604
606
  assert_equal(%w[no unset], order.(User.where { :active.not_true? }))
605
- assert_equal(['no'], order.(User.where { :active.false? }))
607
+ assert_equal(["no"], order.(User.where { :active.false? }))
606
608
  assert_equal(%w[unset yes], order.(User.where { :active.not_false? }))
607
609
  end
608
610
 
@@ -610,9 +612,9 @@ class TestBlockSyntax < Minitest::Test
610
612
  # is NULL for a NULL row, and negating it leaves that row out.
611
613
  def test_not_true_keeps_the_nulls_equality_drops
612
614
  User.delete_all
613
- User.create!([{name: 'no', active: false}, {name: 'unset', active: nil}])
615
+ User.create!([{ name: "no", active: false }, { name: "unset", active: nil }])
614
616
  assert_equal(%w[no unset], User.where { :active.not_true? }.order(:name).pluck(:name))
615
- assert_equal(['no'], User.where { !(:active == true) }.order(:name).pluck(:name))
617
+ assert_equal(["no"], User.where { !(:active == true) }.order(:name).pluck(:name))
616
618
  end
617
619
 
618
620
  def test_in
@@ -644,27 +646,27 @@ class TestBlockSyntax < Minitest::Test
644
646
  # <=> on MySQL, so only the resulting rows are portable.
645
647
  def test_not_distinct_from_execution
646
648
  User.delete_all
647
- User.create!(name: 'named')
649
+ User.create!(name: "named")
648
650
  User.create!(name: nil)
649
651
  assert_equal([nil], User.where { :name.not_distinct_from?(nil) }.pluck(:name))
650
- assert_equal(['named'], User.where { :name.distinct_from?(nil) }.pluck(:name))
652
+ assert_equal(["named"], User.where { :name.distinct_from?(nil) }.pluck(:name))
651
653
  end
652
654
 
653
655
  def test_not_distinct_from_a_value_execution
654
656
  User.delete_all
655
- User.create!(name: 'alice')
657
+ User.create!(name: "alice")
656
658
  User.create!(name: nil)
657
- assert_equal(['alice'], User.where { :name.not_distinct_from?('alice') }.pluck(:name))
659
+ assert_equal(["alice"], User.where { :name.not_distinct_from?("alice") }.pluck(:name))
658
660
  # Unlike !=, this keeps the NULL row.
659
- assert_equal([nil], User.where { :name.distinct_from?('alice') }.pluck(:name))
661
+ assert_equal([nil], User.where { :name.distinct_from?("alice") }.pluck(:name))
660
662
  end
661
663
 
662
664
  def test_distinct_from_postgresql_syntax
663
- skip "#{ADAPTER} spells it differently" unless ADAPTER == 'postgresql'
665
+ skip "#{ADAPTER} spells it differently" unless ADAPTER == "postgresql"
664
666
  assert_sql(/WHERE "users"."name" IS NOT DISTINCT FROM 'x'/,
665
- User.where { :name.not_distinct_from?('x') }.to_sql)
667
+ User.where { :name.not_distinct_from?("x") }.to_sql)
666
668
  assert_sql(/WHERE "users"."name" IS DISTINCT FROM 'x'/,
667
- User.where { :name.distinct_from?('x') }.to_sql)
669
+ User.where { :name.distinct_from?("x") }.to_sql)
668
670
  end
669
671
 
670
672
  def test_comparison_with_scalar_subquery
@@ -685,15 +687,15 @@ class TestBlockSyntax < Minitest::Test
685
687
 
686
688
  def test_scalar_subquery_execution
687
689
  User.delete_all
688
- User.create!(name: 'young', age: 20)
689
- User.create!(name: 'old', age: 60)
690
- assert_equal(['old'], User.where { :age >= User.select { avg(:age) } }.pluck(:name))
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))
691
693
  end
692
694
 
693
695
  def test_in_subquery
694
696
  assert_sql(
695
697
  /WHERE "authors"."id" IN \(SELECT "posts"."author_id" FROM "posts" WHERE "posts"."title" = 'pub'\)/,
696
- Author.where { :id.in?(Post.where(title: 'pub').select(:author_id)) }.to_sql)
698
+ Author.where { :id.in?(Post.where(title: "pub").select(:author_id)) }.to_sql)
697
699
  end
698
700
 
699
701
  # A relation without an explicit select list selects its primary key, the
@@ -712,7 +714,7 @@ class TestBlockSyntax < Minitest::Test
712
714
  skip_without_quantifiers
713
715
  assert_sql(
714
716
  /WHERE "users"."age" > ANY\(SELECT "users"."age" FROM "users" WHERE "users"."name" = 'alice'\)/,
715
- User.where { :age > any(User.where(name: 'alice').select(:age)) }.to_sql)
717
+ User.where { :age > any(User.where(name: "alice").select(:age)) }.to_sql)
716
718
  end
717
719
 
718
720
  def test_all_subquery
@@ -736,7 +738,7 @@ class TestBlockSyntax < Minitest::Test
736
738
  end
737
739
 
738
740
  def test_quantifier_is_unsupported_on_sqlite
739
- if ADAPTER == 'sqlite3'
741
+ if ADAPTER == "sqlite3"
740
742
  e = assert_raises(NotImplementedError) { User.where { :age > any(User.select(:age)) } }
741
743
  assert_match(/ANY/, e.message)
742
744
  else
@@ -749,12 +751,12 @@ class TestBlockSyntax < Minitest::Test
749
751
  def test_quantifier_execution
750
752
  skip_without_quantifiers
751
753
  User.delete_all
752
- User.create!([{name: 'young', age: 20}, {name: 'middle', age: 40},
753
- {name: 'old', age: 60}])
754
+ User.create!([{ name: "young", age: 20 }, { name: "middle", age: 40 },
755
+ { name: "old", age: 60 }])
754
756
  ages = -> { User.select(:age) }
755
757
  assert_equal(%w[middle old], User.where { :age > any(ages.call) }.order(:age).pluck(:name))
756
- assert_equal(['old'], User.where { :age >= all(ages.call) }.pluck(:name))
757
- assert_equal(['young'], User.where { :age <= all(ages.call) }.pluck(:name))
758
+ assert_equal(["old"], User.where { :age >= all(ages.call) }.pluck(:name))
759
+ assert_equal(["young"], User.where { :age <= all(ages.call) }.pluck(:name))
758
760
  end
759
761
 
760
762
  # = ANY is IN and != ALL is NOT IN, which is worth a test because it is the
@@ -762,10 +764,10 @@ class TestBlockSyntax < Minitest::Test
762
764
  def test_quantifier_equality_execution
763
765
  skip_without_quantifiers
764
766
  User.delete_all
765
- User.create!([{name: 'young', age: 20}, {name: 'old', age: 60}])
766
- young = -> { User.where(name: 'young').select(:age) }
767
- assert_equal(['young'], User.where { :age == any(young.call) }.pluck(:name))
768
- assert_equal(['old'], User.where { :age != all(young.call) }.pluck(:name))
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))
769
771
  end
770
772
 
771
773
  # The subquery correlates with the outer table through qualified columns,
@@ -784,33 +786,33 @@ class TestBlockSyntax < Minitest::Test
784
786
  def test_exists_combined
785
787
  assert_sql(
786
788
  /WHERE "authors"."name" = 'alice' AND EXISTS \(SELECT "posts"\.\* FROM "posts" WHERE "posts"."title" = 'pub'\)/,
787
- Author.where { (:name == 'alice') & exists?(Post.where(title: 'pub')) }.to_sql)
789
+ Author.where { (:name == "alice") & exists?(Post.where(title: "pub")) }.to_sql)
788
790
  end
789
791
 
790
792
  def test_exists_execution
791
793
  Author.delete_all
792
794
  Post.delete_all
793
- with_post = Author.create!(name: 'with_post')
794
- Author.create!(name: 'without')
795
- Post.create!(title: 'pub', author_id: with_post.id)
795
+ with_post = Author.create!(name: "with_post")
796
+ Author.create!(name: "without")
797
+ Post.create!(title: "pub", author_id: with_post.id)
796
798
  correlated = -> { Post.where { :posts[:author_id] == :authors[:id] } }
797
- assert_equal(['with_post'],
799
+ assert_equal(["with_post"],
798
800
  Author.where { exists?(correlated.call) }.pluck(:name))
799
- assert_equal(['without'],
801
+ assert_equal(["without"],
800
802
  Author.where { !exists?(correlated.call) }.pluck(:name))
801
803
  end
802
804
 
803
805
  def test_in_subquery_execution
804
806
  Author.delete_all
805
807
  Post.delete_all
806
- published = Author.create!(name: 'published')
807
- drafting = Author.create!(name: 'drafting')
808
- Post.create!(title: 'pub', author_id: published.id)
809
- Post.create!(title: 'draft', author_id: drafting.id)
810
- subquery = -> { Post.where(title: 'pub').select(:author_id) }
811
- assert_equal(['published'],
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"],
812
814
  Author.where { :id.in?(subquery.call) }.pluck(:name))
813
- assert_equal(['drafting'],
815
+ assert_equal(["drafting"],
814
816
  Author.where { !:id.in?(subquery.call) }.pluck(:name))
815
817
  end
816
818
 
@@ -840,7 +842,7 @@ class TestBlockSyntax < Minitest::Test
840
842
 
841
843
  def test_qualified_column
842
844
  assert_sql(/WHERE "users"."name" = 'alice'/,
843
- User.where { :users[:name] == 'alice' }.to_sql)
845
+ User.where { :users[:name] == "alice" }.to_sql)
844
846
  end
845
847
 
846
848
  def test_column_to_column_comparison
@@ -892,7 +894,7 @@ class TestBlockSyntax < Minitest::Test
892
894
  end
893
895
 
894
896
  def test_full_outer_joins_says_where_it_cannot_go
895
- skip "#{ADAPTER} has FULL OUTER JOIN" unless ADAPTER == 'mysql2'
897
+ skip "#{ADAPTER} has FULL OUTER JOIN" unless ADAPTER == "mysql2"
896
898
  e = assert_raises(NotImplementedError) do
897
899
  Author.full_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }
898
900
  end
@@ -910,7 +912,7 @@ class TestBlockSyntax < Minitest::Test
910
912
  def test_the_other_outer_joins_need_a_block
911
913
  e = assert_raises(ArgumentError) { Author.right_outer_joins(:posts) }
912
914
  assert_match(/takes a table and the block/, e.message)
913
- unless ADAPTER == 'mysql2'
915
+ unless ADAPTER == "mysql2"
914
916
  assert_raises(ArgumentError) { Author.full_outer_joins(:posts) }
915
917
  end
916
918
  end
@@ -933,21 +935,21 @@ class TestBlockSyntax < Minitest::Test
933
935
  def test_cross_joins_execution
934
936
  Author.delete_all
935
937
  Post.delete_all
936
- Author.create!(name: 'a')
937
- Author.create!(name: 'b')
938
- Post.create!(title: 'one')
939
- Post.create!(title: 'two')
940
- Post.create!(title: 'three')
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")
941
943
  assert_equal(6, Author.cross_joins(:posts).count)
942
944
  end
943
945
 
944
946
  def test_right_outer_joins_execution
945
947
  Author.delete_all
946
948
  Post.delete_all
947
- author = Author.create!(name: 'a')
948
- Post.create!(title: 'hers', author_id: author.id)
949
- Post.create!(title: 'nobody\'s', author_id: nil)
950
- assert_equal(['a', nil],
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],
951
953
  Author.right_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }.
952
954
  order { :posts[:title] }.pluck(:'authors.name'))
953
955
  end
@@ -959,8 +961,8 @@ class TestBlockSyntax < Minitest::Test
959
961
 
960
962
  def test_self_join_execution
961
963
  Author.delete_all
962
- Author.create!(name: 'shared')
963
- Author.create!(name: 'other')
964
+ Author.create!(name: "shared")
965
+ Author.create!(name: "other")
964
966
  assert_equal(%w[other shared],
965
967
  Author.joins(:authors, as: :mentors) { :mentors[:name] == :authors[:name] }.
966
968
  pluck(:name).sort)
@@ -976,11 +978,11 @@ class TestBlockSyntax < Minitest::Test
976
978
  end
977
979
 
978
980
  def test_from_string_still_delegates
979
- assert_sql(/FROM subq/, Node.from('subq').to_sql)
981
+ assert_sql(/FROM subq/, Node.from("subq").to_sql)
980
982
  end
981
983
 
982
984
  def test_from_alias_needs_a_symbol
983
- assert_raises(ArgumentError) { Node.from('tree', as: :nodes) }
985
+ assert_raises(ArgumentError) { Node.from("tree", as: :nodes) }
984
986
  end
985
987
 
986
988
  def test_from_cte_takes_the_alias_from_the_model
@@ -991,7 +993,7 @@ class TestBlockSyntax < Minitest::Test
991
993
  end
992
994
 
993
995
  def test_from_cte_needs_a_symbol
994
- assert_raises(ArgumentError) { Node.from_cte('tree') }
996
+ assert_raises(ArgumentError) { Node.from_cte("tree") }
995
997
  end
996
998
 
997
999
  # The name has to be one `with` declares, or the query is against a table
@@ -1025,10 +1027,10 @@ class TestBlockSyntax < Minitest::Test
1025
1027
  # from_cte exists; without it the SQL names a table the query does not have.
1026
1028
  def test_from_cte_leaves_where_able_to_qualify
1027
1029
  Node.delete_all
1028
- root = Node.create!(name: 'root')
1029
- Node.create!(name: 'child', parent_id: root.id)
1030
- other = Node.create!(name: 'other root')
1031
- Node.create!(name: 'other child', parent_id: other.id)
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)
1032
1034
  forest = Node.with_recursive(
1033
1035
  tree: [
1034
1036
  Node.where { :parent_id.null? }.
@@ -1046,10 +1048,10 @@ class TestBlockSyntax < Minitest::Test
1046
1048
  # ON clause is a block rather than the string join Rails' own docs use.
1047
1049
  def test_recursive_cte
1048
1050
  Node.delete_all
1049
- root = Node.create!(name: 'root')
1050
- child = Node.create!(name: 'child', parent_id: root.id)
1051
- Node.create!(name: 'grandchild', parent_id: child.id)
1052
- Node.create!(name: 'unrelated', parent_id: nil)
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)
1053
1055
  descendants = Node.with_recursive(
1054
1056
  tree: [
1055
1057
  Node.where { :id == root.id },
@@ -1061,12 +1063,12 @@ class TestBlockSyntax < Minitest::Test
1061
1063
 
1062
1064
  def test_cte_joined_by_name
1063
1065
  Node.delete_all
1064
- root = Node.create!(name: 'root')
1065
- Node.create!(name: 'child', parent_id: root.id)
1066
- Node.create!(name: 'orphan', parent_id: nil)
1066
+ root = Node.create!(name: "root")
1067
+ Node.create!(name: "child", parent_id: root.id)
1068
+ Node.create!(name: "orphan", parent_id: nil)
1067
1069
  q = Node.with(roots: Node.where { :parent_id.null? }).
1068
1070
  joins(:roots) { :roots[:id] == :nodes[:parent_id] }
1069
- assert_equal(['child'], q.pluck(:name))
1071
+ assert_equal(["child"], q.pluck(:name))
1070
1072
  end
1071
1073
 
1072
1074
  def test_select_sum
@@ -1200,7 +1202,7 @@ class TestBlockSyntax < Minitest::Test
1200
1202
  # exactly.
1201
1203
  def test_fn
1202
1204
  assert_sql(/SELECT date_trunc\('day', "users"."name"\)/,
1203
- User.select { fn(:date_trunc, 'day', :name) }.to_sql)
1205
+ User.select { fn(:date_trunc, "day", :name) }.to_sql)
1204
1206
  end
1205
1207
 
1206
1208
  def test_fn_is_comparable
@@ -1210,13 +1212,13 @@ class TestBlockSyntax < Minitest::Test
1210
1212
 
1211
1213
  def test_fn_alias
1212
1214
  assert_sql(/SELECT date_trunc\('day', "users"."name"\) AS "d"/,
1213
- User.select { fn(:date_trunc, 'day', :name).as(:d) }.to_sql)
1215
+ User.select { fn(:date_trunc, "day", :name).as(:d) }.to_sql)
1214
1216
  end
1215
1217
 
1216
1218
  # Aliases and function names are written into the SQL where a value would
1217
1219
  # have been quoted, so a name that is not plain is refused rather than
1218
1220
  # given the chance to close the identifier and carry on.
1219
- INJECTION = %q{a" AS x, (SELECT 1) AS "y}
1221
+ INJECTION = 'a" AS x, (SELECT 1) AS "y'
1220
1222
 
1221
1223
  # An alias that is not a plain name is quoted by the adapter rather than
1222
1224
  # refused, so an injected one becomes an alias with a strange name and
@@ -1224,10 +1226,10 @@ class TestBlockSyntax < Minitest::Test
1224
1226
  # that the payload arrived as the name of the column it labelled.
1225
1227
  def test_an_injected_alias_is_quoted_rather_than_refused
1226
1228
  User.delete_all
1227
- User.create!(name: 'alice')
1229
+ User.create!(name: "alice")
1228
1230
  payload = 'a" FROM users; --'
1229
1231
  row = User.select { :name.as(payload.to_sym) }.first
1230
- assert_equal('alice', row[payload])
1232
+ assert_equal("alice", row[payload])
1231
1233
  assert_equal(1, User.count)
1232
1234
  end
1233
1235
 
@@ -1243,8 +1245,8 @@ class TestBlockSyntax < Minitest::Test
1243
1245
  def test_an_alias_keeps_the_name_as_written
1244
1246
  assert_sql(/AS "postCount"/, User.select { :name.as(:postCount) }.to_sql)
1245
1247
  User.delete_all
1246
- User.create!(name: 'alice')
1247
- assert_equal('alice', User.select { :name.as(:postCount) }.first['postCount'])
1248
+ User.create!(name: "alice")
1249
+ assert_equal("alice", User.select { :name.as(:postCount) }.first["postCount"])
1248
1250
  end
1249
1251
 
1250
1252
  def test_quote_false_asks_for_the_name_as_it_is
@@ -1264,6 +1266,55 @@ class TestBlockSyntax < Minitest::Test
1264
1266
  assert_raises(ArgumentError) { User.select { fn(INJECTION.to_sym, :name) } }
1265
1267
  end
1266
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
+
1267
1318
  def test_plain_names_are_still_accepted
1268
1319
  assert_sql(/AS "post_count"/, User.select { :name.as(:post_count) }.to_sql)
1269
1320
  assert_sql(/AS "名前"/, User.select { :name.as(:名前) }.to_sql)
@@ -1277,8 +1328,8 @@ class TestBlockSyntax < Minitest::Test
1277
1328
  # rather than opening the condition up.
1278
1329
  def test_values_are_quoted
1279
1330
  User.delete_all
1280
- User.create!(name: 'alice')
1281
- User.create!(name: 'bob')
1331
+ User.create!(name: "alice")
1332
+ User.create!(name: "bob")
1282
1333
  payload = "x' OR 1=1 --"
1283
1334
  assert_empty(User.where { :name == payload }.pluck(:name))
1284
1335
  assert_empty(User.where { :name.like?(payload) }.pluck(:name))
@@ -1296,7 +1347,7 @@ class TestBlockSyntax < Minitest::Test
1296
1347
 
1297
1348
  def test_scalar_functions_shared_by_every_adapter
1298
1349
  assert_sql(/SELECT CONCAT\(UPPER\("users"."name"\), 'x'\)/,
1299
- User.select { concat(upper(:name), 'x') }.to_sql)
1350
+ User.select { concat(upper(:name), "x") }.to_sql)
1300
1351
  assert_sql(/WHERE MOD\("users"."age", 7\) = 0/,
1301
1352
  User.where { mod(:age, 7) == 0 }.to_sql)
1302
1353
  end
@@ -1304,7 +1355,7 @@ class TestBlockSyntax < Minitest::Test
1304
1355
  # SQLite has no CHAR_LENGTH, GREATEST or LEAST, but LENGTH, MAX and MIN
1305
1356
  # mean the same thing there.
1306
1357
  def test_scalar_functions_spelled_differently_on_sqlite
1307
- expected = ADAPTER == 'sqlite3' ? %w[LENGTH MAX MIN] : %w[CHAR_LENGTH GREATEST LEAST]
1358
+ expected = ADAPTER == "sqlite3" ? %w[LENGTH MAX MIN] : %w[CHAR_LENGTH GREATEST LEAST]
1308
1359
  assert_sql(/SELECT #{expected[0]}\("users"."name"\)/,
1309
1360
  User.select { char_length(:name) }.to_sql)
1310
1361
  assert_sql(/SELECT #{expected[1]}\("users"."age", 18\)/,
@@ -1315,8 +1366,8 @@ class TestBlockSyntax < Minitest::Test
1315
1366
 
1316
1367
  def test_scalar_functions_run
1317
1368
  User.delete_all
1318
- User.create!(name: 'alice', age: 60)
1319
- assert_equal(['ALICE-x'], User.select { concat(upper(:name), '-x').as(:v) }.map(&:v))
1369
+ User.create!(name: "alice", age: 60)
1370
+ assert_equal(["ALICE-x"], User.select { concat(upper(:name), "-x").as(:v) }.map(&:v))
1320
1371
  assert_equal([5], User.select { char_length(:name).as(:v) }.map(&:v))
1321
1372
  assert_equal([60], User.select { greatest(:age, 18).as(:v) }.map(&:v))
1322
1373
  end
@@ -1324,18 +1375,18 @@ class TestBlockSyntax < Minitest::Test
1324
1375
  # rand takes the name back from Kernel#rand, which would otherwise answer
1325
1376
  # inside the block and never reach the database.
1326
1377
  def test_rand
1327
- expected = ADAPTER == 'mysql2' ? 'RAND' : 'RANDOM'
1378
+ expected = ADAPTER == "mysql2" ? "RAND" : "RANDOM"
1328
1379
  assert_sql(/ORDER BY #{expected}\(\)/, User.order { rand }.to_sql)
1329
1380
  end
1330
1381
 
1331
1382
  # Where an adapter has no equivalent, the block raises instead of leaving
1332
1383
  # the database to reject the SQL.
1333
1384
  def test_unsupported_function_raises
1334
- if ADAPTER == 'postgresql'
1385
+ if ADAPTER == "postgresql"
1335
1386
  assert_sql(/SELECT DATE_TRUNC\('day', "users"."name"\)/,
1336
- User.select { date_trunc('day', :name) }.to_sql)
1387
+ User.select { date_trunc("day", :name) }.to_sql)
1337
1388
  else
1338
- e = assert_raises(NotImplementedError) { User.select { date_trunc('day', :name) } }
1389
+ e = assert_raises(NotImplementedError) { User.select { date_trunc("day", :name) } }
1339
1390
  assert_match(/date_trunc/, e.message)
1340
1391
  end
1341
1392
  end
@@ -1344,12 +1395,12 @@ class TestBlockSyntax < Minitest::Test
1344
1395
  # and reads a printf template as the number zero rather than complaining,
1345
1396
  # so the name carries the printf one and MySQL raises.
1346
1397
  def test_format_is_printf_and_unsupported_on_mysql
1347
- if ADAPTER == 'mysql2'
1348
- assert_raises(NotImplementedError) { User.select { format('%s!', :name) } }
1398
+ if ADAPTER == "mysql2"
1399
+ assert_raises(NotImplementedError) { User.select { format("%s!", :name) } }
1349
1400
  else
1350
1401
  User.delete_all
1351
- User.create!(name: 'alice')
1352
- assert_equal(['alice!'], User.select { format('%s!', :name).as(:v) }.map(&:v))
1402
+ User.create!(name: "alice")
1403
+ assert_equal(["alice!"], User.select { format("%s!", :name).as(:v) }.map(&:v))
1353
1404
  end
1354
1405
  end
1355
1406
 
@@ -1360,7 +1411,7 @@ class TestBlockSyntax < Minitest::Test
1360
1411
  end
1361
1412
 
1362
1413
  def test_now_is_unsupported_on_sqlite
1363
- if ADAPTER == 'sqlite3'
1414
+ if ADAPTER == "sqlite3"
1364
1415
  assert_raises(NotImplementedError) { User.select { now } }
1365
1416
  else
1366
1417
  assert_sql(/SELECT NOW\(\)/, User.select { now }.to_sql)
@@ -1385,14 +1436,14 @@ class TestBlockSyntax < Minitest::Test
1385
1436
 
1386
1437
  def test_current_timestamp_runs
1387
1438
  User.delete_all
1388
- User.create!(name: 'alice')
1439
+ User.create!(name: "alice")
1389
1440
  refute_nil(User.select { current_timestamp.as(:v) }.sole.v)
1390
1441
  end
1391
1442
 
1392
1443
  # The one thing that does go into the parentheses is a precision, which
1393
1444
  # current_date never takes and SQLite never accepts.
1394
1445
  def test_datetime_value_function_with_precision
1395
- if ADAPTER == 'sqlite3'
1446
+ if ADAPTER == "sqlite3"
1396
1447
  e = assert_raises(NotImplementedError) { User.select { current_timestamp(3) } }
1397
1448
  assert_match(/precision/, e.message)
1398
1449
  else
@@ -1401,7 +1452,7 @@ class TestBlockSyntax < Minitest::Test
1401
1452
  assert_sql(/SELECT CURRENT_TIME\(0\) FROM/,
1402
1453
  User.select { current_time(0) }.to_sql)
1403
1454
  User.delete_all
1404
- User.create!(name: 'alice')
1455
+ User.create!(name: "alice")
1405
1456
  refute_nil(User.select { current_timestamp(0).as(:v) }.sole.v)
1406
1457
  end
1407
1458
  end
@@ -1419,7 +1470,7 @@ class TestBlockSyntax < Minitest::Test
1419
1470
  end
1420
1471
 
1421
1472
  def test_localtime_is_unsupported_on_sqlite
1422
- if ADAPTER == 'sqlite3'
1473
+ if ADAPTER == "sqlite3"
1423
1474
  assert_raises(NotImplementedError) { User.select { localtime } }
1424
1475
  assert_raises(NotImplementedError) { User.select { localtimestamp } }
1425
1476
  else
@@ -1440,7 +1491,7 @@ class TestBlockSyntax < Minitest::Test
1440
1491
 
1441
1492
  def test_math_functions_run
1442
1493
  User.delete_all
1443
- User.create!(name: 'alice', age: 60)
1494
+ User.create!(name: "alice", age: 60)
1444
1495
  assert_equal(1, User.select { sign(:age).as(:v) }.sole.v.to_i)
1445
1496
  assert_equal(60,
1446
1497
  User.select { round(degrees(radians(:age))).as(:v) }.sole.v.to_i)
@@ -1448,7 +1499,7 @@ class TestBlockSyntax < Minitest::Test
1448
1499
 
1449
1500
  # PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
1450
1501
  def test_log2_is_unsupported_on_postgresql
1451
- if ADAPTER == 'postgresql'
1502
+ if ADAPTER == "postgresql"
1452
1503
  assert_raises(NotImplementedError) { User.select { log2(:age) } }
1453
1504
  else
1454
1505
  assert_sql(/SELECT LOG2\("users"."age"\)/, User.select { log2(:age) }.to_sql)
@@ -1458,7 +1509,7 @@ class TestBlockSyntax < Minitest::Test
1458
1509
  # MySQL spells trunc TRUNCATE, and insists on the second argument the
1459
1510
  # others default to zero.
1460
1511
  def test_trunc
1461
- expected = ADAPTER == 'mysql2' ? 'TRUNCATE' : 'TRUNC'
1512
+ expected = ADAPTER == "mysql2" ? "TRUNCATE" : "TRUNC"
1462
1513
  assert_sql(/SELECT #{expected}\("users"."age", 0\)/,
1463
1514
  User.select { trunc(:age, 0) }.to_sql)
1464
1515
  end
@@ -1467,7 +1518,7 @@ class TestBlockSyntax < Minitest::Test
1467
1518
  # SQLite spells all of this as strftime formats, which no renaming
1468
1519
  # carries.
1469
1520
  def test_extract
1470
- if ADAPTER == 'sqlite3'
1521
+ if ADAPTER == "sqlite3"
1471
1522
  e = assert_raises(NotImplementedError) { User.select { extract(:year, :name) } }
1472
1523
  assert_match(/extract/, e.message)
1473
1524
  else
@@ -1485,11 +1536,11 @@ class TestBlockSyntax < Minitest::Test
1485
1536
  end
1486
1537
 
1487
1538
  def test_extract_runs
1488
- skip "#{ADAPTER} has no extract" if ADAPTER == 'sqlite3'
1539
+ skip "#{ADAPTER} has no extract" if ADAPTER == "sqlite3"
1489
1540
  User.delete_all
1490
- User.create!(name: 'alice')
1541
+ User.create!(name: "alice")
1491
1542
  assert_equal(2026,
1492
- User.select { extract(:year, cast('2026-01-05', :date)).as(:v) }.sole.v.to_i)
1543
+ User.select { extract(:year, cast("2026-01-05", :date)).as(:v) }.sole.v.to_i)
1493
1544
  end
1494
1545
 
1495
1546
  def test_cast
@@ -1499,9 +1550,9 @@ class TestBlockSyntax < Minitest::Test
1499
1550
 
1500
1551
  def test_cast_runs
1501
1552
  User.delete_all
1502
- User.create!(name: 'alice')
1553
+ User.create!(name: "alice")
1503
1554
  assert_equal(12.5,
1504
- User.select { cast('12.5', 'decimal(10,2)').as(:v) }.sole.v.to_f)
1555
+ User.select { cast("12.5", "decimal(10,2)").as(:v) }.sole.v.to_f)
1505
1556
  end
1506
1557
 
1507
1558
  # The type is written into the SQL as given, so it has to look like one:
@@ -1509,12 +1560,12 @@ class TestBlockSyntax < Minitest::Test
1509
1560
  # spellings with a space in them pass too.
1510
1561
  def test_cast_type_names
1511
1562
  assert_sql(/AS double precision\)/,
1512
- User.select { cast(:age, 'double precision') }.to_sql)
1563
+ User.select { cast(:age, "double precision") }.to_sql)
1513
1564
  assert_sql(/AS decimal\(10,2\)\)/,
1514
- User.select { cast(:age, 'decimal(10,2)') }.to_sql)
1565
+ User.select { cast(:age, "decimal(10,2)") }.to_sql)
1515
1566
  assert_raises(ArgumentError) { User.select { cast(:age, INJECTION.to_sym) } }
1516
1567
  assert_raises(ArgumentError) do
1517
- User.select { cast(:age, 'integer); DROP TABLE users --') }
1568
+ User.select { cast(:age, "integer); DROP TABLE users --") }
1518
1569
  end
1519
1570
  end
1520
1571
 
@@ -1552,6 +1603,72 @@ class TestBlockSyntax < Minitest::Test
1552
1603
  User.select { :users[:age] - 1 }.to_sql)
1553
1604
  end
1554
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
+
1555
1672
  def test_bitwise_and_or
1556
1673
  assert_sql(/SELECT \("users"."flags" & 4\) AS "masked"/,
1557
1674
  User.select { (:flags & 4).as(:masked) }.to_sql)
@@ -1582,8 +1699,8 @@ class TestBlockSyntax < Minitest::Test
1582
1699
  def test_bitwise_xor_is_spelled_per_adapter
1583
1700
  sql = User.select { :flags ^ 10 }.to_sql
1584
1701
  case ADAPTER
1585
- when 'postgresql' then assert_sql(/SELECT \("users"."flags" # 10\)/, sql)
1586
- when 'mysql2' then assert_sql(/SELECT \("users"."flags" \^ 10\)/, sql)
1702
+ when "postgresql" then assert_sql(/SELECT \("users"."flags" # 10\)/, sql)
1703
+ when "mysql2" then assert_sql(/SELECT \("users"."flags" \^ 10\)/, sql)
1587
1704
  else assert_sql(
1588
1705
  /SELECT \(\("users"."flags" \| 10\) - \("users"."flags" & 10\)\)/, sql)
1589
1706
  end
@@ -1592,7 +1709,7 @@ class TestBlockSyntax < Minitest::Test
1592
1709
  # Whatever the spelling, the answers agree.
1593
1710
  def test_bitwise_execution
1594
1711
  User.delete_all
1595
- User.create!(name: 'a', flags: 12)
1712
+ User.create!(name: "a", flags: 12)
1596
1713
  assert_equal(8, User.select { (:flags & 10).as(:v) }.take.v.to_i)
1597
1714
  assert_equal(14, User.select { (:flags | 10).as(:v) }.take.v.to_i)
1598
1715
  assert_equal(6, User.select { (:flags ^ 10).as(:v) }.take.v.to_i)
@@ -1620,13 +1737,13 @@ class TestBlockSyntax < Minitest::Test
1620
1737
 
1621
1738
  def test_conditions_still_and_with_the_same_operators
1622
1739
  assert_sql(/WHERE "users"."age" = 1 AND "users"."name" = 'a'/,
1623
- User.where { (:age == 1) & (:name == 'a') }.to_sql)
1740
+ User.where { (:age == 1) & (:name == "a") }.to_sql)
1624
1741
  end
1625
1742
 
1626
1743
  def test_bit_aggregates
1627
1744
  skip_without_bit_aggregates
1628
1745
  User.delete_all
1629
- User.create!([{name: 'a', flags: 12}, {name: 'b', flags: 10}, {name: 'c', flags: 3}])
1746
+ User.create!([{ name: "a", flags: 12 }, { name: "b", flags: 10 }, { name: "c", flags: 3 }])
1630
1747
  assert_equal(0, User.select { bit_and(:flags).as(:v) }.take.v.to_i)
1631
1748
  assert_equal(15, User.select { bit_or(:flags).as(:v) }.take.v.to_i)
1632
1749
  assert_equal(5, User.select { bit_xor(:flags).as(:v) }.take.v.to_i)
@@ -1635,20 +1752,20 @@ class TestBlockSyntax < Minitest::Test
1635
1752
  # PostgreSQL counts the bits of a bit string rather than of a number, so the
1636
1753
  # argument is cast there; bit(64) is what makes a negative answer alike.
1637
1754
  def test_bit_count
1638
- if ADAPTER == 'sqlite3'
1755
+ if ADAPTER == "sqlite3"
1639
1756
  assert_raises(NotImplementedError) { User.select { bit_count(:flags) } }
1640
1757
  return
1641
1758
  end
1642
1759
  User.delete_all
1643
- User.create!(name: 'a', flags: 12)
1644
- User.create!(name: 'b', flags: -1)
1645
- assert_equal([2, 64], User.select { bit_count(:flags).as(:v) }.order(:name).map {|u| u.v.to_i })
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 })
1646
1763
  assert_sql(/BIT_COUNT\(CAST\("users"."flags" AS bit\(64\)\)\)/,
1647
- User.select { bit_count(:flags) }.to_sql) if ADAPTER == 'postgresql'
1764
+ User.select { bit_count(:flags) }.to_sql) if ADAPTER == "postgresql"
1648
1765
  end
1649
1766
 
1650
1767
  def test_bit_aggregates_are_unsupported_on_sqlite
1651
- if ADAPTER == 'sqlite3'
1768
+ if ADAPTER == "sqlite3"
1652
1769
  e = assert_raises(NotImplementedError) { User.select { bit_or(:flags) } }
1653
1770
  assert_match(/bit_or/, e.message)
1654
1771
  else
@@ -1658,17 +1775,17 @@ class TestBlockSyntax < Minitest::Test
1658
1775
 
1659
1776
  def test_coalesce_function_with_literal
1660
1777
  assert_sql(/SELECT COALESCE\("users"."name", 'unknown'\)/,
1661
- User.select { coalesce(:name, 'unknown') }.to_sql)
1778
+ User.select { coalesce(:name, "unknown") }.to_sql)
1662
1779
  end
1663
1780
 
1664
1781
  def test_function_comparison
1665
1782
  assert_sql(/WHERE UPPER\("users"."name"\) = 'MATZ'/,
1666
- User.where { upper(:name) == 'MATZ' }.to_sql)
1783
+ User.where { upper(:name) == "MATZ" }.to_sql)
1667
1784
  end
1668
1785
 
1669
1786
  def test_function_like
1670
1787
  assert_sql(/WHERE UPPER\("users"."name"\) LIKE 'MA%'/,
1671
- User.where { upper(:name).like?('MA%') }.to_sql)
1788
+ User.where { upper(:name).like?("MA%") }.to_sql)
1672
1789
  end
1673
1790
 
1674
1791
  def test_function_in
@@ -1683,7 +1800,7 @@ class TestBlockSyntax < Minitest::Test
1683
1800
 
1684
1801
  def test_nested_function
1685
1802
  assert_sql(/SELECT UPPER\(COALESCE\("users"."name", 'x'\)\)/,
1686
- User.select { upper(coalesce(:name, 'x')) }.to_sql)
1803
+ User.select { upper(coalesce(:name, "x")) }.to_sql)
1687
1804
  end
1688
1805
 
1689
1806
  def test_function_qualified_column_arg
@@ -1761,9 +1878,9 @@ class TestBlockSyntax < Minitest::Test
1761
1878
  # The order itself is portable even where the syntax is not.
1762
1879
  def test_order_nulls_execution
1763
1880
  User.delete_all
1764
- User.create!(name: 'null_age', age: nil)
1765
- User.create!(name: 'young', age: 20)
1766
- User.create!(name: 'old', age: 60)
1881
+ User.create!(name: "null_age", age: nil)
1882
+ User.create!(name: "young", age: 20)
1883
+ User.create!(name: "old", age: 60)
1767
1884
  assert_equal(%w[null_age young old],
1768
1885
  User.order { :age.asc.nulls_first }.pluck(:name))
1769
1886
  assert_equal(%w[young old null_age],
@@ -1800,36 +1917,36 @@ class TestBlockSyntax < Minitest::Test
1800
1917
  # the column it names, which is what lets the new value be built from the old.
1801
1918
  def test_update_all_from_the_column
1802
1919
  Tally.delete_all
1803
- Tally.create!(page: '/a', hits: 1)
1804
- Tally.create!(page: '/b', hits: 2)
1920
+ Tally.create!(page: "/a", hits: 1)
1921
+ Tally.create!(page: "/b", hits: 2)
1805
1922
  Tally.update_all { { hits: :hits + 1 } }
1806
1923
  assert_equal([2, 3], Tally.order(:page).pluck(:hits))
1807
1924
  end
1808
1925
 
1809
1926
  def test_update_all_takes_any_expression
1810
1927
  Tally.delete_all
1811
- Tally.create!(page: '/a', hits: 5)
1928
+ Tally.create!(page: "/a", hits: 5)
1812
1929
  Tally.update_all { { hits: case_when { :hits > 4 }.then(0).else(:hits), page: upper(:page) } }
1813
- assert_equal([['/A', 0]], Tally.pluck(:page, :hits))
1930
+ assert_equal([["/A", 0]], Tally.pluck(:page, :hits))
1814
1931
  end
1815
1932
 
1816
1933
  def test_update_all_within_a_scope
1817
1934
  Tally.delete_all
1818
- Tally.create!(page: '/a', hits: 1)
1819
- Tally.create!(page: '/b', hits: 1)
1820
- Tally.where { :page == '/a' }.update_all { { hits: 9 } }
1935
+ Tally.create!(page: "/a", hits: 1)
1936
+ Tally.create!(page: "/b", hits: 1)
1937
+ Tally.where { :page == "/a" }.update_all { { hits: 9 } }
1821
1938
  assert_equal([9, 1], Tally.order(:page).pluck(:hits))
1822
1939
  end
1823
1940
 
1824
1941
  def test_update_all_without_a_block_is_unchanged
1825
1942
  Tally.delete_all
1826
- Tally.create!(page: '/a', hits: 1)
1943
+ Tally.create!(page: "/a", hits: 1)
1827
1944
  Tally.update_all(hits: 4)
1828
1945
  assert_equal([4], Tally.pluck(:hits))
1829
1946
  end
1830
1947
 
1831
1948
  def test_update_all_takes_updates_or_a_block
1832
- assert_raises(ArgumentError) { Tally.update_all({hits: 1}) { { hits: 2 } } }
1949
+ assert_raises(ArgumentError) { Tally.update_all({ hits: 1 }) { { hits: 2 } } }
1833
1950
  e = assert_raises(ArgumentError) { Tally.update_all { :hits + 1 } }
1834
1951
  assert_match(/hash of column/, e.message)
1835
1952
  end
@@ -1838,8 +1955,8 @@ class TestBlockSyntax < Minitest::Test
1838
1955
  # some. `excluded` is the row that could not be inserted.
1839
1956
  def test_upsert_all_adds_to_what_is_there
1840
1957
  Tally.delete_all
1841
- Tally.upsert_all([{page: '/a', hits: 1}], **upsert_target)
1842
- Tally.upsert_all([{page: '/a', hits: 10}], **upsert_target) {
1958
+ Tally.upsert_all([{ page: "/a", hits: 1 }], **upsert_target)
1959
+ Tally.upsert_all([{ page: "/a", hits: 10 }], **upsert_target) {
1843
1960
  { hits: :hits + excluded(:hits) }
1844
1961
  }
1845
1962
  assert_equal([11], Tally.pluck(:hits))
@@ -1847,7 +1964,7 @@ class TestBlockSyntax < Minitest::Test
1847
1964
 
1848
1965
  def test_upsert_all_inserts_when_there_is_no_conflict
1849
1966
  Tally.delete_all
1850
- Tally.upsert_all([{page: '/new', hits: 3}], **upsert_target) {
1967
+ Tally.upsert_all([{ page: "/new", hits: 3 }], **upsert_target) {
1851
1968
  { hits: :hits + excluded(:hits) }
1852
1969
  }
1853
1970
  assert_equal([3], Tally.pluck(:hits))
@@ -1855,8 +1972,8 @@ class TestBlockSyntax < Minitest::Test
1855
1972
 
1856
1973
  def test_upsert_all_takes_any_expression
1857
1974
  Tally.delete_all
1858
- Tally.upsert_all([{page: '/a', hits: 7}], **upsert_target)
1859
- Tally.upsert_all([{page: '/a', hits: 2}], **upsert_target) {
1975
+ Tally.upsert_all([{ page: "/a", hits: 7 }], **upsert_target)
1976
+ Tally.upsert_all([{ page: "/a", hits: 2 }], **upsert_target) {
1860
1977
  { hits: greatest(:hits, excluded(:hits)) }
1861
1978
  }
1862
1979
  assert_equal([7], Tally.pluck(:hits))
@@ -1864,18 +1981,18 @@ class TestBlockSyntax < Minitest::Test
1864
1981
 
1865
1982
  def test_upsert_all_without_a_block_is_unchanged
1866
1983
  Tally.delete_all
1867
- Tally.upsert_all([{page: '/a', hits: 1}], **upsert_target)
1868
- Tally.upsert_all([{page: '/a', hits: 6}], **upsert_target)
1984
+ Tally.upsert_all([{ page: "/a", hits: 1 }], **upsert_target)
1985
+ Tally.upsert_all([{ page: "/a", hits: 6 }], **upsert_target)
1869
1986
  assert_equal([6], Tally.pluck(:hits))
1870
1987
  end
1871
1988
 
1872
1989
  def test_upsert_all_takes_on_duplicate_or_a_block
1873
1990
  assert_raises(ArgumentError) do
1874
- Tally.upsert_all([{page: '/a', hits: 1}],
1875
- on_duplicate: Arel.sql('hits = 1'), **upsert_target) { { hits: 2 } }
1991
+ Tally.upsert_all([{ page: "/a", hits: 1 }],
1992
+ on_duplicate: Arel.sql("hits = 1"), **upsert_target) { { hits: 2 } }
1876
1993
  end
1877
1994
  e = assert_raises(ArgumentError) do
1878
- Tally.upsert_all([{page: '/a', hits: 1}], **upsert_target) { {} }
1995
+ Tally.upsert_all([{ page: "/a", hits: 1 }], **upsert_target) { {} }
1879
1996
  end
1880
1997
  assert_match(/at least one column/, e.message)
1881
1998
  end
@@ -1885,10 +2002,10 @@ class TestBlockSyntax < Minitest::Test
1885
2002
  # back rather than the SQL.
1886
2003
  def seed_docs
1887
2004
  Doc.delete_all
1888
- Doc.create!(name: 'one',
1889
- meta: json_document({ 'a' => { 'b' => 'deep' }, 'n' => 5,
1890
- 'tags' => %w[x y], 'odd key' => 1 }))
1891
- Doc.create!(name: 'two', meta: json_document({ 'n' => 9 }))
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 }))
1892
2009
  end
1893
2010
 
1894
2011
  def test_dig_text_a_key
@@ -1898,20 +2015,20 @@ class TestBlockSyntax < Minitest::Test
1898
2015
 
1899
2016
  def test_dig_text_a_path
1900
2017
  seed_docs
1901
- assert_equal(['deep', nil],
2018
+ assert_equal(["deep", nil],
1902
2019
  Doc.order(:name).select { :meta.dig_text(:a, :b).as(:v) }.map(&:v))
1903
2020
  end
1904
2021
 
1905
2022
  def test_dig_text_an_array_index
1906
2023
  seed_docs
1907
- assert_equal(['x', nil],
2024
+ assert_equal(["x", nil],
1908
2025
  Doc.order(:name).select { :meta.dig_text(:tags, 0).as(:v) }.map(&:v))
1909
2026
  end
1910
2027
 
1911
2028
  # A key that is not a plain name travels as itself rather than being refused.
1912
2029
  def test_dig_text_a_key_that_needs_quoting
1913
2030
  seed_docs
1914
- assert_equal(['1', nil],
2031
+ assert_equal(["1", nil],
1915
2032
  Doc.order(:name).select { :meta.dig_text(:'odd key').as(:v) }.map(&:v))
1916
2033
  end
1917
2034
 
@@ -1919,14 +2036,14 @@ class TestBlockSyntax < Minitest::Test
1919
2036
  # value with its type -- so a number is compared through a cast.
1920
2037
  def test_dig_text_is_text_everywhere
1921
2038
  seed_docs
1922
- assert_equal(['one'], Doc.where { :meta.dig_text(:n) == '5' }.pluck(:name))
2039
+ assert_equal(["one"], Doc.where { :meta.dig_text(:n) == "5" }.pluck(:name))
1923
2040
  type = integer_type
1924
- assert_equal(['two'], Doc.where { cast(:meta.dig_text(:n), type) > 6 }.pluck(:name))
2041
+ assert_equal(["two"], Doc.where { cast(:meta.dig_text(:n), type) > 6 }.pluck(:name))
1925
2042
  end
1926
2043
 
1927
2044
  def test_dig_keeps_the_json
1928
2045
  seed_docs
1929
- value = Doc.where { :name == 'one' }.select { :meta.dig(:tags).as(:v) }.first.v
2046
+ value = Doc.where { :name == "one" }.select { :meta.dig(:tags).as(:v) }.first.v
1930
2047
  assert_equal(%w[x y], value.is_a?(String) ? JSON.parse(value) : value)
1931
2048
  end
1932
2049
 
@@ -1948,40 +2065,74 @@ class TestBlockSyntax < Minitest::Test
1948
2065
  # rather than wrote as a literal, since that is nobody's guess to make.
1949
2066
  def test_dig_text_compares_with_text_and_with_expressions
1950
2067
  seed_docs
1951
- assert_equal(['one'], Doc.where { :meta.dig_text(:n) == '5' }.pluck(:name))
2068
+ assert_equal(["one"], Doc.where { :meta.dig_text(:n) == "5" }.pluck(:name))
1952
2069
  assert_equal([], Doc.where { :meta.dig_text(:n) == :name }.pluck(:name))
1953
- assert_equal(['one'], Doc.where { :meta.dig_text(:n) == upper('5') }.pluck(:name))
2070
+ assert_equal(["one"], Doc.where { :meta.dig_text(:n) == upper("5") }.pluck(:name))
1954
2071
  type = integer_type
1955
- assert_equal(['one'], Doc.where { cast(:meta.dig_text(:n), type) == 5 }.pluck(:name))
2072
+ assert_equal(["one"], Doc.where { cast(:meta.dig_text(:n), type) == 5 }.pluck(:name))
1956
2073
  end
1957
2074
 
1958
- # The JSON for a string carries its quotes, so the same comparison is
1959
- # refused the other way about: false on SQLite, an error on PostgreSQL and
1960
- # true on MySQL.
1961
- def test_dig_refuses_a_comparison_with_a_ruby_value
1962
- e = assert_raises(ArgumentError) { Doc.where { :meta.dig(:a) == 'deep' } }
1963
- assert_match(/dig_text gives the value/, e.message)
1964
- assert_raises(ArgumentError) { Doc.where { :meta.dig(:n) == 5 } }
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) } }
1965
2116
  end
1966
2117
 
1967
2118
  # What dig gives is a document, so the JSON operations read it: the
1968
2119
  # same question asked of a part rather than of the whole.
1969
2120
  def test_the_json_operations_read_what_dig_kept
1970
2121
  seed_docs
1971
- assert_equal(['one'], Doc.where { :meta.dig(:a).key?(:b) }.pluck(:name))
2122
+ assert_equal(["one"], Doc.where { :meta.dig(:a).key?(:b) }.pluck(:name))
1972
2123
  assert_equal(%w[one two], Doc.where { :meta.dig(:n).not_null? }.order(:name).pluck(:name))
1973
- assert_equal(['one'],
1974
- Doc.where { :meta.dig(:a).dig_text(:b) == 'deep' }.pluck(:name))
1975
- value = Doc.where { :name == 'one' }.
1976
- select { :meta.dig(:a).bury(:b, 'x').as(:v) }.first.v
1977
- assert_equal('x', (value.is_a?(String) ? JSON.parse(value) : value)['b'])
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"])
1978
2129
  end
1979
2130
 
1980
2131
  def test_containment_reads_what_dig_kept
1981
2132
  skip_without_json_containment
1982
2133
  seed_docs
1983
- assert_equal(['one'], Doc.where { :meta.dig(:tags).contains?(['x']) }.pluck(:name))
1984
- assert_equal([], Doc.where { :meta.dig(:tags).contains?(['z']) }.pluck(:name))
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))
1985
2136
  end
1986
2137
 
1987
2138
  # Reading text back as a document is where the adapters part company:
@@ -1993,30 +2144,346 @@ class TestBlockSyntax < Minitest::Test
1993
2144
  assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:a).contains?(b: 1) } }
1994
2145
  assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).dig_text(:b) } }
1995
2146
  assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).dig(:b) } }
1996
- assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).bury(:b, 'x') } }
2147
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).bury(:b, "x") } }
1997
2148
  assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).except(:b) } }
1998
2149
  end
1999
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
+
2000
2460
  def test_dig_text_from_a_qualified_column
2001
2461
  seed_docs
2002
- assert_equal(['one'], Doc.where { :docs[:meta].dig_text(:a, :b) == 'deep' }.pluck(:name))
2462
+ assert_equal(["one"], Doc.where { :docs[:meta].dig_text(:a, :b) == "deep" }.pluck(:name))
2003
2463
  end
2004
2464
 
2005
2465
  def test_key
2006
2466
  seed_docs
2007
- assert_equal(['one'], Doc.where { :meta.key?(:tags) }.pluck(:name))
2467
+ assert_equal(["one"], Doc.where { :meta.key?(:tags) }.pluck(:name))
2008
2468
  assert_equal(%w[one two], Doc.where { :meta.key?(:n) }.order(:name).pluck(:name))
2009
2469
  end
2010
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
+
2011
2478
  def test_contains
2012
2479
  skip_without_json_containment
2013
2480
  seed_docs
2014
- assert_equal(['one'], Doc.where { :meta.contains?(n: 5) }.pluck(:name))
2481
+ assert_equal(["one"], Doc.where { :meta.contains?(n: 5) }.pluck(:name))
2015
2482
  assert_equal([], Doc.where { :meta.contains?(n: 1) }.pluck(:name))
2016
2483
  end
2017
2484
 
2018
2485
  def test_contains_says_where_it_cannot_go
2019
- skip "#{ADAPTER} has JSON containment" unless ADAPTER == 'sqlite3'
2486
+ skip "#{ADAPTER} has JSON containment" unless ADAPTER == "sqlite3"
2020
2487
  assert_raises(NotImplementedError) { Doc.where { :meta.contains?(n: 5) }.to_sql }
2021
2488
  end
2022
2489
 
@@ -2031,9 +2498,9 @@ class TestBlockSyntax < Minitest::Test
2031
2498
  # comes back rather than the SQL.
2032
2499
  def seed_for_filter
2033
2500
  User.delete_all
2034
- User.create!(name: 'a', age: 10)
2035
- User.create!(name: 'a', age: 20)
2036
- User.create!(name: 'b', age: 100)
2501
+ User.create!(name: "a", age: 10)
2502
+ User.create!(name: "a", age: 20)
2503
+ User.create!(name: "b", age: 100)
2037
2504
  end
2038
2505
 
2039
2506
  def aggregate(&block)
@@ -2062,7 +2529,7 @@ class TestBlockSyntax < Minitest::Test
2062
2529
  end
2063
2530
 
2064
2531
  def test_filter_is_a_clause_where_there_is_one
2065
- skip "#{ADAPTER} has no FILTER" if ADAPTER == 'mysql2'
2532
+ skip "#{ADAPTER} has no FILTER" if ADAPTER == "mysql2"
2066
2533
  assert_sql(/COUNT\(\*\) FILTER \(WHERE "users"."age" < 50\)/,
2067
2534
  User.select { count(:*).filter { :age < 50 } }.to_sql)
2068
2535
  end
@@ -2070,7 +2537,7 @@ class TestBlockSyntax < Minitest::Test
2070
2537
  # Where there is not, the same rows are reached through a case: an aggregate
2071
2538
  # passes over a NULL, so a row the condition misses is a row it does not see.
2072
2539
  def test_filter_becomes_a_case_where_there_is_no_clause
2073
- skip "#{ADAPTER} has FILTER" unless ADAPTER == 'mysql2'
2540
+ skip "#{ADAPTER} has FILTER" unless ADAPTER == "mysql2"
2074
2541
  assert_sql(/COUNT\(CASE WHEN "users"."age" < 50 THEN 1 END\)/,
2075
2542
  User.select { count(:*).filter { :age < 50 } }.to_sql)
2076
2543
  assert_sql(/SUM\(CASE WHEN "users"."age" < 50 THEN "users"."age" END\)/,
@@ -2086,9 +2553,9 @@ class TestBlockSyntax < Minitest::Test
2086
2553
  # DISTINCT ON keeps the first row of each group the order brings up.
2087
2554
  def seed_for_distinct_on
2088
2555
  Author.delete_all
2089
- Author.create!(name: 'a')
2090
- Author.create!(name: 'a')
2091
- Author.create!(name: 'b')
2556
+ Author.create!(name: "a")
2557
+ Author.create!(name: "a")
2558
+ Author.create!(name: "b")
2092
2559
  end
2093
2560
 
2094
2561
  def test_distinct_on
@@ -2119,7 +2586,7 @@ class TestBlockSyntax < Minitest::Test
2119
2586
  # Arel carries the node and refuses to write it elsewhere, as it does a
2120
2587
  # regexp, so the gem has nothing of its own to say.
2121
2588
  def test_distinct_on_says_where_it_cannot_go
2122
- skip "#{ADAPTER} has DISTINCT ON" if ADAPTER == 'postgresql'
2589
+ skip "#{ADAPTER} has DISTINCT ON" if ADAPTER == "postgresql"
2123
2590
  assert_raises(NotImplementedError) { Author.distinct_on { :name }.to_sql }
2124
2591
  end
2125
2592
 
@@ -2144,18 +2611,18 @@ class TestBlockSyntax < Minitest::Test
2144
2611
  def seed_for_lateral
2145
2612
  Author.delete_all
2146
2613
  Post.delete_all
2147
- author = Author.create!(name: 'writes')
2148
- Author.create!(name: 'does not')
2149
- Post.create!(author_id: author.id, title: 'a')
2150
- Post.create!(author_id: author.id, title: 'b')
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")
2151
2618
  end
2152
2619
 
2153
2620
  def test_lateral_join
2154
2621
  skip_without_lateral
2155
2622
  seed_for_lateral
2156
2623
  rows = Author.joins(top_post.lateral, as: :top).
2157
- select { [:name, :top[:title].as(:v)] }.map {|r| [r.name, r.v] }
2158
- assert_equal([['writes', 'b']], rows)
2624
+ select { [:name, :top[:title].as(:v)] }.map { |r| [r.name, r.v] }
2625
+ assert_equal([["writes", "b"]], rows)
2159
2626
  end
2160
2627
 
2161
2628
  # Left, so that a row with nothing to join to is kept.
@@ -2163,8 +2630,8 @@ class TestBlockSyntax < Minitest::Test
2163
2630
  skip_without_lateral
2164
2631
  seed_for_lateral
2165
2632
  rows = Author.left_outer_joins(top_post.lateral, as: :top).
2166
- select { [:name, :top[:title].as(:v)] }.order { :name }.map {|r| [r.name, r.v] }
2167
- assert_equal([['does not', nil], ['writes', 'b']], rows)
2633
+ select { [:name, :top[:title].as(:v)] }.order { :name }.map { |r| [r.name, r.v] }
2634
+ assert_equal([["does not", nil], ["writes", "b"]], rows)
2168
2635
  end
2169
2636
 
2170
2637
  # Without a block the join is ON TRUE; what the subquery may see is said
@@ -2173,7 +2640,7 @@ class TestBlockSyntax < Minitest::Test
2173
2640
  skip_without_lateral
2174
2641
  seed_for_lateral
2175
2642
  assert_equal(0, Author.joins(top_post.lateral, as: :top) {
2176
- :top[:title] == 'nothing'
2643
+ :top[:title] == "nothing"
2177
2644
  }.count)
2178
2645
  assert_sql(/ON TRUE/, Author.joins(top_post.lateral, as: :top).to_sql)
2179
2646
  end
@@ -2192,7 +2659,7 @@ class TestBlockSyntax < Minitest::Test
2192
2659
  end
2193
2660
 
2194
2661
  def test_lateral_join_says_where_it_cannot_go
2195
- skip 'this one has LATERAL' if ADAPTER == 'postgresql' || (ADAPTER == 'mysql2' && !mariadb?)
2662
+ skip "this one has LATERAL" if ADAPTER == "postgresql" || (ADAPTER == "mysql2" && !mariadb?)
2196
2663
  e = assert_raises(NotImplementedError) { Author.joins(top_post.lateral, as: :top) }
2197
2664
  assert_match(/lateral join has no equivalent/, e.message)
2198
2665
  end
@@ -2203,34 +2670,48 @@ class TestBlockSyntax < Minitest::Test
2203
2670
  def seed_for_grouping
2204
2671
  Post.delete_all
2205
2672
  Author.delete_all
2206
- a = Author.create!(name: 'a')
2207
- b = Author.create!(name: 'b')
2208
- Post.create!(author_id: a.id, title: 'x')
2209
- Post.create!(author_id: a.id, title: 'y')
2210
- Post.create!(author_id: b.id, title: 'x')
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")
2211
2678
  end
2212
2679
 
2213
2680
  def grouped(&block)
2214
2681
  Post.group(&block).select { [:author_id, :title, count(:*).as(:n)] }.
2215
- map {|r| [r.author_id, r.title, r.n.to_i] }.sort_by(&:to_s)
2682
+ map { |r| [r.author_id, r.title, r.n.to_i] }.sort_by(&:to_s)
2216
2683
  end
2217
2684
 
2218
2685
  def test_grouping_sets
2219
2686
  skip_without_grouping_sets
2220
2687
  seed_for_grouping
2221
2688
  rows = grouped { grouping_sets([:author_id], [:title], []) }
2222
- assert_equal(3, rows.count {|_, title, _| title.nil? }) # by author
2223
- assert_includes(rows, [nil, 'x', 2]) # by title
2689
+ assert_equal(3, rows.count { |_, title, _| title.nil? }) # by author
2690
+ assert_includes(rows, [nil, "x", 2]) # by title
2224
2691
  assert_includes(rows, [nil, nil, 3]) # the whole
2225
2692
  end
2226
2693
 
2227
2694
  def test_rollup
2228
- skip_without_grouping_sets
2695
+ skip_without_rollup
2229
2696
  seed_for_grouping
2230
2697
  rows = grouped { rollup(:author_id, :title) }
2698
+ assert_equal(6, rows.size) # by both (3), by author (2), the whole (1)
2231
2699
  assert_includes(rows, [nil, nil, 3])
2232
- assert_sql(/GROUP BY ROLLUP\( "posts"."author_id", "posts"."title" \)/,
2233
- Post.group { rollup(:author_id, :title) }.to_sql)
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)
2234
2715
  end
2235
2716
 
2236
2717
  def test_cube
@@ -2243,10 +2724,10 @@ class TestBlockSyntax < Minitest::Test
2243
2724
  end
2244
2725
 
2245
2726
  def test_grouping_sets_say_where_they_cannot_go
2246
- skip 'PostgreSQL has them' if ADAPTER == 'postgresql'
2727
+ skip "PostgreSQL has them" if ADAPTER == "postgresql"
2247
2728
  assert_raises(NotImplementedError) { Post.group { grouping_sets([:title]) } }
2248
- assert_raises(NotImplementedError) { Post.group { rollup(:title) } }
2249
2729
  assert_raises(NotImplementedError) { Post.group { cube(:title) } }
2730
+ assert_raises(NotImplementedError) { Post.group { rollup(:title) } } if ADAPTER == "sqlite3"
2250
2731
  end
2251
2732
 
2252
2733
  def test_grouping_sets_need_something_to_group_by
@@ -2258,75 +2739,96 @@ class TestBlockSyntax < Minitest::Test
2258
2739
  # being written anywhere, so update_all is what makes it stick.
2259
2740
  def buried(&block)
2260
2741
  seed_docs
2261
- Doc.where { :name == 'one' }.update_all(&block)
2262
- value = Doc.find_by(name: 'one').meta
2742
+ Doc.where { :name == "one" }.update_all(&block)
2743
+ value = Doc.find_by(name: "one").meta
2263
2744
  value.is_a?(String) ? JSON.parse(value) : value
2264
2745
  end
2265
2746
 
2266
2747
  def test_bury_a_nested_key
2267
- assert_equal('new', buried { { meta: :meta.bury(:a, :b, 'new') } }.dig('a', 'b'))
2748
+ assert_equal("new", buried { { meta: :meta.bury(:a, :b, "new") } }.dig("a", "b"))
2268
2749
  end
2269
2750
 
2270
2751
  def test_bury_a_key_that_is_not_there_yet
2271
- assert_equal(9, buried { { meta: :meta.bury(:fresh, 9) } }['fresh'])
2752
+ assert_equal(9, buried { { meta: :meta.bury(:fresh, 9) } }["fresh"])
2272
2753
  end
2273
2754
 
2274
2755
  # A whole document, which each adapter takes its own way round.
2275
2756
  def test_bury_an_object_and_an_array
2276
- assert_equal({ 'x' => 1 }, buried { { meta: :meta.bury(:obj, { 'x' => 1 }) } }['obj'])
2277
- assert_equal([1, 2], buried { { meta: :meta.bury(:arr, [1, 2]) } }['arr'])
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"])
2278
2771
  end
2279
2772
 
2280
2773
  def test_bury_an_array_index
2281
- assert_equal(%w[7 y], buried { { meta: :meta.bury(:tags, 0, '7') } }['tags'])
2774
+ assert_equal(%w[7 y], buried { { meta: :meta.bury(:tags, 0, "7") } }["tags"])
2282
2775
  end
2283
2776
 
2284
- # The value can be read out of the document it is going into.
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.
2285
2779
  def test_bury_an_expression
2286
- assert_equal('5', buried { { meta: :meta.bury(:copy, :meta.dig_text(:n)) } }['copy'].to_s)
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"])
2287
2782
  end
2288
2783
 
2289
2784
  # It is an expression, so it does not have to be written anywhere.
2290
2785
  def test_bury_in_a_select
2291
2786
  seed_docs
2292
- value = Doc.where { :name == 'one' }.select { :meta.bury(:a, :b, 'x').as(:v) }.first.v
2293
- assert_equal('x', (value.is_a?(String) ? JSON.parse(value) : value).dig('a', 'b'))
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"))
2294
2789
  end
2295
2790
 
2296
2791
  def test_bury_needs_a_path
2297
- assert_raises(ArgumentError) { Doc.select { :meta.bury('v') } }
2298
- assert_raises(ArgumentError) { Doc.select { :meta.bury(1.5, 'v') } }
2792
+ assert_raises(ArgumentError) { Doc.select { :meta.bury("v") } }
2793
+ assert_raises(ArgumentError) { Doc.select { :meta.bury(1.5, "v") } }
2299
2794
  end
2300
2795
 
2301
2796
  # except takes keys out, by the name of what Hash does. PostgreSQL
2302
2797
  # subtracts them where the others remove a path apiece, and what comes back
2303
2798
  # is the same document on all three.
2304
2799
  def test_except_a_key
2305
- assert_equal({ 'a' => { 'b' => 'deep' }, 'tags' => %w[x y], 'odd key' => 1 },
2800
+ assert_equal({ "a" => { "b" => "deep" }, "tags" => %w[x y], "odd key" => 1 },
2306
2801
  buried { { meta: :meta.except(:n) } })
2307
2802
  end
2308
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
+
2309
2811
  def test_except_several_keys
2310
- assert_equal({ 'a' => { 'b' => 'deep' } },
2812
+ assert_equal({ "a" => { "b" => "deep" } },
2311
2813
  buried { { meta: :meta.except(:n, :tags, :'odd key') } })
2312
2814
  end
2313
2815
 
2314
2816
  # A key that is not there is not an error, as Hash#except has none for it.
2315
2817
  def test_except_a_key_that_is_not_there
2316
- assert_equal(5, buried { { meta: :meta.except(:nothing) } }['n'])
2818
+ assert_equal(5, buried { { meta: :meta.except(:nothing) } }["n"])
2317
2819
  end
2318
2820
 
2319
2821
  # The document a bury gives back is one to take keys out of.
2320
2822
  def test_except_after_bury
2321
2823
  document = buried { { meta: :meta.bury(:fresh, 9).except(:n) } }
2322
- assert_equal(9, document['fresh'])
2323
- assert_nil(document['n'])
2824
+ assert_equal(9, document["fresh"])
2825
+ assert_nil(document["n"])
2324
2826
  end
2325
2827
 
2326
2828
  def test_except_in_a_select
2327
2829
  seed_docs
2328
- value = Doc.where { :name == 'one' }.select { :meta.except(:n).as(:v) }.first.v
2329
- assert_nil((value.is_a?(String) ? JSON.parse(value) : value)['n'])
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"])
2330
2832
  end
2331
2833
 
2332
2834
  # An index is not what the name says anywhere, and a path is bury's.
@@ -2338,7 +2840,7 @@ class TestBlockSyntax < Minitest::Test
2338
2840
 
2339
2841
  def test_default_where_syntax
2340
2842
  assert_sql(/WHERE "users"."name" = 'Ruby' AND "users"."age" = 19/,
2341
- User.where(name: 'Ruby', age: 19).to_sql)
2843
+ User.where(name: "Ruby", age: 19).to_sql)
2342
2844
  end
2343
2845
 
2344
2846
  def test_value_in_a_select_list
@@ -2350,17 +2852,58 @@ class TestBlockSyntax < Minitest::Test
2350
2852
  # that the string stays a value rather than reaching the SQL as written.
2351
2853
  def test_value_is_quoted
2352
2854
  User.delete_all
2353
- User.create!(name: 'alice')
2855
+ User.create!(name: "alice")
2354
2856
  payload = "it's a value"
2355
2857
  assert_sql(/SELECT 'draft' AS "state"/,
2356
- User.select { value('draft').as(:state) }.to_sql)
2858
+ User.select { value("draft").as(:state) }.to_sql)
2357
2859
  assert_equal([payload],
2358
2860
  User.select { value(payload).as(:note) }.map(&:note))
2359
2861
  end
2360
2862
 
2361
- def test_a_bare_string_is_still_sql
2362
- assert_sql(/SELECT "users"."name", 1 \+ 1 AS two/,
2363
- User.select { [:name, '1 + 1 AS two'] }.to_sql)
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)
2364
2907
  end
2365
2908
 
2366
2909
  def test_value_takes_the_predications
@@ -2392,11 +2935,24 @@ class TestBlockSyntax < Minitest::Test
2392
2935
  assert_raises(NoMethodError) { User.order { 1.desc } }
2393
2936
  end
2394
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
+
2395
2951
  # The alias on a literal is quoted like any other, so a name that is not a
2396
2952
  # plain one arrives as itself rather than as SQL.
2397
2953
  def test_a_value_alias_is_quoted_rather_than_refused
2398
2954
  User.delete_all
2399
- User.create!(name: 'alice')
2955
+ User.create!(name: "alice")
2400
2956
  payload = 'a" FROM users; --'
2401
2957
  assert_equal(0, User.select { value(0).as(payload.to_sym) }.first[payload].to_i)
2402
2958
  assert_equal(0, User.select { 0.as(payload.to_sym) }.first[payload].to_i)
@@ -2405,9 +2961,9 @@ class TestBlockSyntax < Minitest::Test
2405
2961
 
2406
2962
  def test_a_value_selected_reaches_the_row
2407
2963
  User.delete_all
2408
- User.create!(name: 'alice', age: 60)
2409
- assert_equal([['alice', 0]],
2410
- User.select { [:name, 0.as(:depth)] }.map {|u| [u.name, u.depth] })
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] })
2411
2967
  end
2412
2968
 
2413
2969
  def test_numeric_shorthand_is_confined_to_the_block
@@ -2427,13 +2983,13 @@ class TestBlockSyntax < Minitest::Test
2427
2983
  end
2428
2984
 
2429
2985
  {
2430
- 'sqlite3' => :sqlite,
2431
- 'postgresql' => :postgresql,
2432
- 'postgis' => :postgresql,
2433
- 'pglite' => :postgresql,
2434
- 'mysql2' => :mysql,
2435
- 'trilogy' => :mysql,
2436
- 'nothing_of_the_sort' => :unknown,
2986
+ "sqlite3" => :sqlite,
2987
+ "postgresql" => :postgresql,
2988
+ "postgis" => :postgresql,
2989
+ "pglite" => :postgresql,
2990
+ "mysql2" => :mysql,
2991
+ "trilogy" => :mysql,
2992
+ "nothing_of_the_sort" => :unknown,
2437
2993
  }.each do |adapter, family|
2438
2994
  assert_equal(family,
2439
2995
  ActiveRecord::Refined::AST.adapter_family(model.with_adapter(adapter)),