activerecord-refined 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/docs/ctes.md +2 -2
- data/docs/expressions.md +27 -13
- data/docs/functions.md +14 -10
- data/docs/index.md +1 -16
- data/docs/windows.md +2 -2
- data/examples/expressions.rb +13 -7
- data/lib/active_record/refined/ast.rb +330 -55
- data/lib/active_record/refined/dialect/mysql_compat.rb +16 -0
- data/lib/active_record/refined/dialect/mysqlish_json_functions.rb +45 -0
- data/lib/active_record/refined/dialect/oracle.rb +1 -7
- data/lib/active_record/refined/dialect/postgresql.rb +8 -0
- data/lib/active_record/refined/dialect/sql_server.rb +29 -5
- data/lib/active_record/refined/dialect/sqlite.rb +23 -0
- data/lib/active_record/refined/dialect.rb +132 -66
- data/lib/active_record/refined.rb +59 -22
- data/lib/activerecord-refined/version.rb +6 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f77f34a31719d6697c0d77c378392a6bffa95f033345272e981c51f3fb6f977
|
|
4
|
+
data.tar.gz: bed64c1983fd76634d924a28a690e3b54705f9a948bf06735314ddd91bed0037
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd03b6d8647c0aef54927c5c68f6c7145ebfcdfa1223abc9b9650905dbaf21281b05678cd708889297a75dfc388e17b886358ebd6db445948b209d1e84d9f6f1
|
|
7
|
+
data.tar.gz: 2d971fb7ea3f757569b3a8e0127c52693afd20a5eeb15523dd58d2747376f2e426833ef71a343c880e9d225fb9f1a37f1a3f16f1998ac96fb6341e7c5f3f8fb6
|
data/docs/ctes.md
CHANGED
|
@@ -18,11 +18,11 @@ Node.with_recursive(
|
|
|
18
18
|
]
|
|
19
19
|
).from_cte(:tree)
|
|
20
20
|
# WITH RECURSIVE "tree" AS (
|
|
21
|
-
# SELECT "nodes"."id", "nodes"."name", "nodes"."parent_id", 0 AS depth
|
|
21
|
+
# SELECT "nodes"."id", "nodes"."name", "nodes"."parent_id", 0 AS "depth"
|
|
22
22
|
# FROM "nodes" WHERE "nodes"."id" = 1
|
|
23
23
|
# UNION ALL
|
|
24
24
|
# SELECT "nodes"."id", "nodes"."name", "nodes"."parent_id",
|
|
25
|
-
# ("tree"."depth" + 1) AS depth
|
|
25
|
+
# ("tree"."depth" + 1) AS "depth"
|
|
26
26
|
# FROM "nodes" INNER JOIN "tree" ON "nodes"."parent_id" = "tree"."id"
|
|
27
27
|
# ) SELECT "nodes".* FROM "tree" AS "nodes"
|
|
28
28
|
```
|
data/docs/expressions.md
CHANGED
|
@@ -20,21 +20,35 @@ Item.select { greatest(20 - :quantity, 0).as(:shortfall) }
|
|
|
20
20
|
Item.where { BigDecimal("1.08") * :price > 500 }
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
`bitwise_and`, `bitwise_or`, `^`, `~`, `<<` and `>>` are SQL's bitwise
|
|
24
|
+
operations. The first two are named rather than spelled `&` and `|`, which
|
|
25
|
+
mean AND and OR between conditions and nothing else anywhere; `bitwise_xor`
|
|
26
|
+
and `bitwise_not` stand beside `^` and `~` for a reader who would rather have
|
|
27
|
+
the name:
|
|
26
28
|
|
|
27
29
|
```ruby
|
|
28
|
-
Post.where { :flags
|
|
30
|
+
Post.where { :flags.bitwise_and(4) > 0 }
|
|
29
31
|
# WHERE ("posts"."flags" & 4) > 0
|
|
30
32
|
|
|
31
|
-
Post.select {
|
|
33
|
+
Post.select { :flags.bitwise_or(4).as(:flags) }
|
|
32
34
|
Post.select { (~:flags).as(:inverted) }
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
A method binds tighter than any comparison, so nothing has to be parenthesised
|
|
38
|
+
to be compared. Each operation parenthesises itself in the SQL as well, since
|
|
39
|
+
PostgreSQL gives `&` and `|` the same precedence and reads `a | b & c` from
|
|
40
|
+
the left.
|
|
41
|
+
|
|
42
|
+
Reaching for the operator on a column says which name was meant:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Post.where { :flags & 4 }
|
|
46
|
+
# ArgumentError: & between conditions is AND; bitwise_and is SQL's bitwise operator
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Oracle has none of the operators — its one bit operation is the `BITAND`
|
|
50
|
+
function — and the block refuses every one there rather than leaving its
|
|
51
|
+
server to.
|
|
38
52
|
|
|
39
53
|
A boolean column is refused rather than taken for the one bit it is stored as.
|
|
40
54
|
MySQL and SQLite would quietly answer as `AND` would, PostgreSQL has no such
|
|
@@ -79,12 +93,12 @@ since a literal that has been sent `as` has already said it is a value:
|
|
|
79
93
|
|
|
80
94
|
```ruby
|
|
81
95
|
Node.select { [:id, value(0).as(:depth)] }
|
|
82
|
-
# SELECT "nodes"."id", 0 AS depth FROM "nodes"
|
|
96
|
+
# SELECT "nodes"."id", 0 AS "depth" FROM "nodes"
|
|
83
97
|
|
|
84
98
|
Node.select { [:id, 0.as(:depth)] } # the same thing
|
|
85
99
|
|
|
86
100
|
Post.select { [:title, "draft".as(:state)] }
|
|
87
|
-
# SELECT "posts"."title", 'draft' AS state FROM "posts"
|
|
101
|
+
# SELECT "posts"."title", 'draft' AS "state" FROM "posts"
|
|
88
102
|
```
|
|
89
103
|
|
|
90
104
|
What the shorthand does not cover, `value` still spells: `value(true)`,
|
|
@@ -98,10 +112,10 @@ that does not need it:
|
|
|
98
112
|
|
|
99
113
|
```ruby
|
|
100
114
|
Author.select { :country.when("JP").then("Japan").else("elsewhere").as(:where) }
|
|
101
|
-
# CASE "country" WHEN 'JP' THEN 'Japan' ELSE 'elsewhere' END AS where
|
|
115
|
+
# CASE "country" WHEN 'JP' THEN 'Japan' ELSE 'elsewhere' END AS "where"
|
|
102
116
|
|
|
103
117
|
Author.select { case_when { :age >= 60 }.then("senior").else("adult").as(:band) }
|
|
104
|
-
# CASE WHEN "age" >= 60 THEN 'senior' ELSE 'adult' END AS band
|
|
118
|
+
# CASE WHEN "age" >= 60 THEN 'senior' ELSE 'adult' END AS "band"
|
|
105
119
|
|
|
106
120
|
Author.select { self.case(mod(:age, 10)).when(0).then("round").else("not").as(:v) }
|
|
107
121
|
```
|
|
@@ -121,5 +135,5 @@ Author.select {
|
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
Author.select { sum(case_when { :age >= 60 }.then(1).else(0)).as(:seniors) }
|
|
124
|
-
# SUM(CASE WHEN "age" >= 60 THEN 1 ELSE 0 END) AS seniors
|
|
138
|
+
# SUM(CASE WHEN "age" >= 60 THEN 1 ELSE 0 END) AS "seniors"
|
|
125
139
|
```
|
data/docs/functions.md
CHANGED
|
@@ -96,7 +96,7 @@ written, so a case-sensitive one can be spelled exactly:
|
|
|
96
96
|
|
|
97
97
|
```ruby
|
|
98
98
|
Post.select { fn(:date_trunc, "day", :created_at).as(:day) }
|
|
99
|
-
# SELECT date_trunc('day', "posts"."created_at") AS day
|
|
99
|
+
# SELECT date_trunc('day', "posts"."created_at") AS "day"
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
`op` is the same escape hatch for operators — PostgreSQL alone has dozens
|
|
@@ -107,10 +107,10 @@ allows an operator, so a letter, a space or a quote is refused rather than
|
|
|
107
107
|
written into the SQL. Both sides take what `fn`'s arguments take — a
|
|
108
108
|
column, an expression, a value quoted by the adapter — and a value is
|
|
109
109
|
spelled in the adapter's own syntax, `to_json` saying a document, `'{a,b}'`
|
|
110
|
-
an array; a Ruby Hash or Array is refused rather than guessed at.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
under it:
|
|
110
|
+
an array; a Ruby Hash or Array is refused rather than guessed at. What it
|
|
111
|
+
gives combines with `&`, `|` and `!` as a condition does. The result is
|
|
112
|
+
parenthesized, its precedence being unknown, and so is an expression on
|
|
113
|
+
either side, so a dug value cannot be re-grouped out from under it:
|
|
114
114
|
|
|
115
115
|
```ruby
|
|
116
116
|
Post.where { op("&&", :tags, "{ruby,sql}") }
|
|
@@ -127,15 +127,19 @@ for by name, and an interpolation has a spelling that is not it: `?` and
|
|
|
127
127
|
`:name` placeholders take values quoted by the adapter, through
|
|
128
128
|
`sanitize_sql_array`. A `?` is rewritten only when there are positional binds
|
|
129
129
|
to put in it, so PostgreSQL's `?` operators can share a statement with named
|
|
130
|
-
binds, or with none. The result carries the predications and arithmetic,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
binds, or with none. The result carries the predications and arithmetic,
|
|
131
|
+
combines with `&`, `|` and `!` as a condition does, and is parenthesized
|
|
132
|
+
where it stands inside a larger expression — its precedence is whatever was
|
|
133
|
+
written — but comes out bare at the top of a select list, where parentheses
|
|
134
|
+
would refuse an alias written into the string:
|
|
134
135
|
|
|
135
136
|
```ruby
|
|
136
137
|
Post.where { sql("length(title) > ?", 10) }
|
|
137
138
|
# WHERE (length(title) > 10)
|
|
138
139
|
|
|
140
|
+
Post.where { sql("score > 0") & (:published == true) }
|
|
141
|
+
# WHERE (score > 0) AND "posts"."published" = TRUE
|
|
142
|
+
|
|
139
143
|
Post.where { sql("score + ?", 10) * 2 >= 60 }
|
|
140
144
|
# WHERE (score + 10) * 2 >= 60
|
|
141
145
|
|
|
@@ -195,7 +199,7 @@ Post.where { extract(:year, :created_at) == 2026 }
|
|
|
195
199
|
# SELECT "posts".* FROM "posts" WHERE EXTRACT(YEAR FROM "posts"."created_at") = 2026
|
|
196
200
|
|
|
197
201
|
Post.select { cast(:price, "decimal(10,2)").as(:price) }
|
|
198
|
-
# SELECT CAST("posts"."price" AS decimal(10,2)) AS price
|
|
202
|
+
# SELECT CAST("posts"."price" AS decimal(10,2)) AS "price"
|
|
199
203
|
```
|
|
200
204
|
|
|
201
205
|
A duration moves a date. Active Support's `7.days` and the rest go on the
|
data/docs/index.md
CHANGED
|
@@ -11,17 +11,6 @@ Author.
|
|
|
11
11
|
# WHERE "authors"."age" BETWEEN 20 AND 40 AND "posts"."published" = TRUE
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Inside a block, symbols denote columns of the receiver's table, and
|
|
15
|
-
`:table[:column]` denotes a qualified column. That holds in every position —
|
|
16
|
-
on the right of a comparison too, so `:age == :retirement_age` compares two
|
|
17
|
-
columns. A value is written as its literal, an enum's as its string; a symbol
|
|
18
|
-
naming no column of the model is refused rather than compared against nothing
|
|
19
|
-
anyone meant.
|
|
20
|
-
|
|
21
|
-
**[Try it in your browser](https://shugo.github.io/activerecord-refined/)** —
|
|
22
|
-
Ruby 4.1, Active Record, SQLite and PostgreSQL run in the page, so the examples
|
|
23
|
-
build real SQL and return real rows without a `ruby-master` build of your own.
|
|
24
|
-
|
|
25
14
|
## Reference
|
|
26
15
|
|
|
27
16
|
- {ActiveRecord::Refined::BlockSyntax BlockSyntax} — what a symbol answers to
|
|
@@ -50,7 +39,7 @@ differ:
|
|
|
50
39
|
- {file:docs/functions.md Aggregates and functions} — `count`, `filter`,
|
|
51
40
|
`string_agg`, the scalar functions, the clock, durations, `extract` and
|
|
52
41
|
`cast`.
|
|
53
|
-
- {file:docs/expressions.md Expressions} — arithmetic, the bitwise
|
|
42
|
+
- {file:docs/expressions.md Expressions} — arithmetic, the bitwise operations,
|
|
54
43
|
`CASE`.
|
|
55
44
|
- {file:docs/json.md JSON} — `dig`, `bury`, `except`, `key?`, `keys`, the
|
|
56
45
|
JSON aggregates, and where the adapters' JSON types part.
|
|
@@ -64,7 +53,3 @@ differ:
|
|
|
64
53
|
- {file:docs/writing.md Writing} — `update_all` and `upsert_all`.
|
|
65
54
|
- {file:docs/time_zones.md Time zones} — what a block converts and what it
|
|
66
55
|
leaves to the session.
|
|
67
|
-
|
|
68
|
-
The repository's README, on
|
|
69
|
-
[GitHub](https://github.com/shugo/activerecord-refined), has the history,
|
|
70
|
-
the requirements, and how the tests and the release are run.
|
data/docs/windows.md
CHANGED
|
@@ -7,10 +7,10 @@ The window is built by chaining, as Arel's own is:
|
|
|
7
7
|
|
|
8
8
|
```ruby
|
|
9
9
|
Author.select { avg(:age).over.partition(:country).as(:country_average) }
|
|
10
|
-
# AVG("age") OVER (PARTITION BY "country") AS country_average
|
|
10
|
+
# AVG("age") OVER (PARTITION BY "country") AS "country_average"
|
|
11
11
|
|
|
12
12
|
Author.select { row_number.over.partition(:country).order(:age.desc).as(:rank) }
|
|
13
|
-
# ROW_NUMBER() OVER (PARTITION BY "country" ORDER BY "age" DESC) AS rank
|
|
13
|
+
# ROW_NUMBER() OVER (PARTITION BY "country" ORDER BY "age" DESC) AS "rank"
|
|
14
14
|
|
|
15
15
|
Author.select { count(:*).over.as(:total) } # COUNT(*) OVER () — every row
|
|
16
16
|
```
|
data/examples/expressions.rb
CHANGED
|
@@ -82,12 +82,12 @@ show("a due date a month on",
|
|
|
82
82
|
LineItem.select { [:sku, (:ordered_on + 1.month).as(:due_on)] }.
|
|
83
83
|
map { |i| [i.sku, i.due_on] })
|
|
84
84
|
|
|
85
|
-
# The bitwise
|
|
86
|
-
#
|
|
87
|
-
#
|
|
85
|
+
# The bitwise operations. AND and OR are what & and | mean, so those two go
|
|
86
|
+
# by name here; a method binds tighter than any comparison, and the expression
|
|
87
|
+
# parenthesises itself in the SQL so the adapter cannot regroup it.
|
|
88
88
|
show("a bit test in a condition",
|
|
89
|
-
LineItem.where { :flags
|
|
90
|
-
LineItem.where { :flags
|
|
89
|
+
LineItem.where { :flags.bitwise_and(4) > 0 },
|
|
90
|
+
LineItem.where { :flags.bitwise_and(4) > 0 }.pluck(:sku))
|
|
91
91
|
|
|
92
92
|
# XOR is the one the three adapters do not share. SQLite has none, so it gets
|
|
93
93
|
# the two operations XOR is made of; PostgreSQL would say #, MySQL ^.
|
|
@@ -99,9 +99,9 @@ show("xor, spelled the way the adapter spells it",
|
|
|
99
99
|
# the three adapters would quietly answer as AND does and the third has no
|
|
100
100
|
# such operator, so the block refuses instead.
|
|
101
101
|
begin
|
|
102
|
-
LineItem.where { :flags
|
|
102
|
+
LineItem.where { :flags.bitwise_and(:price == 1) }
|
|
103
103
|
rescue ArgumentError => e
|
|
104
|
-
puts "--- a condition is not an operand of
|
|
104
|
+
puts "--- a condition is not an operand of a bitwise operation ---"
|
|
105
105
|
puts " #{e.message}"
|
|
106
106
|
puts
|
|
107
107
|
end
|
|
@@ -184,6 +184,12 @@ show("sql, for what neither fn nor op can spell",
|
|
|
184
184
|
LineItem.where { sql("price % ?", 100) == 0 },
|
|
185
185
|
LineItem.where { sql("price % ?", 100) == 0 }.pluck(:sku))
|
|
186
186
|
|
|
187
|
+
# What sql or op gives is a condition when what it holds is one, and
|
|
188
|
+
# combines with & | ! like any other.
|
|
189
|
+
show("an sql condition combined with a built one",
|
|
190
|
+
LineItem.where { sql("quantity > 4") & (:category == "tools") },
|
|
191
|
+
LineItem.where { sql("quantity > 4") & (:category == "tools") }.pluck(:sku))
|
|
192
|
+
|
|
187
193
|
# A string sent `as` is a value, like a number.
|
|
188
194
|
show("a string literal in a select list",
|
|
189
195
|
LineItem.select { [:sku, "listed".as(:state)] },
|