activerecord-refined 0.5.1 → 0.6.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.
data/README.md CHANGED
@@ -105,6 +105,23 @@ select the same rows, NULLs included. `not_between?` is the one whose SQL
105
105
  looks unlike its name — Arel writes it as the two comparisons, `age < 20 OR
106
106
  age > 40`, which is again the same rows.
107
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
+
108
125
  `in?` also takes a relation as a subquery. Without an explicit select list the
109
126
  subquery selects the relation's primary key, the same way ActiveRecord's own
110
127
  `where(id: relation)` does:
@@ -123,6 +140,24 @@ Author.where { :age >= Author.select { avg(:age) } }
123
140
  # "authors"."age" >= (SELECT AVG("authors"."age") FROM "authors")
124
141
  ```
125
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
+
126
161
  `exists?` takes a relation and becomes `EXISTS (SELECT ...)`. Correlate the
127
162
  subquery with the outer table through qualified columns — its `where` block
128
163
  goes through the DSL like any other:
@@ -259,6 +294,82 @@ Employee.joins(:employees, as: :managers) { :managers[:id] == :employees[:manage
259
294
  # INNER JOIN "employees" "managers" ON "managers"."id" = "employees"."manager_id"
260
295
  ```
261
296
 
297
+ ### Keeping one row per group
298
+
299
+ `distinct_on` is PostgreSQL's `DISTINCT ON`: the first row of each group the
300
+ order brings up.
301
+
302
+ ```ruby
303
+ Post.distinct_on { :author_id }.order { [:author_id, :likes.desc] }
304
+ # SELECT DISTINCT ON ( "author_id" ) "posts".* FROM "posts"
305
+ # ORDER BY "author_id", "likes" DESC
306
+ ```
307
+
308
+ Arel carries the node and refuses to write it for the others, the way it does
309
+ a regexp, so it raises `NotImplementedError` on SQLite and MySQL. The shape
310
+ that runs everywhere is a `row_number` window in a subquery, which says the
311
+ same thing at more length:
312
+
313
+ ```ruby
314
+ ranked = Post.select {
315
+ [:author_id, :likes, row_number.over.partition(:author_id).order(:likes.desc).as(:rn)]
316
+ }
317
+ Post.from(ranked, :posts).where { :rn == 1 }
318
+ ```
319
+
320
+ The subquery is named after the model's own table for the reason `from_cte`
321
+ is: ActiveRecord goes on qualifying columns with that name, so `where` needs
322
+ to find it.
323
+
324
+ ### Grouping several ways at once
325
+
326
+ `grouping_sets`, `rollup` and `cube` ask for more than one grouping in a
327
+ single query, the totals of each coming back beside the rows. Each set is a
328
+ list of its own, and an empty one is the grand total:
329
+
330
+ ```ruby
331
+ Sale.group { grouping_sets([:region], [:product], []) }.
332
+ select { [:region, :product, sum(:amount).as(:total)] }
333
+ # GROUP BY GROUPING SETS( ( "region" ), ( "product" ), ( ) )
334
+
335
+ Sale.group { rollup(:region, :product) } # GROUP BY ROLLUP( "region", "product" )
336
+ Sale.group { cube(:region, :product) } # GROUP BY CUBE( "region", "product" )
337
+ ```
338
+
339
+ A row that a set did not group by comes back with NULL there, which is also
340
+ what a real NULL looks like; `fn(:grouping, :region)` tells the two apart.
341
+
342
+ These are PostgreSQL's. SQLite has none of them, and MySQL has only `WITH
343
+ ROLLUP`, which says one of the three and says it elsewhere in the clause, so
344
+ the block raises `NotImplementedError` on both.
345
+
346
+ ### Lateral joins
347
+
348
+ `lateral: true` joins a relation rather than a table, and lets it see the row
349
+ being joined to. That is what makes the top row of each group reachable in one
350
+ query:
351
+
352
+ ```ruby
353
+ top_post = Post.select { :title }.
354
+ where { :posts[:author_id] == :authors[:id] }.
355
+ order { :likes.desc }.limit(1)
356
+
357
+ Author.left_outer_joins(top_post, as: :top, lateral: true).
358
+ select { [:name, :top[:title].as(:top_post)] }
359
+ # SELECT "name", "top"."title" AS "top_post" FROM "authors"
360
+ # LEFT OUTER JOIN LATERAL (SELECT "title" FROM "posts"
361
+ # WHERE "posts"."author_id" = "authors"."id" ORDER BY "likes" DESC LIMIT 1) "top" ON TRUE
362
+ ```
363
+
364
+ `as` is required — the relation has no name of its own to qualify with. Without
365
+ a block the join is `ON TRUE`, which is the usual shape: what the subquery is
366
+ allowed to see is said inside it. A block writes a real `ON` clause.
367
+
368
+ PostgreSQL has `LATERAL` and so has MySQL, from 8.0.14. SQLite has none, and
369
+ neither has MariaDB, which answers to the same adapter as MySQL; both raise
370
+ `NotImplementedError`. Arel has a node for it but only PostgreSQL's visitor
371
+ writes it, so the SQL is written here instead.
372
+
262
373
  ### Common table expressions
263
374
 
264
375
  ActiveRecord's `with` and `with_recursive` need nothing from this gem: a CTE
@@ -274,8 +385,7 @@ Node.with_recursive(
274
385
  Node.where { :id == root.id }.
275
386
  select { [:id, :name, :parent_id, 0.as(:depth)] },
276
387
  Node.joins(:tree) { :nodes[:parent_id] == :tree[:id] }.
277
- select { [:nodes[:id], :nodes[:name], :nodes[:parent_id],
278
- (:tree[:depth] + 1).as(:depth)] },
388
+ select { [:id, :name, :parent_id, (:tree[:depth] + 1).as(:depth)] },
279
389
  ]
280
390
  ).from_cte(:tree)
281
391
  # WITH RECURSIVE "tree" AS (
@@ -304,8 +414,13 @@ Node.with_recursive(tree: [...]).from(:tree).where(name: 'root')
304
414
  ```
