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