activerecord-refined 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +17 -0
  3. data/README.md +93 -965
  4. data/activerecord-refined.gemspec +12 -7
  5. data/docs/conditions.md +206 -0
  6. data/docs/ctes.md +65 -0
  7. data/docs/expressions.md +125 -0
  8. data/docs/functions.md +219 -0
  9. data/docs/grouping.md +55 -0
  10. data/docs/joins.md +73 -0
  11. data/docs/json.md +230 -0
  12. data/docs/ordering.md +55 -0
  13. data/docs/time_zones.md +30 -0
  14. data/docs/windows.md +41 -0
  15. data/docs/writing.md +33 -0
  16. data/examples/aggregations.rb +18 -0
  17. data/examples/expressions.rb +35 -5
  18. data/lib/active_record/refined/ast.rb +461 -246
  19. data/lib/active_record/refined/dialect/mariadb.rb +25 -0
  20. data/lib/active_record/refined/dialect/mysql.rb +18 -0
  21. data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
  22. data/lib/active_record/refined/dialect/oracle.rb +110 -0
  23. data/lib/active_record/refined/dialect/postgresql.rb +120 -0
  24. data/lib/active_record/refined/dialect/sql_server.rb +115 -0
  25. data/lib/active_record/refined/dialect/sqlite.rb +57 -0
  26. data/lib/active_record/refined/dialect.rb +340 -0
  27. data/lib/active_record/refined.rb +682 -192
  28. data/lib/activerecord-refined/version.rb +1 -1
  29. data/lib/activerecord-refined.rb +1 -0
  30. metadata +58 -16
  31. data/.github/workflows/push_gem.yml +0 -45
  32. data/.github/workflows/sandbox.yml +0 -295
  33. data/.github/workflows/test.yml +0 -104
  34. data/.gitignore +0 -19
  35. data/.rubocop.yml +0 -393
  36. data/Gemfile +0 -14
  37. data/Rakefile +0 -53
  38. data/benchmark/query_building.rb +0 -129
  39. data/test/test_block_syntax.rb +0 -2999
  40. data/test/test_helper.rb +0 -238
data/README.md CHANGED
@@ -79,1030 +79,157 @@ comparison too, so `:age == :retirement_age` compares two columns. A value is
79
79
  written as its literal, an enum's as its string; a symbol naming no column of
80
80
  the model is refused rather than compared against nothing anyone meant.
81
81
 