305
415
 
306
416
  Since the model's name is the only one that works, `from_cte` takes it from
307
- the model rather than asking. `from(:tree, as: :nodes)` is the same thing
308
- spelled out, and is what to reach for when the name wanted is not the model's.
417
+ the model rather than asking. It also checks that the name is one `with`
418
+ declares, so a typo is an `ArgumentError` here rather than a query against a
419
+ table nobody has — checked when the SQL is built, so the CTE may be declared
420
+ later in the chain or by a scope merged into it.
421
+
422
+ `from(:tree, as: :nodes)` is the same thing spelled out, without the check,
423
+ and is what to reach for when the name wanted is not the model's.
309
424
 
310
425
  What makes this worth spelling out is how selectively it breaks. `count`,
311
426
  `order` and `select` never qualify, so they work without the alias on every
@@ -321,19 +436,46 @@ Node.with(roots: Node.where { :parent_id.null? }).
321
436
 
322
437
  `examples/ctes.rb` walks a category tree with these.
323
438
 
324
- ### Aggregates, functions and aliases
439
+ ### Aggregates and functions
325
440
 
326
441
  `count`, `sum`, `avg`, `min` and `max` are available as methods, as are the
327
- scalar functions below, with `fn` for anything else. Use `.as` for a column
328
- alias, and `.asc` / `.desc` for the sort direction. Return an array to select
442
+ bit aggregates and the scalar functions below, with `fn` for anything else. Return an array to select
329
443
  or order by multiple expressions.
330
444
 
445
+ `filter` takes the aggregate over the rows a condition holds for, as a value
446
+ or a block:
447
+
448
+ ```ruby
449
+ Author.select { count(:*).filter { :age < 50 }.as(:young) }
450
+ # COUNT(*) FILTER (WHERE "age" < 50) AS "young"
451
+
452
+ Author.select {
453
+ [count(:*).as(:all), sum(:age).filter { :country == 'JP' }.as(:jp_years)]
454
+ }
455
+ ```
456
+
457
+ MySQL has no `FILTER` clause, and gets the case that means the same thing —
458
+ `COUNT(CASE WHEN "age" < 50 THEN 1 END)`. An aggregate passes over a NULL, so
459
+ a row the condition misses is a row it does not see, and the number that comes
460
+ back is the same on all three.
461
+
462
+ Pass `:*` to `count` for `COUNT(*)`, and `distinct: true` for
463
+ `COUNT(DISTINCT ...)`:
464
+
465
+ ```ruby
466
+ Author.group { :country }.having { count(:*) > 1 }
467
+ # SELECT "authors".* FROM "authors" GROUP BY "authors"."country" HAVING COUNT(*) > 1
468
+
469
+ Post.select { count(:author_id, distinct: true) } # COUNT(DISTINCT "author_id")
470
+ ```
471
+
331
472
  The scalar functions are real methods rather than anything caught dynamically,
