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