82
- ### Conditions
83
-
84
- ```ruby
85
- Author.where { :age >= 18 }
86
- Author.where { :name.like?("A%") } # LIKE
87
- Author.where { :age.in?(20..40) } # BETWEEN
88
- Author.where { :age.between?(20, 40) } # BETWEEN
89
- Author.where { :age.in?(18..) } # >= 18
90
- Author.where { :country.in?(%w[JP US]) } # IN
91
- Author.where { :country.null? } # IS NULL
92
- ```
93
-
94
- `!` negates any of these. Where SQL has a negative of its own, so does the
95
- block, which is the same rows written the way they would be written by hand:
96
-
97
- ```ruby
98
- Author.where { :country.not_null? } # IS NOT NULL
99
- Author.where { :country.not_in?(%w[JP US]) } # NOT IN
100
- Author.where { :age.not_between?(20, 40) } # not between 20 and 40
101
- Author.where { :name.not_like?("A%") } # NOT LIKE
102
- Author.where { :name.not_ilike?("a%") } # NOT ILIKE / NOT LIKE
103
-
104
- Author.where { !:name.start_with?("A") } # NOT (name LIKE 'A%')
105
- ```
106
-
107
- Nothing turns on the choice: `NOT (country IS NULL)` and `country IS NOT NULL`
108
- select the same rows, NULLs included. `not_between?` is the one whose SQL
109
- looks unlike its name — Arel writes it as the two comparisons, `age < 20 OR
110
- age > 40`, which is again the same rows.
82
+ The reference — every method a symbol answers to inside a block, every
83
+ function a block can call, and what the relation takes — is on
84
+ [rubydoc.info](https://rubydoc.info/gems/activerecord-refined):
85
+ [`BlockSyntax`](https://rubydoc.info/gems/activerecord-refined/ActiveRecord/Refined/BlockSyntax)
86
+ for the symbol,
87
+ [`BlockContext`](https://rubydoc.info/gems/activerecord-refined/ActiveRecord/Refined/BlockContext)
88
+ for the block, and
89
+ [`QueryMethods`](https://rubydoc.info/gems/activerecord-refined/ActiveRecord/Refined/QueryMethods)
90
+ for the relation. What follows is a tour, a topic at a time; each has a page
91
+ of its own under [docs/](docs/) that says the rest.
111
92
 
112
- A number compares as itself, the way a bound `?` does: `:age >= 99.5` says
113
- `>= 99.5`, where `where(age: 99.5..)` casts to the column's type and says
114
- `>= 99`, letting an age of 99 through a bound it does not satisfy. Everything
115
- that is not an `Integer`, `Float` or `BigDecimal` keeps the column's own
116
- serialization — an enum's name, a time's zone, a custom type's scaling — so a
117
- custom type that scales a number, money kept in cents, is the one place the
118
- number has to be written as the column stores it.
119
-
120
- A boolean column has `true?` and `false?`, which become SQL's `IS TRUE` and
121
- `IS FALSE`, and the two negations to go with them:
122
-
123
- ```ruby
124
- Post.where { :published.true? } # IS TRUE
125
- Post.where { :published.not_true? } # IS NOT TRUE
126
- Post.where { :published.false? } # IS FALSE
127
- Post.where { :published.not_false? } # IS NOT FALSE
128
- ```
129
-
130
- `published = TRUE` selects the same rows as `published IS TRUE`, so the
131
- difference is in the negation: `published = TRUE` is itself NULL for a row
132
- where the column is, and a NULL predicate selects nothing, while `IS TRUE`
133
- answers false there. `not_true?` is therefore "false or never set" and
134
- `!(:published == true)` only "false". Every adapter spells all four the same
135
- way and answers them alike.
136
-
137
- `in?` also takes a relation as a subquery. Without an explicit select list the
138
- subquery selects the relation's primary key, the same way Active Record's own
139
- `where(id: relation)` does:
140
-
141
- ```ruby
142
- Author.where { :id.in?(Post.published.select(:author_id)) }
143
- # "authors"."id" IN (SELECT "posts"."author_id" FROM "posts" WHERE ...)
144
- ```
145
-
146
- A relation on the right of a comparison is a scalar subquery. It has to select
147
- one value, so unlike `in?` there is no default select list and one is
148
- required:
149
-
150
- ```ruby
151
- Author.where { :age >= Author.select { avg(:age) } }
152
- # "authors"."age" >= (SELECT AVG("authors"."age") FROM "authors")
153
- ```
154
-
155
- `any` and `all` quantify that comparison instead, which is what lifts the
156
- one-row rule: `> any` asks whether the subquery holds a smaller value anywhere,
157
- `>= all` whether it holds a larger one nowhere.
158
-
159
- ```ruby
160
- Author.where { :age > any(Author.where(country: "JP").select(:age)) }
161
- # "authors"."age" > ANY(SELECT "authors"."age" FROM "authors" WHERE ...)
162
-
163
- Author.where { :age >= all(Author.select(:age)) }
164
- # "authors"."age" >= ALL(SELECT "authors"."age" FROM "authors")
165
- ```
166
-
167
- The select list follows `in?`'s rule rather than the scalar one: without an
168
- explicit select the subquery selects the primary key. `== any` is what `IN`
169
- says and `!= all` what `NOT IN` says, so what the quantifiers add is the four
170
- comparisons `IN` has no spelling for. SQLite has neither quantifier, and says
171
- so with `NotImplementedError` rather than leaving its parser to.
172
-
173
- `exists?` takes a relation and becomes `EXISTS (SELECT ...)`. Correlate the
174
- subquery with the outer table through qualified columns — its `where` block
175
- goes through the DSL like any other:
93
+ ### Conditions
176
94
 
177
95
  ```ruby
96
+ Author.where { :age.between?(20, 40) & :name.like?("A%") }
97
+ Author.where { :country.in?(%w[JP US]) | :country.null? }
98
+ Author.where { !:name.start_with?("A") }
99
+ Author.where { :id.in?(Post.select(:author_id)) } # IN (subquery)
178
100
  Author.where { exists?(Post.where { :posts[:author_id] == :authors[:id] }) }
179
- # EXISTS (SELECT "posts".* FROM "posts" WHERE "posts"."author_id" = "authors"."id")
180
-
181
- Author.where { !exists?(Post.where { :posts[:author_id] == :authors[:id] }) }
182
- # NOT (EXISTS (...))
183
101
  ```
184
102
 
185
- `like?` is case-sensitive `LIKE` on every adapter, including PostgreSQL, where
186
- Arel would otherwise reach for `ILIKE`. `ilike?` is the one that asks for
187
- `ILIKE`; off PostgreSQL it is plain `LIKE`, which those adapters already match
188
- case-insensitively under their default collations. `casecmp?` is
189
- case-insensitive equality, folded on both sides rather than left to the
190
- collation, so it means the same thing everywhere:
191
-
192
- ```ruby
193
- Author.where { :name.ilike?("ma%") } # ILIKE 'ma%' / LIKE 'ma%'
194
- Author.where { :name.casecmp?("Alice") } # LOWER(name) = LOWER('Alice')
195
- ```
196
-
197
- `not_distinct_from?` and `distinct_from?` compare with NULL treated as a
198
- value, rather than as the unknown that makes `=` and `<>` neither true nor
199
- false. PostgreSQL spells this `IS [NOT] DISTINCT FROM`, SQLite `IS` / `IS NOT`
200
- and MySQL `<=>`, and the rows that come back are the same on all three:
201
-
202
- ```ruby
203
- Author.where { :country.not_distinct_from?(params[:country]) } # matches NULL to nil
204
- Author.where { :country.distinct_from?("JP") } # keeps the NULL rows
205
- ```
206
-
207
- `start_with?`, `end_with?` and `include?` are shortcuts for the usual `like?`
208
- patterns. Unlike `like?`, they treat their argument as a literal string, so `%`
209
- and `_` in it are escaped rather than matched as wildcards:
210
-
211
- ```ruby
212
- Author.where { :name.start_with?("A") } # LIKE 'A%'
213
- Author.where { :name.end_with?("son") } # LIKE '%son'
214
- Author.where { :name.include?("test") } # LIKE '%test%'
215
- ```
216
-
217
- Like their String namesakes, `start_with?` and `end_with?` take any number of
218
- literals; matching any one of them is enough:
219
-
220
- ```ruby
221
- Author.where { :name.start_with?("A", "B") }
222
- # (name LIKE 'A%' OR name LIKE 'B%')
223
- ```
224
-
225
- `member?`, `superset?`, `subset?` and `intersect?` compare against a
226
- PostgreSQL array column, each carrying the meaning of its Ruby namesake:
227
- `member?` is Enumerable's element test (which String does not have — that is
228
- what separates it from `include?`), `superset?` and `subset?` are Set's
229
- whole-array containment, and `intersect?` is Array's "any element in common":
230
-
231
- ```ruby
232
- Article.where { :tags.member?("ruby") } # tags @> '{ruby}'
233
- Article.where { :scores.member?(80) } # scores @> '{80}'
234
- Article.where { :tags.superset?(%w[ruby rails]) } # tags @> '{ruby,rails}'
235
- Article.where { :tags.subset?(%w[ruby rails go]) } # tags <@ '{ruby,rails,go}'
236
- Article.where { :tags.intersect?(%w[ruby go]) } # tags && '{ruby,go}'
237
- ```
238
-
239
- Like its namesake, `member?` takes one element — `[1, 2].member?([1])` is
240
- false in Ruby, so an Array argument raises rather than quietly meaning
241
- something `Array#member?` does not. Requiring every element is `superset?`.
242
-
243
- `=~` and `!~` match a regular expression: `REGEXP` and `NOT REGEXP` on MySQL,
244
- `~` and `!~` on PostgreSQL. SQLite has no regexp operator of its own, so it
245
- raises there.
246
-
247
- ```ruby
248
- Author.where { :name =~ "^A" } # REGEXP / ~
249
- Author.where { :name !~ "^A" } # NOT REGEXP / !~
250
- Author.where { :name =~ /son$/ } # a Regexp literal works too
251
- ```
252
-
253
- Only a literal's source crosses over; the database has its own dialect and no
254
- equivalent of Ruby's flags. Dropping one would silently change what the query
255
- matches, so `/son$/i` raises instead — pass the pattern as a string if the
256
- database can express what you mean.
257
-
258
- `==` always means SQL `=`, and passes its value through untouched. A Range or an
259
- Array therefore compares against a PostgreSQL range or array column, the same
260
- way Active Record's own `where(period: from...to)` does for those column types:
261
-
262
- ```ruby
263
- Reservation.where { :period == (from...to) } # daterange = '[from,to)'
264
- Article.where { :tags == %w[ruby rails] } # text[] = '{ruby,rails}'
265
- ```
266
-
267
- `!=` is SQL `!=` under the same rules, value passed through untouched.
268
-
269
- For the same reason `== nil` and `!= nil` raise `ArgumentError`: `= NULL` is
270
- never true in SQL, so a NULL test has to be spelled as one. Use `null?`:
271
-
272
- ```ruby
273
- Author.where { :country.null? } # country IS NULL
274
- Author.where { !:country.null? } # NOT (country IS NULL)
275
- ```
276
-
277
- Combine predicates with `&`, `|` and `!`. Ruby's operator precedence makes the
278
- parentheses around each comparison necessary, though the `?` methods above need
279
- none:
280
-
281
- ```ruby
282
- Author.where { (:age >= 18) & ((:country == "JP") | (:country == "US")) }
283
- Author.where { !(:age.in?(0..17) | :country.null?) }
284
- Author.where { !:country.in?(%w[JP US]) } # NOT (country IN ('JP', 'US'))
285
- Author.where { !:name.like?("%test%") } # NOT (name LIKE '%test%')
286
- ```
103
+ `&`, `|` and `!` are AND, OR and NOT. A value on the right is quoted as Active
104
+ Record quotes it, a column compares against a column, a relation is a
105
+ subquery. [docs/conditions.md](docs/conditions.md) has the rest: the
106
+ negations SQL spells for itself, `true?` and NULL, regular expressions,
107
+ `ANY` and `ALL`, PostgreSQL arrays.
287
108
 
288
109
  ### Joins
289
110
 
290
- The block is the `ON` clause:
291
-
292
- ```ruby
293
- Author.
294
- joins(:posts) { :posts[:author_id] == :authors[:id] }.
295
- joins(:comments) { :comments[:post_id] == :posts[:id] }
296
-
297
- Author.left_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }
298
- ```
299
-
300
- `as` names the table within the query, which is what makes a self join
301
- expressible — the qualified columns in the block go by that name:
302
-
303
111
  ```ruby
112
+ Author.joins(:posts) { :posts[:author_id] == :authors[:id] }
304
113
  Employee.joins(:employees, as: :managers) { :managers[:id] == :employees[:manager_id] }
305
- # SELECT "employees".* FROM "employees"
306
- # INNER JOIN "employees" "managers" ON "managers"."id" = "employees"."manager_id"
307
- ```
308
-
309
- `right_outer_joins` and `full_outer_joins` are the two Active Record has no
310
- method for, and they take what `joins` takes. An association name is not among
311
- it: what Active Record reads out of one is an inner or a left join and nothing
312
- else, so these two want the block that says how to join.
313
-
314
- ```ruby
315
- Author.right_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }
316
- Author.full_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }
317
- ```
318
-
319
- MySQL has no `FULL OUTER JOIN` and neither has MariaDB, so `full_outer_joins`
320
- raises `NotImplementedError` there. SQLite has had one since 3.39.
321
-
322
- `cross_joins` is every row of one table against every row of the other. There
323
- is no condition to give, so it takes no block — `as` still names the table:
324
-
325
- ```ruby
326
- Post.cross_joins(:authors) # FROM "posts" CROSS JOIN "authors"
327
- Post.cross_joins(:posts, as: :others) # FROM "posts" CROSS JOIN "posts" "others"
328
- ```
329
-
330
- ### Keeping one row per group
331
-
332
- `distinct_on` is PostgreSQL's `DISTINCT ON`: the first row of each group the
333
- order brings up.
334
-
335
- ```ruby
336
- Post.distinct_on { :author_id }.order { [:author_id, :likes.desc] }
337
- # SELECT DISTINCT ON ( "author_id" ) "posts".* FROM "posts"
338
- # ORDER BY "author_id", "likes" DESC
339
- ```
340
-
341
- Arel carries the node and refuses to write it for the others, the way it does
342
- a regexp, so it raises `NotImplementedError` on SQLite and MySQL. The shape
343
- that runs everywhere is a `row_number` window in a subquery, which says the
344
- same thing at more length:
345
-
346
- ```ruby
347
- ranked = Post.select {
348
- [:author_id, :likes, row_number.over.partition(:author_id).order(:likes.desc).as(:rn)]
349
- }
350
- Post.from(ranked, :posts).where { :rn == 1 }
351
- ```
352
-
353
- The subquery is named after the model's own table for the reason `from_cte`
354
- is: Active Record goes on qualifying columns with that name, so `where` needs
355
- to find it.
356
-
357
- ### Grouping several ways at once
358
-
359
- `grouping_sets`, `rollup` and `cube` ask for more than one grouping in a
360
- single query, the totals of each coming back beside the rows. Each set is a
361
- list of its own, and an empty one is the grand total:
362
-
363
- ```ruby
364
- Sale.group { grouping_sets([:region], [:product], []) }.
365
- select { [:region, :product, sum(:amount).as(:total)] }
366
- # GROUP BY GROUPING SETS( ( "region" ), ( "product" ), ( ) )
367
-
368
- Sale.group { rollup(:region, :product) } # GROUP BY ROLLUP( "region", "product" )
369
- Sale.group { cube(:region, :product) } # GROUP BY CUBE( "region", "product" )
370
- ```
371
-
372
- A row that a set did not group by comes back with NULL there, which is also
373
- what a real NULL looks like; `fn(:grouping, :region)` tells the two apart.
374
-
375
- `grouping_sets` and `cube` are PostgreSQL's; SQLite has none of the three and
376
- both raise `NotImplementedError` elsewhere. `rollup` runs on MySQL and
377
- MariaDB too, spelled as their `WITH ROLLUP` — which trails the whole group
378
- list, so there a rollup cannot stand beside other group entries the way
379
- `ROLLUP(...)` can, and the block says so. MariaDB is also the one that
380
- refuses `ORDER BY` next to it, and the one without `fn(:grouping, ...)`.
381
-
382
- ### Lateral joins
383
-
384
- A relation marked `lateral` joins in place of a table, and sees the row being
385
- joined to — in SQL the keyword modifies the subquery, not the join, so that is
386
- where it is written. It is what makes the top row of each group reachable in
387
- one query:
388
-
389
- ```ruby
390
- top_post = Post.select { :title }.
391
- where { :posts[:author_id] == :authors[:id] }.
392
- order { :likes.desc }.limit(1)
393
-
394
- Author.left_outer_joins(top_post.lateral, as: :top).
395
- select { [:name, :top[:title].as(:top_post)] }
396
- # SELECT "name", "top"."title" AS "top_post" FROM "authors"
397
- # LEFT OUTER JOIN LATERAL (SELECT "title" FROM "posts"
398
- # WHERE "posts"."author_id" = "authors"."id" ORDER BY "likes" DESC LIMIT 1) "top" ON TRUE
399
114
  ```
400
115
 
401
- `as` is required — the relation has no name of its own to qualify with. Without
402
- a block the join is `ON TRUE`, which is the usual shape: what the subquery is
403
- allowed to see is said inside it. A block writes a real `ON` clause.
404
-
405
- PostgreSQL has `LATERAL` and so has MySQL, from 8.0.14. SQLite has none, and
406
- neither has MariaDB, which answers to the same adapter as MySQL; both raise
407
- `NotImplementedError`. Arel has a node for it but only PostgreSQL's visitor
408
- writes it, so the SQL is written here instead.
409
-
410
- ### Common table expressions
411
-
412
- Active Record's `with` and `with_recursive` need nothing from this gem: a CTE
413
- is joined by name like any other table, so its `ON` clause is a block, where
414
- Rails' own documentation reaches for a string join.
415
-
416
- `from_cte` takes the CTE's name and selects it under the model's own table
417
- name, so the model's columns resolve:
418
-
419
- ```ruby
420
- Node.with_recursive(
421
- tree: [
422
- Node.where { :id == root.id }.
423
- select { [:id, :name, :parent_id, 0.as(:depth)] },
424
- Node.joins(:tree) { :nodes[:parent_id] == :tree[:id] }.
425
- select { [:id, :name, :parent_id, (:tree[:depth] + 1).as(:depth)] },
426
- ]
427
- ).from_cte(:tree)
428
- # WITH RECURSIVE "tree" AS (
429
- # SELECT "nodes"."id", "nodes"."name", "nodes"."parent_id", 0 AS depth
430
- # FROM "nodes" WHERE "nodes"."id" = 1
431
- # UNION ALL
432
- # SELECT "nodes"."id", "nodes"."name", "nodes"."parent_id",
433
- # ("tree"."depth" + 1) AS depth
434
- # FROM "nodes" INNER JOIN "tree" ON "nodes"."parent_id" = "tree"."id"
435
- # ) SELECT "nodes".* FROM "tree" AS "nodes"
436
- ```
437
-
438
- The anchor starts the count and the recursive member adds one, which is how
439
- the shape of a tree comes out of a flat table. The `0` is a value rather than
440
- SQL — see [`value`](#aggregates-functions-and-aliases) below for why a number
441
- can say `.as` directly.
442
-
443
- The alias on the last line is there for Active Record's sake, not SQL's:
444
- written by hand that line would be `SELECT * FROM tree`. Active Record goes on qualifying
445
- columns with the model's table name, so without the alias that name is not in
446
- the query and anything qualifying a column fails:
447
-
448
- ```ruby
449
- Node.with_recursive(tree: [...]).from(:tree).where(name: 'root')
450
- # PG::UndefinedTable: missing FROM-clause entry for table "nodes"
451
- ```
452
-
453
- Since the model's name is the only one that works, `from_cte` takes it from
454
- the model rather than asking. It also checks that the name is one `with`
455
- declares, so a typo is an `ArgumentError` here rather than a query against a
456
- table nobody has — checked when the SQL is built, so the CTE may be declared
457
- later in the chain or by a scope merged into it.
458
-
459
- `from(:tree, as: :nodes)` is the same thing spelled out, without the check,
460
- and is what to reach for when the name wanted is not the model's.
461
-
462
- What makes this worth spelling out is how selectively it breaks. `count`,
463
- `order` and `select` never qualify, so they work without the alias on every
464
- adapter; it is `where` and `find_by` that stop. A query can therefore look
465
- right until the day a condition is added to it.
466
-
467
- A non-recursive CTE joins the same way:
468
-
469
- ```ruby
470
- Node.with(roots: Node.where { :parent_id.null? }).
471
- joins(:roots) { :roots[:id] == :nodes[:parent_id] }
472
- ```
473
-
474
- `examples/ctes.rb` walks a category tree with these.
116
+ `joins`, `left_outer_joins`, `right_outer_joins`, `full_outer_joins` and
117
+ `cross_joins` take the `ON` as a block and `as:` for a table alias; a
118
+ relation joins as a subquery, and one marked `lateral` as a `LATERAL` one.
119
+ [docs/joins.md](docs/joins.md).
475
120
 
476
121
  ### Aggregates and functions
477
122
 
478
- `count`, `sum`, `avg`, `min` and `max` are available as methods, as are the
479
- bit aggregates and the scalar functions below, with `fn` for anything else. Return an array to select
480
- or order by multiple expressions.
481
-
482
- `filter` takes the aggregate over the rows a condition holds for, as a value
483
- or a block:
484
-
485
- ```ruby
486
- Author.select { count(:*).filter { :age < 50 }.as(:young) }
487
- # COUNT(*) FILTER (WHERE "age" < 50) AS "young"
488
-
489
- Author.select {
490
- [count(:*).as(:all), sum(:age).filter { :country == "JP" }.as(:jp_years)]
491
- }
492
- ```
493
-
494
- MySQL has no `FILTER` clause, and gets the case that means the same thing —
495
- `COUNT(CASE WHEN "age" < 50 THEN 1 END)`. An aggregate passes over a NULL, so
496
- a row the condition misses is a row it does not see, and the number that comes
497
- back is the same on all three.
498
-
499
- Pass `:*` to `count` for `COUNT(*)`, and `distinct: true` for
500
- `COUNT(DISTINCT ...)`:
501
-
502
123
  ```ruby
503
124
  Author.group { :country }.having { count(:*) > 1 }
504
- # SELECT "authors".* FROM "authors" GROUP BY "authors"."country" HAVING COUNT(*) > 1
505
-
506
- Post.select { count(:author_id, distinct: true) } # COUNT(DISTINCT "author_id")
507
- ```
508
-
509
- The scalar functions are real methods rather than anything caught dynamically,
510
- so a misspelling is a `NoMethodError` where you wrote it, and a name Ruby also
511
- answers to — `rand` — means the SQL one inside a block:
512
-
513
- ```
514
- abs acos asin atan atan2 bit_and bit_count bit_or bit_xor cast
515
- ceil char_length coalesce concat
516
- cos current_date current_time current_timestamp date_trunc degrees
517
- exp extract floor format greatest least length ln localtime
518
- localtimestamp log log10 log2 lower ltrim mod now nullif pi
519
- power radians rand replace round rtrim sign sin sqrt substr tan
520
- trim trunc upper
521
- ```
522
-
523
- Most are spelled the same everywhere. Where they are not, the method names one
524
- meaning and each adapter gets its own spelling: `char_length`, `greatest` and
525
- `least` become `LENGTH`, `MAX` and `MIN` on SQLite, and `rand` is `RAND` on
526
- MySQL and `RANDOM` elsewhere, and `trunc` is `TRUNCATE` on MySQL, which
527
- insists on the second argument the others default to zero — SQLite's takes
528
- only the one. Where an adapter has no equivalent — `date_trunc` outside
529
- PostgreSQL, `now` and the `local*` pair on SQLite, `log2` on PostgreSQL,
530
- whose spelling is `log(2, x)`, the four `bit_*` on SQLite — the block raises
531
- `NotImplementedError` rather than leaving the database to reject the SQL.
532
-
533
- `format` is printf formatting, and raises on MySQL, where a function of the
534
- same name does something else entirely: it puts separators in a number, and
535
- reads a printf template as the number zero rather than complaining. `fn` still
536
- reaches it, spelled as the different thing it is:
537
-
538
- ```ruby
539
- Post.select { fn(:format, :amount, 2) } # MySQL's, on purpose
540
- ```
541
-
542
- `fn` reaches functions without a method of their own. Its name is emitted as
543
- written, so a case-sensitive one can be spelled exactly:
544
-
545
- ```ruby
546
- Post.select { fn(:date_trunc, "day", :created_at).as(:day) }
547
- # SELECT date_trunc('day', "posts"."created_at") AS day
548
- ```
549
-
550
- `op` is the same escape hatch for operators — PostgreSQL alone has dozens
551
- with no method here, `<@` and `&&` and the geometric ones among them. The
552
- operator is emitted as written and, like `fn`'s names, whether the adapter
553
- has it is your assertion; it is checked against the characters PostgreSQL
554
- allows an operator, so a letter, a space or a quote is refused rather than
555
- written into the SQL. Both sides take what `fn`'s arguments take — a
556
- column, an expression, a value quoted by the adapter — and a value is
557
- spelled in the adapter's own syntax, `to_json` saying a document, `'{a,b}'`
558
- an array; a Ruby Hash or Array is refused rather than guessed at. The
559
- result is parenthesized, its precedence being unknown, and so is an
560
- expression on either side, so a dug value cannot be re-grouped out from
561
- under it:
562
-
563
- ```ruby
564
- Post.where { op("&&", :tags, "{ruby,sql}") }
565
- # WHERE ("posts"."tags" && '{ruby,sql}')
566
-
567
- Post.where { op("<@", :meta.dig(:author), { name: "alice" }.to_json) }
568
- # WHERE (("meta" #> '{author}') <@ '{"name":"alice"}')
569
- ```
570
-
571
- `sql` is the last resort, for what neither `fn` nor `op` can spell: the
572
- statement goes out as written. It is the one way a string means SQL inside a
573
- block — everywhere else a string is a value — so writing SQL is always asked
574
- for by name, and an interpolation has a spelling that is not it: `?` and
575
- `:name` placeholders take values quoted by the adapter, through
576
- `sanitize_sql_array`. A `?` is rewritten only when there are positional binds
577
- to put in it, so PostgreSQL's `?` operators can share a statement with named
578
- binds, or with none. The result carries the predications and arithmetic, and
579
- is parenthesized where it stands inside a larger expression — its precedence
580
- is whatever was written — but comes out bare at the top of a select list,
581
- where parentheses would refuse an alias written into the string:
582
-
583
- ```ruby
584
- Post.where { sql("length(title) > ?", 10) }
585
- # WHERE (length(title) > 10)
586
-
587
- Post.where { sql("score + ?", 10) * 2 >= 60 }
588
- # WHERE (score + 10) * 2 >= 60
589
-
590
- Post.select { sql("count(*) FILTER (WHERE score > 0) AS positive") }
591
- ```
592
-
593
- Values are quoted by the adapter wherever they appear, as they are in
594
- Active Record, and so is a column alias. That is what makes the name asked for
595
- the name that comes back: unquoted, PostgreSQL folds a capital away where the
596
- other two keep it, so one block would mean two things. It also leaves nothing
597
- to refuse — a name that would have been SQL becomes an identifier with a
598
- strange name instead:
599
-
600
- ```ruby
601
- Author.select { count(:*).as(:postCount) } # AS "postCount" everywhere
602
- Author.select { count(:*).as(:'total sales') } # AS "total sales"
603
- ```
604
-
605
- `quote: false` asks for the name as written, for a schema that wants the
606
- folding. Nothing quotes it then, so a name that is not plain is refused:
607
-
608
- ```ruby
609
- Author.select { count(:*).as(:post_count, quote: false) } # AS post_count
610
- Author.select { count(:*).as(:'total sales', quote: false) } # ArgumentError
611
- ```
612
-
613
- `fn`'s function name is the one that cannot be quoted: quoting stops
614
- PostgreSQL folding it, and `"UPPER"(x)` is a function that does not exist.
615
- That one, `cast`'s type and `extract`'s field are neither values nor
616
- identifiers, so they have to be plain names and anything else raises
617
- `ArgumentError` rather than reaching the query.
618
-
619
- `current_date`, `current_time`, `current_timestamp`, `localtime` and
620
- `localtimestamp` come out without parentheses, as the grammar has them —
621
- written as calls, PostgreSQL and SQLite would reject them. What does go into
622
- parentheses is an optional precision — `current_timestamp(3)` — which
623
- `current_date` never takes and SQLite never accepts. `current_timestamp` is
624
- the portable spelling of what `now` means, and reaches SQLite where `now`
625
- does not:
626
-
627
- ```ruby
628
- Post.where { :published_at <= current_timestamp }
629
- # SELECT "posts".* FROM "posts" WHERE "posts"."published_at" <= CURRENT_TIMESTAMP
630
- ```
631
-
632
- `extract` and `cast` are grammar as well: the field and the type go where no
633
- value could. The field has to be a plain name, and the type has to look like
634
- a type — a plain name, at most parenthesized with lengths, so the adapters'
635
- own spellings like `double precision` or `decimal(10,2)` pass; anything else
636
- raises `ArgumentError`. The type is the adapter's own name for the type, and
637
- whether it exists is the database's to say. SQLite spells everything
638
- `extract` does as `strftime` formats, which no renaming carries, so `extract`
639
- raises there:
640
-
641
- ```ruby
642
- Post.where { extract(:year, :created_at) == 2026 }
643
- # SELECT "posts".* FROM "posts" WHERE EXTRACT(YEAR FROM "posts"."created_at") = 2026
644
-
125
+ Author.select { [count(:*).filter { :age < 50 }.as(:young), avg(:age).as(:average)] }
126
+ Post.group { :author_id }.select { string_agg(:title, ", ").order(:title).as(:titles) }
127
+ Post.where { :created_at > current_timestamp - 7.days }
645
128
  Post.select { cast(:price, "decimal(10,2)").as(:price) }
646
- # SELECT CAST("posts"."price" AS decimal(10,2)) AS price
647
129
  ```
648
130
 
649
- ### Expressions
650
-
651
- `+`, `-`, `*` and `/` build arithmetic. Ruby puts them above the comparison
652
- operators, so an expression groups the way it reads:
131
+ The scalar functions are methods — `upper`, `coalesce`, `round` and the rest
132
+ — spelled the adapter's way where the adapters differ, and `fn` reaches any
133
+ other by name. [docs/functions.md](docs/functions.md).
653
134
 
654
- ```ruby
655
- Item.where { :price * :quantity > 1000 }
656
- Item.select { sum(:price * :quantity).as(:total) }
657
- ```
658
-
659
- The number may stand on the left — only a column or an expression on the
660
- right builds a query, so Ruby's own arithmetic is untouched — and
661
- `BigDecimal` is a number here, being what a decimal column's values are,
662
- quoted as the exact decimal on either side. A `Rational` is refused: no
663
- decimal spells `1/3r` exactly, and `to_d` is what says the decimal meant.
664
-
665
- ```ruby
666
- Item.select { greatest(20 - :quantity, 0).as(:shortfall) }
667
- Item.where { BigDecimal("1.08") * :price > 500 }
668
- ```
669
-
670
- `&`, `|`, `^`, `~`, `<<` and `>>` are SQL's bitwise operators. Between
671
- conditions `&` and `|` are AND and OR, and that is where they are defined,
672
- which leaves them free to mean here what SQL means by them:
135
+ ### Expressions
673
136
 
674
137
  ```ruby
138
+ LineItem.where { :price * :quantity > 1000 }
675
139
  Post.where { :flags & 4 > 0 }
676
- # WHERE ("posts"."flags" & 4) > 0
677
-
678
- Post.select { (:flags | 4).as(:flags) }
679
- Post.select { (~:flags).as(:inverted) }
680
- ```
681
-
682
- Each parenthesises itself, which is what keeps Ruby's grouping: PostgreSQL
683
- gives `&` and `|` the same precedence and reads `a | b & c` from the left,
684
- where Ruby reads the `&` first.
685
-
686
- A boolean column is refused rather than taken for the one bit it is stored as.
687
- MySQL and SQLite would quietly answer as `AND` would, PostgreSQL has no such
688
- operator at all, and one block meaning two things is worse than an
689
- `ArgumentError` saying that `true?` is what makes a boolean column a
690
- condition. A condition as an operand is refused for the same reason.
691
-
692
- XOR is the one the three do not share, and the one where guessing costs most:
693
- MySQL spells it `^`, which is exponentiation to PostgreSQL, and PostgreSQL
694
- spells it `#`, which is where a comment starts on MySQL — either way a wrong
695
- answer rather than an error. Each adapter gets its own, and SQLite, which has
696
- no XOR at all, gets the two operations it is made of, `(a | b) - (a & b)`.
697
- That names each operand twice, so keep them cheap.
698
-
699
- `bit_and`, `bit_or` and `bit_xor` are the aggregates of the first three, and
700
- `bit_count` counts the bits that are set. SQLite has none of the four.
701
- PostgreSQL counts the bits of a bit string rather than of a number, so the
702
- argument is cast there, to `bit(64)` because that is what makes a negative
703
- count as it does on MySQL:
704
-
705
- ```ruby
706
- Post.group { :author_id }.select { bit_or(:flags).as(:flags) }
707
- # SELECT BIT_OR("posts"."flags") AS "flags" ... GROUP BY "posts"."author_id"
708
-
709
- Post.select { bit_count(:flags).as(:bits) }
710
- # MySQL: BIT_COUNT("posts"."flags")
711
- # PostgreSQL: BIT_COUNT(CAST("posts"."flags" AS bit(64)))
712
- ```
713
-
714
- `bit_xor` arrived in PostgreSQL 14. `~` is where the three disagree about the
715
- answer rather than the question: MySQL reads it back as the unsigned 64-bit
716
- number, the others as a negative one, and the bits are the same either way.
717
-
718
- One place asks for a value to be said out loud: the top of a select list.
719
- Everywhere else a bare literal is already a value — `where { :age > 18 }`,
720
- `concat(:name, '-x')` — but a bare string at the top of the list would be SQL
721
- to Active Record and a value everywhere else in the block, so it is refused
722
- rather than read either way: `sql` says the SQL, `value` the value. `value`
723
- carries the predications and arithmetic with it, so a literal can be compared
724
- and combined like anything else, and numbers and strings have a shorthand,
725
- since a literal that has been sent `as` has already said it is a value:
726
-
727
- ```ruby
728
- Node.select { [:id, value(0).as(:depth)] }
729
- # SELECT "nodes"."id", 0 AS depth FROM "nodes"
730
-
731
- Node.select { [:id, 0.as(:depth)] } # the same thing
732
-
733
- Post.select { [:title, "draft".as(:state)] }
734
- # SELECT "posts"."title", 'draft' AS state FROM "posts"
735
- ```
736
-
737
- What the shorthand does not cover, `value` still spells: `value(true)`,
738
- `value(nil)`, or a literal that goes on to be compared rather than selected.
739
-
740
- `CASE` is grammar rather than a function, and has two shapes. With an operand, each `when` is
741
- something to compare it against; without one, each `when` carries a condition
742
- of its own. `case` is a Ruby keyword, so the method behind both is only
743
- reachable through the receiver — `self.case` — and each shape has a shorthand
744
- that does not need it:
745
-
746
- ```ruby
747
140
  Author.select { :country.when("JP").then("Japan").else("elsewhere").as(:where) }
748
- # CASE "country" WHEN 'JP' THEN 'Japan' ELSE 'elsewhere' END AS where
749
-
750
141
  Author.select { case_when { :age >= 60 }.then("senior").else("adult").as(:band) }
751
- # CASE WHEN "age" >= 60 THEN 'senior' ELSE 'adult' END AS band
752
-
753
- Author.select { self.case(mod(:age, 10)).when(0).then("round").else("not").as(:v) }
754
142
  ```
755
143
 
756
- A `when` takes a value or a block, and so do `then` and `else`; the block is
757
- there to read like the blocks around it, since an argument works just as well
758
- — `:age >= 60` has already become an expression by the time it is passed.
759
- Leaving the `else` off is SQL's own default, which is NULL. `when` and `then`
760
- come in pairs, and one without the other is an `ArgumentError` rather than
761
- something that reaches the database:
762
-
763
- ```ruby
764
- Author.select {
765
- case_when { :age < 18 }.then("minor").
766
- when { :age >= 60 }.then("senior").
767
- else("adult").as(:band)
768
- }
769
-
770
- Author.select { sum(case_when { :age >= 60 }.then(1).else(0)).as(:seniors) }
771
- # SUM(CASE WHEN "age" >= 60 THEN 1 ELSE 0 END) AS seniors
772
- ```
144
+ Arithmetic, the bitwise operators and `CASE` are expressions like a column,
145
+ so they compare, alias and aggregate. [docs/expressions.md](docs/expressions.md).
773
146
 
774
147
  ### JSON
775
148
 
776
- `dig` reads inside a JSON document, by the name of what `Hash` does. A string
777
- or symbol steps into an object, an integer into an array, and what comes back
778
- is still JSON — the way `Hash#dig` hands back the structure itself — for a
779
- document to be dug into further or asked the JSON questions. `dig_text` gives
780
- the value as text instead, which is what a comparison wants:
781
-
782
149
  ```ruby
783
- Post.where { :meta.dig_text(:author, :name) == "alice" }
784
- Post.select { :meta.dig(:author).as(:author) }
785
- Post.where { :meta.key?(:draft) }
786
- Post.where { :meta.contains?(status: "open") }
787
- ```
788
-
789
- No two adapters spell any of this alike, and the block is the same on all
790
- three:
791
-
792
- | | PostgreSQL | SQLite | MySQL |
793
- | --- | --- | --- | --- |
794
- | `dig(:a, :b)` | `#> '{a,b}'` | `-> '$.a.b'` | `JSON_EXTRACT(…, '$.a.b')` |
795
- | `dig_text(:a, :b)` | `#>> '{a,b}'` | `->> '$.a.b'` | `JSON_UNQUOTE(JSON_EXTRACT(…, '$.a.b'))` |
796
- | `key?(:a)` | `? 'a'` | `json_type(…, '$.a') IS NOT NULL` | `JSON_CONTAINS_PATH(…, 'one', '$.a')` |
797
- | `contains?(…)` | `@>` | — | `JSON_CONTAINS` |
798
-
799
- MariaDB answers to the `mysql2` adapter and has none of `->` or `->>`, so the
800
- MySQL family goes through the functions, which both have.
801
-
802
- `dig_text` gives text everywhere. SQLite's `->>` would otherwise hand back the
803
- value with its type, so a comparison that worked there would fail on the other
804
- two; a number is compared through a `cast` on all three:
805
-
806
- ```ruby
807
- Post.where { :meta.dig_text(:n) == "5" }
808
- Post.where { cast(:meta.dig_text(:n), "integer") > 6 } # 'signed' on MySQL
809
- ```
810
-
811
- The type is the adapter's own name for it, here as everywhere `cast` is used.
812
-
813
- Strings and numbers are where the adapters agree. A JSON boolean comes back as
814
- `"1"` on SQLite, which turns `true` into SQL's `1` before the text cast, and
815
- as `"true"` on the other two; a JSON `null` is SQL `NULL` everywhere but
816
- MariaDB, which spells it `"null"`. A key that is not there is `NULL` on all
817
- three.
818
-
819
- Comparing `dig_text`'s value with anything but a string raises
820
- `ArgumentError` rather than being left to the adapters, which answer it three
821
- ways: `dig_text(:n) == 5` is true on SQLite, an error on PostgreSQL and true
822
- on MySQL, and `dig_text(:flag) == true` is true, an error and false. `cast`
823
- is what says which type was meant, and then all three agree.
824
-
825
- A JSON comparison — `dig`'s side, and `bury`'s and `except`'s — belongs to
826
- the JSON types: on PostgreSQL's `jsonb` and MySQL's `JSON` alike, numbers
827
- compare as numbers and documents structurally, key order and spelling aside,
828
- so a dug value compares with a Ruby one directly. SQLite and MariaDB have
829
- only the text of each, which is a different question, and raise
830
- `NotImplementedError` as the SQL is written. `in?` and `between?` are the
831
- two MySQL leaves out of its JSON comparisons, so there they are spelled as
832
- the comparisons they mean — the range as its bounds, the list as one
833
- equality per element, which names the dug value once per element the way
834
- SQLite's XOR names its operands twice:
835
-
836
- ```ruby
837
- Post.where { :meta.dig(:stars) >= 10 } # PostgreSQL and MySQL
838
- Post.where { :meta.dig(:author) == { "name" => "alice" } }
839
- Post.where { :meta.dig(:stars).in?([5, 10]) }
840
- Post.where { cast(:meta.dig_text(:stars), "integer") >= 10 } # everywhere
841
- ```
842
-
843
- A column, a function or another dug value on the right goes through untouched
844
- on every adapter. Arithmetic and the bit operators are refused outright on
845
- both sides — `dig_text(:n) + 1` is 6 on SQLite, an error on PostgreSQL and
846
- 6.0 on MariaDB — and `cast` settles those too.
847
-
848
- `bury` sets what `dig` reads: the last argument is the value and the rest are
849
- the path to it. The document comes back changed rather than being written
850
- anywhere, so `update_all` is what makes it stick:
851
-
852
- ```ruby
853
- Post.update_all { { meta: :meta.bury(:author, :name, "alice") } }
854
- # SET "meta" = jsonb_set("meta", '{author,name}', '"alice"')
855
- # ... JSON_SET("meta", '$.author.name', 'alice') elsewhere
856
-
857
- Post.update_all { { meta: :meta.bury(:tags, ["ruby", "sql"]) } }
858
- Post.update_all { { meta: :meta.bury(:copy, :meta.dig(:n)) } }
859
- ```
860
-
861
- A whole document goes in as one — an object or an array rather than the string
862
- that spells it — which each adapter takes its own way round, and a boolean
863
- goes in as JSON too, which SQLite would otherwise write as its `1`. `bury` is
864
- not a Ruby method; it is the name Ruby considered for the other end of `dig`,
865
- and
866
- SQL has no one name to borrow here, since PostgreSQL says `jsonb_set` where
867
- the others say `JSON_SET`.
868
-
869
- `except` takes keys out again, and takes them as `Hash#except` does — keys of
870
- the document, however many, rather than a path, which is `bury`'s way of
871
- reaching further in. It gives back the document changed, so it chains with
872
- `bury` and goes where `bury` goes:
873
-
874
- ```ruby
875
- Post.update_all { { meta: :meta.except(:draft) } }
876
- # SET "meta" = "meta" - CAST('{"draft"}' AS text[])
877
- # ... JSON_REMOVE("meta", '$.draft') elsewhere
878
-
879
- Post.update_all { { meta: :meta.bury(:author, :name, "alice").except(:tmp) } }
880
- ```
881
-
882
- A key that is not there is not an error, as it is not to `Hash#except`. The
883
- cast is not decoration: `jsonb` has three subtractions — a key, an array of
884
- keys, an element by index — and an array literal written without a type is
885
- read as the first of them, so `"meta" - '{draft}'` takes out the key spelled
886
- `{draft}`, which is nothing, and says nothing about it.
887
-
888
- A key deeper in is reached through the chain: `dig` reads the part out,
889
- `except` takes the key from it, and `bury` puts it back:
890
-
891
- ```ruby
892
- Post.update_all { { meta: :meta.bury(:author, :meta.dig(:author).except(:email)) } }
893
- ```
894
-
895
- What `dig` gives is a document, so the JSON operations read it — the same
896
- question asked of a part of the document rather than of all of it:
897
-
898
- ```ruby
899
- Post.where { :meta.dig(:author).key?(:email) }
900
- Post.where { :meta.dig(:author).dig_text(:name) == "alice" }
901
- Post.update_all { { meta: :meta.dig(:author).bury(:name, "alice") } }
902
- ```
903
-
904
- Containment reads it too, on the adapters that have containment at all:
905
-
906
- ```ruby
907
- Post.where { :meta.dig(:tags).contains?(["ruby"]) }
908
- ```
909
-
910
- Asking the same of `dig_text` raises `ArgumentError`: what it gives is text,
911
- and reading text back as a document is where the adapters part company —
912
- SQLite parses it, MySQL takes it as written, and PostgreSQL has no such
913
- function for text at all.
914
-
915
- `contains?` has no equivalent on SQLite and raises `NotImplementedError`
916
- there — later than the rest, since the adapter is only known when the SQL is
917
- built. On PostgreSQL, `dig` and `dig_text` are all the `json` type carries;
918
- `key?`, `contains?`, `bury` and `except` want a `jsonb` column.
919
-
920
- A key that is not a plain name travels as itself rather than being refused:
921
- `dig(:'odd key')` becomes `'{odd key}'` or `$."odd key"`.
922
-
923
- `keys` gives the keys of the document, as `Hash#keys` does — a JSON array
924
- of them. Only the MySQL family has a function for it; the other two reach
925
- the same array through a subquery over their key-listing functions, guarded
926
- by type so that all four answer alike: the keys of anything that is not an
927
- object are `NULL` — rather than SQLite's array indices or PostgreSQL's
928
- error — and the keys of `{}` are `[]` rather than PostgreSQL's `NULL`:
929
-
930
- ```ruby
931
- Post.select { :meta.keys.as(:fields) }
932
- Post.select { :meta.dig(:author).keys.as(:author_fields) }
933
- # JSON_KEYS("meta") MySQL
934
- # CASE WHEN jsonb_typeof("meta") = 'object' THEN COALESCE((…)) PostgreSQL
935
- # CASE WHEN json_type("meta") = 'object' THEN (SELECT …) SQLite
150
+ Doc.where { :meta.dig_text(:author, :name) == "alice" }
151
+ Doc.where { :meta.key?(:draft) }
152
+ Doc.update_all { { meta: :meta.bury(:author, :name, "alice") } }
153
+ Post.group { :author_id }.select { json_arrayagg(:title).as(:titles) }
936
154
  ```
937
155
 
938
- The order the keys come in is the adapters' own: the JSON types give their
939
- normalized order and the text ones the stored order the same divide every
940
- JSON comparison here rides on.
156
+ `dig` and `dig_text`, `bury`, `except`, `key?` and `keys` read and change a
157
+ document by the names Hash uses; `json_object` and `json_arrayagg` build one. The same block
158
+ runs on PostgreSQL's `jsonb`, MySQL's JSON and SQLite's.
159
+ [docs/json.md](docs/json.md).
941
160
 
942
- `json_array` and `json_object` build a document in the row — `json_array`
943
- from the values given, `json_object` from a Ruby hash. The names are the
944
- standard's, which SQLite and the MySQL family say as written; PostgreSQL is
945
- asked to build `jsonb`. A hash rather than SQL's alternating keys and
946
- values, because a bare symbol means a column in every block here: the keys
947
- are Ruby's and the values are expressions, so `title: :title` reads the
948
- column in under its own name with no rule to remember:
161
+ ### Window functions
949
162
 
950
163
  ```ruby
951
- Post.select { json_object(title: :title, stars: :meta.dig(:stars)).as(:summary) }
952
- # jsonb_build_object('title', "title", 'stars', "meta" #> '{stars}') PostgreSQL
953
- # JSON_OBJECT('title', "title", 'stars', JSON_EXTRACT(…)) elsewhere
954
-
955
- Post.where { :meta.dig(:author) == json_object(name: :name) }
164
+ Author.select { [:name, row_number.over.partition(:country).order(:age.desc).as(:rank)] }
165
+ Post.select { sum(:likes).over.order(:created_at).rows(..0).as(:running_total) }
956
166
  ```
957
167
 
958
- A Ruby value among the arguments goes in as its JSON self — a string or a
959
- number as themselves, `nil` as `null`, and a boolean or a whole document
960
- through the same route `bury` takes them, so SQLite's `true` is not its
961
- `1`. A key that is not a string or a symbol is refused, before the
962
- adapters answer a NULL key three ways. The empty calls stand —
963
- `json_array()` is `[]` and `json_object()` is `{}` on all four — and what
964
- comes back is JSON as `dig`'s is, so the operations and comparisons above
965
- read it.
168
+ `over` on any aggregate or window function, then `partition`, `order`,
169
+ `rows` and `range`. [docs/windows.md](docs/windows.md).
966
170
 
967
- `json_arrayagg` and `json_objectagg` gather rows into one JSON document — a
968
- value from each row into an array, a key and a value into an object. The
969
- names are the SQL standard's, which the MySQL family says as written;
970
- PostgreSQL is asked the `jsonb` pair and SQLite its own:
171
+ ### Ordering, aliases and collation
971
172
 
972
173
  ```ruby
973
- Post.group { :author_id }.select { json_arrayagg(:title).as(:titles) }
974
- # jsonb_agg("title") PostgreSQL
975
- # json_group_array("title") SQLite
976
- # JSON_ARRAYAGG("title") MySQL
977
-
978
- Post.select { json_objectagg(:title, :meta.dig(:stars)).as(:stars) }
979
-
980
- Post.group { :author_id }.
981
- select { json_arrayagg(json_object(title: :title, stars: :meta.dig(:stars))).as(:posts) }
174
+ Author.order { :country.asc.nulls_last }
175
+ Author.select { upper(:name).as(:author) }
176
+ Author.where { :name.collate(:nocase) == "alice" }
982
177
  ```
983
178
 
984
- What they give is JSON as `dig`'s is, so it compares the way a dug value
985
- does, and `filter` and `over` come along as with any aggregate — with two
986
- refusals where a respelling would change the meaning rather than the
987
- spelling. The MySQL family has no `FILTER`, and the `CASE` that stands in
988
- for it elsewhere would leave a JSON `null` in the document for every row it
989
- drops, so there `filter` raises `NotImplementedError`; MariaDB takes every
990
- other aggregate as a window function but not these two, so `over` raises
991
- there too.
992
-
993
- The documents agree across adapters, up to the edges of their JSON types.
994
- Over no rows at all SQLite answers `[]` and `{}` where the others answer
995
- `NULL`, as their aggregates do. A key aggregated twice keeps the last pair
996
- on the JSON types — `jsonb` and MySQL's — and every pair on the text ones,
997
- SQLite and MariaDB, and a `NULL` key is an error on the former pair and a
998
- dropped pair on the latter. And a bare JSON *column* is text to SQLite's
999
- `json_group_array`, so it lands as the string that spells the document
1000
- rather than nesting as it does on the other three; a dug value nests
1001
- everywhere, so `json_arrayagg(:meta.dig(:author))` is the portable way to
1002
- collect part of a document.
179
+ [docs/ordering.md](docs/ordering.md).
1003
180
 
1004
- ### Window functions
1005
-
1006
- `over` gives a function a window, which is what turns an aggregate into a
1007
- running one and the only thing `row_number` and its kind can be used with.
1008
- The window is built by chaining, as Arel's own is:
181
+ ### Grouping, CTEs and DISTINCT ON
1009
182
 
1010
183
  ```ruby
1011
- Author.select { avg(:age).over.partition(:country).as(:country_average) }
1012
- # AVG("age") OVER (PARTITION BY "country") AS country_average
1013
-
1014
- Author.select { row_number.over.partition(:country).order(:age.desc).as(:rank) }
1015
- # ROW_NUMBER() OVER (PARTITION BY "country" ORDER BY "age" DESC) AS rank
1016
-
1017
- Author.select { count(:*).over.as(:total) } # COUNT(*) OVER () — every row
184
+ Sale.group { rollup(:region, :product) }
185
+ Post.distinct_on { :author_id }.order { [:author_id, :likes.desc] } # PostgreSQL
186
+ Node.with_recursive(tree: [Node.where { :id == 1 }, Node.joins(:tree) { :nodes[:parent_id] == :tree[:id] }]).from_cte(:tree)
1018
187
  ```
1019
188
 
1020
- `row_number`, `rank`, `dense_rank`, `percent_rank`, `cume_dist`, `ntile`,
1021
- `lag`, `lead`, `first_value`, `last_value` and `nth_value` are the functions
1022
- that say nothing without a window; each raises `ArgumentError` if `over` never
1023
- arrives, rather than reaching the database as an error there. Every adapter
1024
- that has window functions at all spells them the same way, so unlike the
1025
- scalar functions there is nothing here to translate.
189
+ [docs/grouping.md](docs/grouping.md) and [docs/ctes.md](docs/ctes.md).
1026
190
 
1027
- A frame is a range of rows counted from the current one — negative before it,
1028
- positive after, 0 the row itself, and an open end for unbounded:
191
+ ### Writing
1029
192
 
1030
193
  ```ruby
1031
- Post.select { sum(:likes).over.order(:created_at).rows(..0).as(:running) }
1032
- # SUM("likes") OVER (ORDER BY "created_at" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
1033
-
1034
- Post.select { avg(:likes).over.order(:created_at).rows(-1..1).as(:smoothed) }
1035
- # ... ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
1036
-
1037
- Post.select { sum(:likes).over.order(:created_at).rows(0..).as(:remaining) }
1038
- # ... ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
194
+ Post.where { :published == true }.update_all { { likes: :likes + 1 } }
195
+ Tally.upsert_all(rows, unique_by: :page) { { hits: :hits + excluded(:hits) } }
1039
196
  ```
1040
197
 
1041
- `range` says `RANGE` where `rows` says `ROWS`, and a window has one frame or
1042
- none. Named windows — `WINDOW w AS (...)` — have no clause in Active Record to
1043
- live in, so they are not here.
198
+ [docs/writing.md](docs/writing.md).
1044
199
 
1045
- ### Aliases and ordering
200
+ ### Time zones
1046
201
 
1047
- `.as` gives an expression a column alias, and `.asc` / `.desc` give an
1048
- ordering its direction. The orderings take `.nulls_first` / `.nulls_last` as
1049
- well. MySQL has no such syntax, but Arel emulates it there, so the resulting
1050
- order is the same everywhere:
202
+ Active Record stores in UTC and a `Time` in a block is quoted the way Active
203
+ Record quotes one, so `where { :created_at > Time.current - 1.day }` is right
204
+ whatever `Time.zone` is; what the database says the time is
205
+ `current_timestamp`, `extract` — is the session's business.
206
+ [docs/time_zones.md](docs/time_zones.md).
1051
207
 
1052
- ```ruby
1053
- Author.order { :country.asc.nulls_last }
1054
- ```
1055
-
1056
- Together:
1057
-
1058
- ```ruby
1059
- Author.
1060
- joins(:posts) { :posts[:author_id] == :authors[:id] }.
1061
- where { :posts[:published] == true }.
1062
- group { :authors[:id] }.
1063
- having { count(:posts[:id]) > 1 }.
1064
- order { count(:posts[:id]).desc }.
1065
- select {
1066
- [
1067
- upper(:authors[:name]).as(:author),
1068
- count(:posts[:id]).as(:post_count),
1069
- avg(:posts[:likes]).as(:avg_likes),
1070
- ]
1071
- }
1072
- ```
208
+ ## Other adapters
1073
209
 
1074
- ### Writing
1075
-
1076
- `update_all` reads its hash the way Active Record does `update_all(likes: :likes)`
1077
- sets the column to the symbol itself. The block reads a symbol as the column it
1078
- names, as every other block here does, which is what lets the new value be
1079
- worked out from the old:
210
+ SQLite, PostgreSQL, MySQL, MariaDB, Oracle and SQL Server are built in: each
211
+ is a `Dialect`, one class per family of spellings, asked for whatever the
212
+ databases write differently. An adapter the gem does not know keeps the
213
+ standard spellings, which reach further than you might expect; where they
214
+ fall short, a dialect of your own says the rest. Subclass
215
+ `ActiveRecord::Refined::Dialect` or the built-in family the database
216
+ descends from — override only what it spells differently, and register it
217
+ under the adapter's name:
1080
218
 
1081
219
  ```ruby
1082
- Post.where { :published == true }.update_all { { likes: :likes + 1 } }
1083
- # UPDATE "posts" SET "likes" = ("posts"."likes" + 1) WHERE ...
1084
-
1085
- Post.update_all { { title: upper(:title), likes: case_when { :likes < 0 }.then(0).else(:likes) } }
1086
- ```
220
+ class ExampleDialect < ActiveRecord::Refined::Dialect
221
+ # The example database spells char_length LEN, and has no random ordering.
222
+ FUNCTIONS = { char_length: "LEN", rand: nil }.freeze
1087
223
 
1088
- `upsert_all` takes one too, for the part that decides what happens to a row
1089
- that is already there. `excluded` is the row that could not be inserted:
224
+ def full_outer_join_supported? = false
225
+ end
1090
226
 
1091
- ```ruby
1092
- Tally.upsert_all(rows, unique_by: :page) { { hits: :hits + excluded(:hits) } }
1093
- # ... ON CONFLICT ("page") DO UPDATE SET "hits"=("tallies"."hits" + "excluded"."hits")
227
+ ActiveRecord::Refined::Dialect.register("exampledb", ExampleDialect)
1094
228
  ```
1095
229
 
1096
- PostgreSQL and SQLite name that row `excluded`; MySQL spells the same thing
1097
- `VALUES(column)`, and the block comes out as whichever the adapter reads.
1098
- Active Record's own `on_duplicate:` takes SQL text and nothing else, so this is
1099
- the one place the DSL writes SQL out itself rather than handing Arel a tree —
1100
- and the two cannot both be given.
1101
-
1102
- `insert_all` has no block: its values are literals by construction.
1103
- Active Record type-casts each one on the way into the `VALUES` list, so an
1104
- expression does not become SQL there — it becomes nothing, silently. Use
1105
- `upsert_all` where a row's value has to be worked out.
230
+ `register` also takes a block for an adapter whose dialect only the
231
+ connection can name — the way `mysql2` answers for MySQL and MariaDB both —
232
+ receiving the model and returning the class.
1106
233
 
1107
234
  ## Performance
1108
235
 
@@ -1171,16 +298,17 @@ differently and CI runs both. Override with `DB_HOST`, `DB_PORT`,
1171
298
  `DB_USERNAME` and `DB_PASSWORD`. The `activerecord_refined_test` database is
1172
299
  created on first use.
1173
300
 
1174
- The `pg` and `mysql2` gems are in the Gemfile's `db` group, since building them
1175
- needs the client libraries installed. Skip them if SQLite is all you need,
1176
- which is what CI's SQLite job does:
301
+ The client gems sit in optional Gemfile groups named after their adapters,
302
+ since building each needs its client library installed. A plain bundle
303
+ serves SQLite with nothing extra; opt in to the adapters you will reach:
1177
304
 
1178
305
  ```sh
1179
- bundle config set --local without db
306
+ bundle config set --local with postgresql mysql2 trilogy
1180
307
  ```
1181
308
 
1182
- CI runs all three, one job per adapter, with PostgreSQL and MySQL as service
1183
- containers.
309
+ CI runs one job per adapter with the servers as service containers, Oracle
310
+ and SQL Server included — those two have no local server here and run on CI
311
+ alone, their clients opted in the same way.
1184
312
 
1185
313
  ## Releasing
1186
314