332
473
  so a misspelling is a `NoMethodError` where you wrote it, and a name Ruby also
333
474
  answers to — `rand` — means the SQL one inside a block:
334
475
 
335
476
  ```
336
- abs acos asin atan atan2 cast ceil char_length coalesce concat
477
+ abs acos asin atan atan2 bit_and bit_count bit_or bit_xor cast
478
+ ceil char_length coalesce concat
337
479
  cos current_date current_time current_timestamp date_trunc degrees
338
480
  exp extract floor format greatest least length ln localtime
339
481
  localtimestamp log log10 log2 lower ltrim mod now nullif pi
@@ -348,8 +490,51 @@ MySQL and `RANDOM` elsewhere, and `trunc` is `TRUNCATE` on MySQL, which
348
490
  insists on the second argument the others default to zero — SQLite's takes
349
491
  only the one. Where an adapter has no equivalent — `date_trunc` outside
350
492
  PostgreSQL, `now` and the `local*` pair on SQLite, `log2` on PostgreSQL,
351
- whose spelling is `log(2, x)` — the block raises `NotImplementedError`
352
- rather than leaving the database to reject the SQL.
493
+ whose spelling is `log(2, x)`, the four `bit_*` on SQLite — the block raises
494
+ `NotImplementedError` rather than leaving the database to reject the SQL.
495
+
496
+ `format` is printf formatting, and raises on MySQL, where a function of the
497
+ same name does something else entirely: it puts separators in a number, and
498
+ reads a printf template as the number zero rather than complaining. `fn` still
499
+ reaches it, spelled as the different thing it is:
500
+
501
+ ```ruby
502
+ Post.select { fn(:format, :amount, 2) } # MySQL's, on purpose
503
+ ```
504
+
505
+ `fn` reaches functions without a method of their own. Its name is emitted as
506
+ written, so a case-sensitive one can be spelled exactly:
507
+
508
+ ```ruby
509
+ Post.select { fn(:date_trunc, 'day', :created_at).as(:day) }
510
+ # SELECT date_trunc('day', "posts"."created_at") AS day
511
+ ```
512
+
513
+ Values are quoted by the adapter wherever they appear, as they are in
514
+ ActiveRecord, and so is a column alias. That is what makes the name asked for
515
+ the name that comes back: unquoted, PostgreSQL folds a capital away where the
516
+ other two keep it, so one block would mean two things. It also leaves nothing
517
+ to refuse — a name that would have been SQL becomes an identifier with a
518
+ strange name instead:
519
+
520
+ ```ruby
521
+ Author.select { count(:*).as(:postCount) } # AS "postCount" everywhere
522
+ Author.select { count(:*).as(:'total sales') } # AS "total sales"
523
+ ```
524
+
525
+ `quote: false` asks for the name as written, for a schema that wants the
526
+ folding. Nothing quotes it then, so a name that is not plain is refused:
527
+
528
+ ```ruby
529
+ Author.select { count(:*).as(:post_count, quote: false) } # AS post_count
530
+ Author.select { count(:*).as(:'total sales', quote: false) } # ArgumentError
531
+ ```
532
+
533
+ `fn`'s function name is the one that cannot be quoted: quoting stops
534
+ PostgreSQL folding it, and `"UPPER"(x)` is a function that does not exist.
535
+ That one, `cast`'s type and `extract`'s field are neither values nor
536
+ identifiers, so they have to be plain names and anything else raises
537
+ `ArgumentError` rather than reaching the query.
353
538
 
354
539
  `current_date`, `current_time`, `current_timestamp`, `localtime` and
355
540
  `localtimestamp` come out without parentheses, as the grammar has them —
@@ -381,30 +566,63 @@ Post.select { cast(:price, 'decimal(10,2)').as(:price) }
381
566
  # SELECT CAST("posts"."price" AS decimal(10,2)) AS price
382
567
  ```
383
568
 
384
- `format` is printf formatting, and raises on MySQL, where a function of the
385
- same name does something else entirely: it puts separators in a number, and
386
- reads a printf template as the number zero rather than complaining. `fn` still
387
- reaches it, spelled as the different thing it is:
569
+ ### Expressions
570
+
571
+ `+`, `-`, `*` and `/` build arithmetic. Ruby puts them above the comparison
572
+ operators, so an expression groups the way it reads:
388
573
 
