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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +14 -0
- data/.rubocop.yml +393 -0
- data/Gemfile +5 -3
- data/README.md +254 -74
- data/Rakefile +31 -3
- data/activerecord-refined.gemspec +29 -15
- data/benchmark/query_building.rb +11 -11
- data/examples/aggregations.rb +13 -11
- data/examples/complex_joins.rb +12 -10
- data/examples/ctes.rb +22 -20
- data/examples/expressions.rb +79 -44
- data/examples/json.rb +77 -38
- data/examples/postgresql.rb +64 -53
- data/examples/predicates.rb +35 -33
- data/examples/subqueries.rb +20 -18
- data/examples/windows.rb +23 -21
- data/examples/writes.rb +26 -24
- data/lib/active_record/refined/ast.rb +862 -272
- data/lib/active_record/refined.rb +260 -180
- data/lib/activerecord-refined/version.rb +3 -1
- data/lib/activerecord-refined.rb +8 -5
- data/test/test_block_syntax.rb +879 -323
- data/test/test_helper.rb +56 -39
- metadata +130 -1
data/test/test_block_syntax.rb
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
|
|
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 ==
|
|
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 !=
|
|
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?(
|
|
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 ==
|
|
40
|
+
User.where { (:name == "alice") & (:age > 18) }.to_sql)
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
def test_or
|
|
42
|
-
assert_sql(/WHERE \(
|
|
43
|
-
User.where { (:name ==
|
|
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 \(
|
|
48
|
-
User.where { !(:name ==
|
|
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 ==
|
|
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?(
|
|
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 ==
|
|
69
|
+
expected = ADAPTER == "postgresql" ? "ILIKE" : "LIKE"
|
|
68
70
|
assert_sql(/WHERE "users"."name" #{expected} 'ma%'/,
|
|
69
|
-
User.where { :name.ilike?(
|
|
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?(
|
|
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?(
|
|
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:
|
|
90
|
-
User.create!(name:
|
|
91
|
-
assert_equal([
|
|
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?(
|
|
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?(
|
|
103
|
+
User.where { :name.not_like?("tender%") }.to_sql)
|
|
102
104
|
end
|
|
103
105
|
|
|
104
106
|
def test_not_ilike
|
|
105
|
-
expected = ADAPTER ==
|
|
107
|
+
expected = ADAPTER == "postgresql" ? "ILIKE" : "LIKE"
|
|
106
108
|
assert_sql(/WHERE "users"."name" NOT #{expected} 'tender%'/,
|
|
107
|
-
User.where { :name.not_ilike?(
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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[
|
|
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?(
|
|
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:
|
|
219
|
-
User.create!(name:
|
|
220
|
-
User.create!(name:
|
|
221
|
-
assert_equal([
|
|
222
|
-
assert_equal([
|
|
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?(
|
|
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?(
|
|
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:
|
|
250
|
-
User.create!(name:
|
|
251
|
-
assert_equal([
|
|
252
|
-
assert_equal([
|
|
253
|
-
assert_equal([
|
|
254
|
-
assert_equal([
|
|
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 =~
|
|
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 !~
|
|
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] =~
|
|
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) =~
|
|
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(
|
|
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(
|
|
359
|
-
User.select { :age.when(10).then(
|
|
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(
|
|
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(
|
|
386
|
-
when { :age >= 60 }.then(
|
|
387
|
-
else(
|
|
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(
|
|
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(
|
|
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:
|
|
415
|
-
User.create!(name:
|
|
416
|
-
User.create!(name:
|
|
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(
|
|
420
|
-
when { :age >= 60 }.then(
|
|
421
|
-
else(
|
|
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:
|
|
504
|
-
User.create!(name:
|
|
505
|
-
User.create!(name:
|
|
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(
|
|
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:
|
|
554
|
-
User.create!(name:
|
|
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?(
|
|
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:
|
|
601
|
-
{name:
|
|
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([
|
|
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([
|
|
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:
|
|
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([
|
|
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:
|
|
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([
|
|
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:
|
|
657
|
+
User.create!(name: "alice")
|
|
656
658
|
User.create!(name: nil)
|
|
657
|
-
assert_equal([
|
|
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?(
|
|
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 ==
|
|
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?(
|
|
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?(
|
|
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:
|
|
689
|
-
User.create!(name:
|
|
690
|
-
assert_equal([
|
|
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:
|
|
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:
|
|
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 ==
|
|
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:
|
|
753
|
-
{name:
|
|
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([
|
|
757
|
-
assert_equal([
|
|
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:
|
|
766
|
-
young = -> { User.where(name:
|
|
767
|
-
assert_equal([
|
|
768
|
-
assert_equal([
|
|
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 ==
|
|
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:
|
|
794
|
-
Author.create!(name:
|
|
795
|
-
Post.create!(title:
|
|
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([
|
|
799
|
+
assert_equal(["with_post"],
|
|
798
800
|
Author.where { exists?(correlated.call) }.pluck(:name))
|
|
799
|
-
assert_equal([
|
|
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:
|
|
807
|
-
drafting = Author.create!(name:
|
|
808
|
-
Post.create!(title:
|
|
809
|
-
Post.create!(title:
|
|
810
|
-
subquery = -> { Post.where(title:
|
|
811
|
-
assert_equal([
|
|
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([
|
|
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] ==
|
|
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 ==
|
|
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 ==
|
|
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:
|
|
937
|
-
Author.create!(name:
|
|
938
|
-
Post.create!(title:
|
|
939
|
-
Post.create!(title:
|
|
940
|
-
Post.create!(title:
|
|
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:
|
|
948
|
-
Post.create!(title:
|
|
949
|
-
Post.create!(title:
|
|
950
|
-
assert_equal([
|
|
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:
|
|
963
|
-
Author.create!(name:
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
1029
|
-
Node.create!(name:
|
|
1030
|
-
other = Node.create!(name:
|
|
1031
|
-
Node.create!(name:
|
|
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:
|
|
1050
|
-
child = Node.create!(name:
|
|
1051
|
-
Node.create!(name:
|
|
1052
|
-
Node.create!(name:
|
|
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:
|
|
1065
|
-
Node.create!(name:
|
|
1066
|
-
Node.create!(name:
|
|
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([
|
|
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,
|
|
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,
|
|
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 =
|
|
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:
|
|
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(
|
|
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:
|
|
1247
|
-
assert_equal(
|
|
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:
|
|
1281
|
-
User.create!(name:
|
|
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),
|
|
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 ==
|
|
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:
|
|
1319
|
-
assert_equal([
|
|
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 ==
|
|
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 ==
|
|
1385
|
+
if ADAPTER == "postgresql"
|
|
1335
1386
|
assert_sql(/SELECT DATE_TRUNC\('day', "users"."name"\)/,
|
|
1336
|
-
User.select { date_trunc(
|
|
1387
|
+
User.select { date_trunc("day", :name) }.to_sql)
|
|
1337
1388
|
else
|
|
1338
|
-
e = assert_raises(NotImplementedError) { User.select { date_trunc(
|
|
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 ==
|
|
1348
|
-
assert_raises(NotImplementedError) { User.select { format(
|
|
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:
|
|
1352
|
-
assert_equal([
|
|
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 ==
|
|
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:
|
|
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 ==
|
|
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:
|
|
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 ==
|
|
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:
|
|
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 ==
|
|
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 ==
|
|
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 ==
|
|
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 ==
|
|
1539
|
+
skip "#{ADAPTER} has no extract" if ADAPTER == "sqlite3"
|
|
1489
1540
|
User.delete_all
|
|
1490
|
-
User.create!(name:
|
|
1541
|
+
User.create!(name: "alice")
|
|
1491
1542
|
assert_equal(2026,
|
|
1492
|
-
User.select { extract(:year, cast(
|
|
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:
|
|
1553
|
+
User.create!(name: "alice")
|
|
1503
1554
|
assert_equal(12.5,
|
|
1504
|
-
User.select { cast(
|
|
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,
|
|
1563
|
+
User.select { cast(:age, "double precision") }.to_sql)
|
|
1513
1564
|
assert_sql(/AS decimal\(10,2\)\)/,
|
|
1514
|
-
User.select { cast(:age,
|
|
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,
|
|
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
|
|
1586
|
-
when
|
|
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:
|
|
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 ==
|
|
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:
|
|
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 ==
|
|
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:
|
|
1644
|
-
User.create!(name:
|
|
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 ==
|
|
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 ==
|
|
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,
|
|
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) ==
|
|
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?(
|
|
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,
|
|
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:
|
|
1765
|
-
User.create!(name:
|
|
1766
|
-
User.create!(name:
|
|
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:
|
|
1804
|
-
Tally.create!(page:
|
|
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:
|
|
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([[
|
|
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:
|
|
1819
|
-
Tally.create!(page:
|
|
1820
|
-
Tally.where { :page ==
|
|
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:
|
|
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:
|
|
1842
|
-
Tally.upsert_all([{page:
|
|
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:
|
|
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:
|
|
1859
|
-
Tally.upsert_all([{page:
|
|
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:
|
|
1868
|
-
Tally.upsert_all([{page:
|
|
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:
|
|
1875
|
-
on_duplicate: Arel.sql(
|
|
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:
|
|
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:
|
|
1889
|
-
meta: json_document({
|
|
1890
|
-
|
|
1891
|
-
Doc.create!(name:
|
|
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([
|
|
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([
|
|
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([
|
|
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([
|
|
2039
|
+
assert_equal(["one"], Doc.where { :meta.dig_text(:n) == "5" }.pluck(:name))
|
|
1923
2040
|
type = integer_type
|
|
1924
|
-
assert_equal([
|
|
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 ==
|
|
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([
|
|
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([
|
|
2070
|
+
assert_equal(["one"], Doc.where { :meta.dig_text(:n) == upper("5") }.pluck(:name))
|
|
1954
2071
|
type = integer_type
|
|
1955
|
-
assert_equal([
|
|
2072
|
+
assert_equal(["one"], Doc.where { cast(:meta.dig_text(:n), type) == 5 }.pluck(:name))
|
|
1956
2073
|
end
|
|
1957
2074
|
|
|
1958
|
-
#
|
|
1959
|
-
#
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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([
|
|
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([
|
|
1974
|
-
Doc.where { :meta.dig(:a).dig_text(:b) ==
|
|
1975
|
-
value = Doc.where { :name ==
|
|
1976
|
-
select { :meta.dig(:a).bury(:b,
|
|
1977
|
-
assert_equal(
|
|
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([
|
|
1984
|
-
assert_equal([], Doc.where { :meta.dig(:tags).contains?([
|
|
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,
|
|
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([
|
|
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([
|
|
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([
|
|
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 ==
|
|
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:
|
|
2035
|
-
User.create!(name:
|
|
2036
|
-
User.create!(name:
|
|
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 ==
|
|
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 ==
|
|
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:
|
|
2090
|
-
Author.create!(name:
|
|
2091
|
-
Author.create!(name:
|
|
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 ==
|
|
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:
|
|
2148
|
-
Author.create!(name:
|
|
2149
|
-
Post.create!(author_id: author.id, title:
|
|
2150
|
-
Post.create!(author_id: author.id, title:
|
|
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([[
|
|
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([[
|
|
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] ==
|
|
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
|
|
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:
|
|
2207
|
-
b = Author.create!(name:
|
|
2208
|
-
Post.create!(author_id: a.id, title:
|
|
2209
|
-
Post.create!(author_id: a.id, title:
|
|
2210
|
-
Post.create!(author_id: b.id, title:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
2233
|
-
|
|
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
|
|
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 ==
|
|
2262
|
-
value = Doc.find_by(name:
|
|
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(
|
|
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) } }[
|
|
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({
|
|
2277
|
-
assert_equal([1, 2], buried { { meta: :meta.bury(:arr, [1, 2]) } }[
|
|
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,
|
|
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(
|
|
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 ==
|
|
2293
|
-
assert_equal(
|
|
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(
|
|
2298
|
-
assert_raises(ArgumentError) { Doc.select { :meta.bury(1.5,
|
|
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({
|
|
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({
|
|
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) } }[
|
|
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[
|
|
2323
|
-
assert_nil(document[
|
|
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 ==
|
|
2329
|
-
assert_nil((value.is_a?(String) ? JSON.parse(value) : value)[
|
|
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:
|
|
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:
|
|
2855
|
+
User.create!(name: "alice")
|
|
2354
2856
|
payload = "it's a value"
|
|
2355
2857
|
assert_sql(/SELECT 'draft' AS "state"/,
|
|
2356
|
-
User.select { value(
|
|
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
|
|
2362
|
-
|
|
2363
|
-
|
|
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:
|
|
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:
|
|
2409
|
-
assert_equal([[
|
|
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
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
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)),
|