389
574
  ```ruby
390
- Post.select { fn(:format, :amount, 2) } # MySQL's, on purpose
575
+ Item.where { :price * :quantity > 1000 }
576
+ Item.select { sum(:price * :quantity).as(:total) }
391
577
  ```
392
578
 
393
- Pass `:*` to `count` for `COUNT(*)`, and `distinct: true` for
394
- `COUNT(DISTINCT ...)`:
579
+ `&`, `|`, `^`, `~`, `<<` and `>>` are SQL's bitwise operators. Between
580
+ conditions `&` and `|` are AND and OR, and that is where they are defined,
581
+ which leaves them free to mean here what SQL means by them:
395
582
 
396
583
  ```ruby
397
- Author.group { :country }.having { count(:*) > 1 }
398
- # SELECT "authors".* FROM "authors" GROUP BY "authors"."country" HAVING COUNT(*) > 1
584
+ Post.where { :flags & 4 > 0 }
585
+ # WHERE ("posts"."flags" & 4) > 0
399
586
 
400
- Post.select { count(:author_id, distinct: true) } # COUNT(DISTINCT "author_id")
587
+ Post.select { (:flags | 4).as(:flags) }
588
+ Post.select { (~:flags).as(:inverted) }
401
589
  ```
402
590
 
403
- Values are quoted by the adapter wherever they appear, as they are in
404
- ActiveRecord. Column aliases and `fn`'s function name are not they are
405
- written into the SQL as given — so those two have to be plain names,
406
- optionally qualified by a schema in `fn`'s case. Anything else raises
407
- `ArgumentError` rather than reaching the query.
591
+ Each parenthesises itself, which is what keeps Ruby's grouping: PostgreSQL
592
+ gives `&` and `|` the same precedence and reads `a | b & c` from the left,
593
+ where Ruby reads the `&` first.
594
+
595
+ A boolean column is refused rather than taken for the one bit it is stored as.
596
+ MySQL and SQLite would quietly answer as `AND` would, PostgreSQL has no such
597
+ operator at all, and one block meaning two things is worse than an
598
+ `ArgumentError` saying that `true?` is what makes a boolean column a
599
+ condition. A condition as an operand is refused for the same reason.
600
+
601
+ XOR is the one the three do not share, and the one where guessing costs most:
602
+ MySQL spells it `^`, which is exponentiation to PostgreSQL, and PostgreSQL
603
+ spells it `#`, which is where a comment starts on MySQL — either way a wrong
604
+ answer rather than an error. Each adapter gets its own, and SQLite, which has
605
+ no XOR at all, gets the two operations it is made of, `(a | b) - (a & b)`.
606
+ That names each operand twice, so keep them cheap.
607
+
608
+ `bit_and`, `bit_or` and `bit_xor` are the aggregates of the first three, and
609
+ `bit_count` counts the bits that are set. SQLite has none of the four.
610
+ PostgreSQL counts the bits of a bit string rather than of a number, so the
611
+ argument is cast there, to `bit(64)` because that is what makes a negative
612
+ count as it does on MySQL:
613
+
614
+ ```ruby
615
+ Post.group { :author_id }.select { bit_or(:flags).as(:flags) }
616
+ # SELECT BIT_OR("posts"."flags") AS "flags" ... GROUP BY "posts"."author_id"
617
+
618
+ Post.select { bit_count(:flags).as(:bits) }
619
+ # MySQL: BIT_COUNT("posts"."flags")
620
+ # PostgreSQL: BIT_COUNT(CAST("posts"."flags" AS bit(64)))
621
+ ```
622
+
623
+ `bit_xor` arrived in PostgreSQL 14. `~` is where the three disagree about the
624
+ answer rather than the question: MySQL reads it back as the unsigned 64-bit
625
+ number, the others as a negative one, and the bits are the same either way.
408
626
 
409
627
  One place asks for a value to be said out loud: the top of a select list.
410
628
  Everywhere else a bare literal is already a value — `where { :age > 18 }`,
@@ -429,30 +647,158 @@ SQL in a select list, a value everywhere else — and refining it would make the
429
647
  same literal mean one thing or the other depending on whether it had been sent
430
648
  a message.
431
649
 
432
- `fn` reaches functions without a method of their own. Its name is emitted as
433
- written, so a case-sensitive one can be spelled exactly:
650
+ `CASE` is grammar rather than a function, and has two shapes. With an operand, each `when` is
651
+ something to compare it against; without one, each `when` carries a condition
652
+ of its own. `case` is a Ruby keyword, so the method behind both is only
653
+ reachable through the receiver — `self.case` — and each shape has a shorthand
654
+ that does not need it:
434
655
 
435
656
  ```ruby
436
- Post.select { fn(:date_trunc, 'day', :created_at).as(:day) }
437
- # SELECT date_trunc('day', "posts"."created_at") AS day
657
+ Author.select { :country.when('JP').then('Japan').else('elsewhere').as(:where) }
658
+ # CASE "country" WHEN 'JP' THEN 'Japan' ELSE 'elsewhere' END AS where
659
+
660
+ Author.select { case_when { :age >= 60 }.then('senior').else('adult').as(:band) }
661
+ # CASE WHEN "age" >= 60 THEN 'senior' ELSE 'adult' END AS band
662
+
663
+ Author.select { self.case(mod(:age, 10)).when(0).then('round').else('not').as(:v) }
438
664
  ```
439
665
 
440
- `+`, `-`, `*` and `/` build arithmetic. Ruby puts them above the comparison
441
- operators, so an expression groups the way it reads:
666
+ A `when` takes a value or a block, and so do `then` and `else`; the block is
667
+ there to read like the blocks around it, since an argument works just as well
668
+ — `:age >= 60` has already become an expression by the time it is passed.
669
+ Leaving the `else` off is SQL's own default, which is NULL. `when` and `then`
670
+ come in pairs, and one without the other is an `ArgumentError` rather than
671
+ something that reaches the database:
442
672
 
443
673
  ```ruby
444
- Item.where { :price * :quantity > 1000 }
445
- Item.select { sum(:price * :quantity).as(:total) }
674
+ Author.select {
675
+ case_when { :age < 18 }.then('minor').
676
+ when { :age >= 60 }.then('senior').
677
+ else('adult').as(:band)
678
+ }
679
+
680
+ Author.select { sum(case_when { :age >= 60 }.then(1).else(0)).as(:seniors) }
681
+ # SUM(CASE WHEN "age" >= 60 THEN 1 ELSE 0 END) AS seniors
682
+ ```
683
+
684
+ ### JSON
685
+
686
+ `dig` reads inside a JSON document, by the name of what `Hash` does. A string
687
+ or symbol steps into an object, an integer into an array:
688
+
689
+ ```ruby
690
+ Post.where { :meta.dig(:author, :name) == 'alice' }
691
+ Post.select { :meta.dig(:tags, 0).as(:first_tag) }
692
+ Post.where { :meta.key?(:draft) }
693
+ Post.where { :meta.contains?(status: 'open') }
694
+ ```
695
+
696
+ No two adapters spell any of this alike, and the block is the same on all
697
+ three:
698
+
699
+ | | PostgreSQL | SQLite | MySQL |
700
+ | --- | --- | --- | --- |
701
+ | `dig(:a, :b)` | `#>> '{a,b}'` | `->> '$.a.b'` | `JSON_UNQUOTE(JSON_EXTRACT(…, '$.a.b'))` |
702
+ | `dig_json(:a)` | `#> '{a}'` | `-> '$.a'` | `JSON_EXTRACT(…, '$.a')` |
703
+ | `key?(:a)` | `jsonb_exists(…, 'a')` | `json_type(…, '$.a') IS NOT NULL` | `JSON_CONTAINS_PATH(…, 'one', '$.a')` |
704
+ | `contains?(…)` | `@>` | — | `JSON_CONTAINS` |
705
+
706
+ MariaDB answers to the `mysql2` adapter and has none of `->` or `->>`, so the
707
+ MySQL family goes through the functions, which both have.
708
+
709
+ `dig` gives text everywhere. SQLite's `->>` would otherwise hand back the value
710
+ with its type, so a comparison that worked there would fail on the other two;
711
+ a number is compared through a `cast` on all three:
712
+
713
+ ```ruby
714
+ Post.where { :meta.dig(:n) == '5' }
715
+ Post.where { cast(:meta.dig(:n), 'integer') > 6 } # 'signed' on MySQL
446
716
  ```
447
717
 
448
- `.asc` and `.desc` take `.nulls_first` / `.nulls_last`. MySQL has no such
449
- syntax, but Arel emulates it there, so the resulting order is the same
450
- everywhere:
718
+ The type is the adapter's own name for it, here as everywhere `cast` is used.
719
+
720
+ `bury` sets what `dig` reads: the last argument is the value and the rest are
721
+ the path to it. The document comes back changed rather than being written
722
+ anywhere, so `update_all` is what makes it stick:
723
+
724
+ ```ruby
725
+ Post.update_all { { meta: :meta.bury(:author, :name, 'alice') } }
726
+ # SET "meta" = jsonb_set("meta", '{author,name}', '"alice"')
727
+ # ... JSON_SET("meta", '$.author.name', 'alice') elsewhere
728
+
729
+ Post.update_all { { meta: :meta.bury(:tags, ['ruby', 'sql']) } }
730
+ Post.update_all { { meta: :meta.bury(:copy, :meta.dig(:n)) } }
731
+ ```
732
+
733
+ A whole document goes in as one — an object or an array rather than the string
734
+ that spells it — which each adapter takes its own way round. `bury` is not a
735
+ Ruby method; it is the name Ruby considered for the other end of `dig`, and
736
+ SQL has no one name to borrow here, since PostgreSQL says `jsonb_set` where
737
+ the others say `JSON_SET`.
738
+
739
+ `dig_json` keeps the JSON, for a document to be dug into further or compared
740
+ whole. `contains?` has no equivalent on SQLite and raises `NotImplementedError`
741
+ there — later than the rest, since the adapter is only known when the SQL is
742
+ built. On PostgreSQL, `contains?` and `key?` want a `jsonb` column; the
743
+ `json` type carries neither operator.
744
+
745
+ A key that is not a plain name travels as itself rather than being refused:
746
+ `dig(:'odd key')` becomes `'{odd key}'` or `$."odd key"`.
747
+
748
+ ### Window functions
749
+
750
+ `over` gives a function a window, which is what turns an aggregate into a
751
+ running one and the only thing `row_number` and its kind can be used with.
752
+ The window is built by chaining, as Arel's own is:
753
+
754
+ ```ruby
755
+ Author.select { avg(:age).over.partition(:country).as(:country_average) }
756
+ # AVG("age") OVER (PARTITION BY "country") AS country_average
757
+
758
+ Author.select { row_number.over.partition(:country).order(:age.desc).as(:rank) }
759
+ # ROW_NUMBER() OVER (PARTITION BY "country" ORDER BY "age" DESC) AS rank
760
+
761
+ Author.select { count(:*).over.as(:total) } # COUNT(*) OVER () — every row
762
+ ```
763
+
764
+ `row_number`, `rank`, `dense_rank`, `percent_rank`, `cume_dist`, `ntile`,
765
+ `lag`, `lead`, `first_value`, `last_value` and `nth_value` are the functions
766
+ that say nothing without a window; each raises `ArgumentError` if `over` never
767
+ arrives, rather than reaching the database as an error there. Every adapter
768
+ that has window functions at all spells them the same way, so unlike the
769
+ scalar functions there is nothing here to translate.
770
+
771
+ A frame is a range of rows counted from the current one — negative before it,
772
+ positive after, 0 the row itself, and an open end for unbounded:
773
+
774
+ ```ruby
775
+ Post.select { sum(:likes).over.order(:created_at).rows(..0).as(:running) }
776
+ # SUM("likes") OVER (ORDER BY "created_at" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
777
+
778
+ Post.select { avg(:likes).over.order(:created_at).rows(-1..1).as(:smoothed) }
779
+ # ... ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
780
+
781
+ Post.select { sum(:likes).over.order(:created_at).rows(0..).as(:remaining) }
782
+ # ... ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
783
+ ```
784
+
785
+ `range` says `RANGE` where `rows` says `ROWS`, and a window has one frame or
786
+ none. Named windows — `WINDOW w AS (...)` — have no clause in ActiveRecord to
787
+ live in, so they are not here.
788
+
789
+ ### Aliases and ordering
790
+
791
+ `.as` gives an expression a column alias, and `.asc` / `.desc` give an
792
+ ordering its direction. The orderings take `.nulls_first` / `.nulls_last` as
793
+ well. MySQL has no such syntax, but Arel emulates it there, so the resulting
794
+ order is the same everywhere:
451
795
 
452
796
  ```ruby
453
797
  Author.order { :country.asc.nulls_last }
454
798
  ```
455
799
 
800
+ Together:
801
+
456
802
  ```ruby
457
803
  Author.
458
804
  joins(:posts) { :posts[:author_id] == :authors[:id] }.
@@ -469,21 +815,38 @@ Author.
469
815
  }
470
816
  ```
471
817
 
472
- ## Examples
818
+ ### Writing
819
+
820
+ `update_all` reads its hash the way ActiveRecord does — `update_all(likes: :likes)`
821
+ sets the column to the symbol itself. The block reads a symbol as the column it
822
+ names, as every other block here does, which is what lets the new value be
823
+ worked out from the old:
824
+
825
+ ```ruby
826
+ Post.where { :published == true }.update_all { { likes: :likes + 1 } }
827
+ # UPDATE "posts" SET "likes" = ("posts"."likes" + 1) WHERE ...
828
+
829
+ Post.update_all { { title: upper(:title), likes: case_when { :likes < 0 }.then(0).else(:likes) } }
830
+ ```
831
+
832
+ `upsert_all` takes one too, for the part that decides what happens to a row
833
+ that is already there. `excluded` is the row that could not be inserted:
834
+
835
+ ```ruby
836
+ Tally.upsert_all(rows, unique_by: :page) { { hits: :hits + excluded(:hits) } }
837
+ # ... ON CONFLICT ("page") DO UPDATE SET "hits"=("tallies"."hits" + "excluded"."hits")
838
+ ```
473
839
 
474
- `examples/` holds runnable scripts, each printing the SQL it builds and, where
475
- the result is the point, the rows that come back. All but the last run against
476
- an in-memory SQLite database and need no setup.
840
+ PostgreSQL and SQLite name that row `excluded`; MySQL spells the same thing
841
+ `VALUES(column)`, and the block comes out as whichever the adapter reads.
842
+ ActiveRecord's own `on_duplicate:` takes SQL text and nothing else, so this is
843
+ the one place the DSL writes SQL out itself rather than handing Arel a tree —
844
+ and the two cannot both be given.
477
845
 
478
- | | |
479
- | --- | --- |
480
- | `predicates.rb` | the `where` vocabulary: ranges, sets, NULL, text matching |
481
- | `subqueries.rb` | `in?` with a relation, `exists?`, scalar subqueries |
482
- | `expressions.rb` | arithmetic, aggregates, functions, `NULLS LAST` |
483
- | `complex_joins.rb` | compound `ON` clauses, outer joins, a self join |
484
- | `aggregations.rb` | `GROUP BY`, `HAVING` and aggregates across joins |
485
- | `ctes.rb` | `with` and `with_recursive` |
486
- | `postgresql.rb` | array columns, regular expressions, `ILIKE` (needs a server) |
846
+ `insert_all` has no block: its values are literals by construction.
847
+ ActiveRecord type-casts each one on the way into the `VALUES` list, so an
848
+ expression does not become SQL there it becomes nothing, silently. Use
849
+ `upsert_all` where a row's value has to be worked out.
487
850
 
488
851
  ## Performance
489
852
 
data/examples/ctes.rb CHANGED
@@ -63,7 +63,7 @@ forest =
63
63
  Category.where { :parent_id.null? }.
64
64
  select { [:id, :name, :parent_id, :id.as(:root_id), 0.as(:depth)] },
65
65
  Category.joins(:tree) { :categories[:parent_id] == :tree[:id] }.
66
- select { [:categories[:id], :categories[:name], :categories[:parent_id],
66
+ select { [:id, :name, :parent_id,
67
67
  :tree[:root_id], (:tree[:depth] + 1).as(:depth)] },
68
68
  ]
69
69
  ).from_cte(:tree).order { [:depth, :id] }
@@ -98,10 +98,10 @@ puts
98
98
  expensive =
99
99
  Category.with(pricey: Product.where { :price >= 100 }).
100
100
  joins(:pricey) { :pricey[:category_id] == :categories[:id] }.
101
- group { :categories[:id] }.
101
+ group { :id }.
102
102
  select {
103
103
  [
104
- :categories[:name].as(:category),
104
+ :name.as(:category),
105
105
  count(:pricey[:id]).as(:pricey_count),
106
106
  max(:pricey[:price]).as(:top_price),
107
107
  ]