activerecord-refined 0.10.2 → 0.12.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/README.md +19 -22
  4. data/activerecord-refined.gemspec +4 -3
  5. data/docs/conditions.md +3 -1
  6. data/docs/ctes.md +2 -2
  7. data/docs/expressions.md +37 -20
  8. data/docs/functions.md +14 -10
  9. data/docs/index.md +2 -17
  10. data/docs/windows.md +2 -2
  11. data/examples/expressions.rb +21 -15
  12. data/lib/active_record/refined/ast/arithmetics.rb +137 -0
  13. data/lib/active_record/refined/ast/conditions.rb +451 -0
  14. data/lib/active_record/refined/ast/expressions.rb +304 -0
  15. data/lib/active_record/refined/ast/functions.rb +254 -0
  16. data/lib/active_record/refined/ast/grouping.rb +63 -0
  17. data/lib/active_record/refined/ast/json.rb +530 -0
  18. data/lib/active_record/refined/ast/node.rb +298 -0
  19. data/lib/active_record/refined/ast/ordering.rb +104 -0
  20. data/lib/active_record/refined/ast/predications.rb +384 -0
  21. data/lib/active_record/refined/ast/windows.rb +126 -0
  22. data/lib/active_record/refined/ast.rb +39 -2364
  23. data/lib/active_record/refined/block_context.rb +671 -0
  24. data/lib/active_record/refined/block_syntax.rb +128 -0
  25. data/lib/active_record/refined/dialect/mysql_compat.rb +16 -0
  26. data/lib/active_record/refined/dialect/mysqlish_json_functions.rb +45 -0
  27. data/lib/active_record/refined/dialect/oracle.rb +1 -7
  28. data/lib/active_record/refined/dialect/postgresql.rb +8 -0
  29. data/lib/active_record/refined/dialect/sql_server.rb +29 -5
  30. data/lib/active_record/refined/dialect/sqlite.rb +23 -0
  31. data/lib/active_record/refined/dialect.rb +132 -66
  32. data/lib/active_record/refined/query_methods.rb +444 -0
  33. data/lib/{activerecord-refined → active_record/refined}/version.rb +3 -2
  34. data/lib/active_record/refined/writes.rb +72 -0
  35. data/lib/active_record/refined.rb +7 -1270
  36. data/lib/activerecord-refined.rb +0 -4
  37. metadata +36 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9270b5bf04c083f8cefdd5c669810a63a94a2073c1cae49ba28027693ee5b684
4
- data.tar.gz: 23f7e622c8b6b1e85cf670798b705aec8e57c0149315c5fb50699fc95d10ee18
3
+ metadata.gz: 8ee6cc682434a9d10c523f5df80085c460ba44fc6fa8653304a38fbd0cf85c83
4
+ data.tar.gz: 55185eb38377c3ba4cb57136a68157531527f8ec7ea4e5cc568c95be88cdf2a1
5
5
  SHA512:
6
- metadata.gz: dcd5cbfab0c7d60498b21a25240f29ae224aa6de7b5fd0a37c627ee10e0f642ed4bb66740734bca03f64f3b231032e273163d68fc9de59b3c07447bef1f69eab
7
- data.tar.gz: e701353709c2f8dfb7ebc38a9bdfb9e8b56c940579f139beed543f20391cf8d50166391c3de1718d85797185f4bcfb0cec3a3f4a2d91948c1344527f0ea18e6e
6
+ metadata.gz: 5dc45baffd355d00a918c144ae802c4f8f5ab2288b792bf1660f16978d65fd909e449b9c65a14fff834a89011eb632b2428c1fd9abaf5bdc5705f557e336551a
7
+ data.tar.gz: b6b35bebfc29eaba5c793268fc7e343aed5323e7078c81a25a1add336969e415b0951fe6d92bc8abe4e513151bfe3c94acadefd2d59a5a320fd2bb2f5f9778bc
data/.yardopts CHANGED
@@ -1,6 +1,6 @@
1
1
  --markup markdown
2
2
  --no-private
3
- --title "ActiveRecord::Refined"
3
+ --title "activerecord-refined"
4
4
  --readme docs/index.md
5
5
  lib/**/*.rb
6
6
  -
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ActiveRecord::Refined
1
+ # activerecord-refined
2
2
 
3
3
  [![gem](https://img.shields.io/gem/v/activerecord-refined.svg)](https://rubygems.org/gems/activerecord-refined)
4
4
  [![test](https://github.com/shugo/activerecord-refined/actions/workflows/test.yml/badge.svg)](https://github.com/shugo/activerecord-refined/actions/workflows/test.yml)
@@ -14,21 +14,12 @@ Author.
14
14
  # WHERE "authors"."age" BETWEEN 20 AND 40 AND "posts"."published" = TRUE
15
15
  ```
16
16
 
17
- Inside a block, symbols denote columns of the receiver's table, and
18
- `:table[:column]` denotes a qualified column. That holds in every position —
19
- on the right of a comparison too, so `:age == :retirement_age` compares two
20
- columns. A value is written as its literal, an enum's as its string; a symbol
21
- naming no column of the model is refused rather than compared against nothing
22
- anyone meant.
23
-
24
- **[Try it in your browser](https://shugo.github.io/activerecord-refined/)** —
25
- Ruby 4.1, Active Record, SQLite and PostgreSQL run in the page, so the examples
26
- build real SQL and return real rows without a `ruby-master` build of your own.
17
+ **[Try it in your browser](https://shugo.github.io/activerecord-refined/)**
27
18
 
28
19
  ## Requirements
29
20
 
30
21
  * Ruby 4.1 or later (for `Proc#refined`; not released yet, so a `ruby-master` build is needed for now)
31
- * Active Record 7.0 or later
22
+ * Active Record 7.2 or later
32
23
 
33
24
  ## Installation
34
25
 
@@ -92,28 +83,28 @@ block DSL and through Active Record's other argument styles. Only query
92
83
  construction (through `to_sql`) is measured — every style produces the same
93
84
  SQL, so execution costs the same regardless.
94
85
 
95
- Queries built per second (ruby 4.1.0dev, Active Record 8.1.3, one machine —
86
+ Queries built per second (ruby 4.1.0dev, Active Record 8.1.3.1, one machine —
96
87
  treat the ratios, not the absolute numbers, as the result):
97
88
 
98
89
  | query | string | arel | block (this gem) | hash | relation and/or |
99
90
  | --- | --- | --- | --- | --- | --- |
100
- | simple equality | 42.5k | 42.0k | 37.7k | 31.5k | — |
101
- | range (BETWEEN) | — | 34.6k | 32.7k | 24.8k | — |
102
- | LIKE | 41.5k | 41.0k | 36.8k | — | — |
103
- | compound AND/OR | 34.4k | 27.7k | 24.4k | — | 11.9k |
91
+ | simple equality | 40.9k | 40.5k | 36.1k | 31.5k | — |
92
+ | range (BETWEEN) | — | 33.7k | 31.8k | 24.7k | — |
93
+ | LIKE | 40.1k | 39.9k | 36.4k | — | — |
94
+ | compound AND/OR | 32.8k | 26.7k | 23.4k | — | 11.4k |
104
95
 
105
96
  Allocated memory per built query:
106
97
 
107
98
  | query | arel | block (this gem) | hash | string | relation and/or |
108
99
  | --- | --- | --- | --- | --- | --- |
109
- | simple equality | 2,600 B | 2,832 B | 3,328 B | 3,448 B | — |
110
- | compound AND/OR | 3,208 B | 3,584 B | — | 4,680 B | 9,120 B |
100
+ | simple equality | 2,176 B | 2,392 B | 2,576 B | 2,976 B | — |
101
+ | compound AND/OR | 2,744 B | 3,104 B | — | 4,144 B | 7,152 B |
111
102
 
112
103
  In short: the block DSL is 6–13% slower than hand-written Arel (which it
113
104
  compiles to), a little faster than hash conditions, and both faster and
114
105
  leaner than `where(...).and(where(...).or(where(...)))` relation chains,
115
106
  which pay for structural-compatibility checks and relation copies. The
116
- `Proc#refined` call itself costs about 150 ns of the ~25 μs build — the
107
+ `Proc#refined` call itself costs about 170 ns of the ~28 μs build — the
117
108
  re-interpretation of the block is not where the time goes. Against a
118
109
  database round trip of tens to hundreds of microseconds, none of these
119
110
  differences are visible in an application.
@@ -123,8 +114,8 @@ under the refinements, `Proc#refined` deep-copies its instruction sequence,
123
114
  nested blocks included. The copy is made lazily on the refined proc's first
124
115
  call and memoized per block and refinement list for the life of the process,
125
116
  so it is paid once per `where { ... }` call site, not per query — the
126
- benchmark measures the copy at the size of the original (568 bytes for the
127
- simple-equality block, 888 bytes for the compound one), and a thousand
117
+ benchmark measures the copy at the size of the original (552 bytes for the
118
+ simple-equality block, 872 bytes for the compound one), and a thousand
128
119
  further calls from the same call site copy nothing. Steady state, an
129
120
  application holds one extra copy of each distinct query block's bytecode:
130
121
  a few hundred bytes per call site. "Per call site" assumes blocks compiled
@@ -187,6 +178,12 @@ CI runs one job per adapter with the servers as service containers, Oracle
187
178
  and SQL Server included — those two have no local server here and run on CI
188
179
  alone, their clients opted in the same way.
189
180
 
181
+ Coverage is opt-in: `COVERAGE=1 rake test` writes an HTML report to
182
+ `coverage/`, and `COVERAGE=1 rake test:all` merges the adapters' runs into
183
+ one — the number worth reading, since a dialect's lines are reached only by
184
+ its own adapter's leg. The Oracle and SQL Server dialects stay mostly
185
+ uncovered locally for the same reason their tests run on CI alone.
186
+
190
187
  ## Releasing
191
188
 
192
189
  Pushing a `v*` tag runs `.github/workflows/push_gem.yml`, which builds the gem
@@ -3,11 +3,11 @@
3
3
 
4
4
  lib = File.expand_path("../lib", __FILE__)
5
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
- require "activerecord-refined/version"
6
+ require "active_record/refined/version"
7
7
 
8
8
  Gem::Specification.new do |gem|
9
9
  gem.name = "activerecord-refined"
10
- gem.version = Activerecord::Refined::VERSION
10
+ gem.version = ActiveRecord::Refined::VERSION
11
11
  gem.authors = ["Shugo Maeda"]
12
12
  gem.email = ["shugo@ruby-lang.org"]
13
13
  gem.description = "Adding clean and powerful query syntax on Active Record using refinements"
@@ -25,10 +25,11 @@ Gem::Specification.new do |gem|
25
25
  # ruby-master builds, which sort before the 4.1.0 release.
26
26
  gem.required_ruby_version = ">= 4.1.0.dev"
27
27
 
28
- gem.add_dependency "activerecord", [">= 7.0"]
28
+ gem.add_dependency "activerecord", [">= 7.2"]
29
29
  gem.add_development_dependency "sqlite3", [">= 0"]
30
30
  gem.add_development_dependency "minitest", [">= 0"]
31
31
  gem.add_development_dependency "rake", [">= 0"]
32
+ gem.add_development_dependency "simplecov", [">= 0"]
32
33
  # What cuts a release: `bump patch --tag`, then push with --follow-tags.
33
34
  gem.add_development_dependency "bump", [">= 0"]
34
35
  # What benchmark/query_building.rb measures with.
data/docs/conditions.md CHANGED
@@ -196,7 +196,9 @@ Author.where { !:country.null? } # NOT (country IS NULL)
196
196
 
197
197
  Combine predicates with `&`, `|` and `!`. Ruby's operator precedence makes the
198
198
  parentheses around each comparison necessary, though the `?` methods above need
199
- none:
199
+ none. Leaving them off is mostly refused, but beside `true`, `false` or `nil`
200
+ it is Ruby's own `&` that runs, and the condition to its right is lost without
201
+ a word: `:active == true & cond` is `:active == true`.
200
202
 
201
203
  ```ruby
202
204
  Author.where { (:age >= 18) & ((:country == "JP") | (:country == "US")) }
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
@@ -9,32 +9,49 @@ Item.where { :price * :quantity > 1000 }
9
9
  Item.select { sum(:price * :quantity).as(:total) }
10
10
  ```
11
11
 
12
- The number may stand on the left — only a column or an expression on the
13
- right builds a query, so Ruby's own arithmetic is untouched — and
14
- `BigDecimal` is a number here, being what a decimal column's values are,
15
- quoted as the exact decimal on either side. A `Rational` is refused: no
16
- decimal spells `1/3r` exactly, and `to_d` is what says the decimal meant.
12
+ A number may stand on the left of an expression, and Ruby's own arithmetic
13
+ is untouched. Before a bare column, though, the number is written with
14
+ `value`: to Ruby, `20 - :quantity` is a number minus a symbol, and the number
15
+ has no way to know the symbol is a column. `BigDecimal` is a number here,
16
+ being what a decimal column's values are, quoted as the exact decimal on
17
+ either side. A `Rational` is refused: no decimal spells `1/3r` exactly, and
18
+ `to_d` is what says the decimal meant.
17
19
 
18
20
  ```ruby
19
- Item.select { greatest(20 - :quantity, 0).as(:shortfall) }
20
- Item.where { BigDecimal("1.08") * :price > 500 }
21
+ Item.select { greatest(value(20) - :quantity, 0).as(:shortfall) }
22
+ Item.select { (100 - (:price * :quantity)).as(:headroom) }
23
+ Item.where { :price * BigDecimal("1.08") > 500 }
21
24
  ```
22
25
 
23
- `&`, `|`, `^`, `~`, `<<` and `>>` are SQL's bitwise operators. Between
24
- conditions `&` and `|` are AND and OR, and that is where they are defined,
25
- which leaves them free to mean here what SQL means by them:
26
+ `bitwise_and`, `bitwise_or`, `^`, `~`, `<<` and `>>` are SQL's bitwise
27
+ operations. The first two are named rather than spelled `&` and `|`, which
28
+ mean AND and OR between conditions and nothing else anywhere; `bitwise_xor`
29
+ and `bitwise_not` stand beside `^` and `~` for a reader who would rather have
30
+ the name:
26
31
 
27
32
  ```ruby
28
- Post.where { :flags & 4 > 0 }
33
+ Post.where { :flags.bitwise_and(4) > 0 }
29
34
  # WHERE ("posts"."flags" & 4) > 0
30
35
 
31
- Post.select { (:flags | 4).as(:flags) }
36
+ Post.select { :flags.bitwise_or(4).as(:flags) }
32
37
  Post.select { (~:flags).as(:inverted) }
33
38
  ```
34
39
 
35
- Each parenthesises itself, which is what keeps Ruby's grouping: PostgreSQL
36
- gives `&` and `|` the same precedence and reads `a | b & c` from the left,
37
- where Ruby reads the `&` first.
40
+ A method binds tighter than any comparison, so nothing has to be parenthesised
41
+ to be compared. Each operation parenthesises itself in the SQL as well, since
42
+ PostgreSQL gives `&` and `|` the same precedence and reads `a | b & c` from
43
+ the left.
44
+
45
+ Reaching for the operator on a column says which name was meant:
46
+
47
+ ```ruby
48
+ Post.where { :flags & 4 }
49
+ # ArgumentError: & between conditions is AND; bitwise_and is SQL's bitwise operator
50
+ ```
51
+
52
+ Oracle has none of the operators — its one bit operation is the `BITAND`
53
+ function — and the block refuses every one there rather than leaving its
54
+ server to.
38
55
 
39
56
  A boolean column is refused rather than taken for the one bit it is stored as.
40
57
  MySQL and SQLite would quietly answer as `AND` would, PostgreSQL has no such
@@ -79,12 +96,12 @@ since a literal that has been sent `as` has already said it is a value:
79
96
 
80
97
  ```ruby
81
98
  Node.select { [:id, value(0).as(:depth)] }
82
- # SELECT "nodes"."id", 0 AS depth FROM "nodes"
99
+ # SELECT "nodes"."id", 0 AS "depth" FROM "nodes"
83
100
 
84
101
  Node.select { [:id, 0.as(:depth)] } # the same thing
85
102
 
86
103
  Post.select { [:title, "draft".as(:state)] }
87
- # SELECT "posts"."title", 'draft' AS state FROM "posts"
104
+ # SELECT "posts"."title", 'draft' AS "state" FROM "posts"
88
105
  ```
89
106
 
90
107
  What the shorthand does not cover, `value` still spells: `value(true)`,
@@ -98,10 +115,10 @@ that does not need it:
98
115
 
99
116
  ```ruby
100
117
  Author.select { :country.when("JP").then("Japan").else("elsewhere").as(:where) }
101
- # CASE "country" WHEN 'JP' THEN 'Japan' ELSE 'elsewhere' END AS where
118
+ # CASE "country" WHEN 'JP' THEN 'Japan' ELSE 'elsewhere' END AS "where"
102
119
 
103
120
  Author.select { case_when { :age >= 60 }.then("senior").else("adult").as(:band) }
104
- # CASE WHEN "age" >= 60 THEN 'senior' ELSE 'adult' END AS band
121
+ # CASE WHEN "age" >= 60 THEN 'senior' ELSE 'adult' END AS "band"
105
122
 
106
123
  Author.select { self.case(mod(:age, 10)).when(0).then("round").else("not").as(:v) }
107
124
  ```
@@ -121,5 +138,5 @@ Author.select {
121
138
  }
122
139
 
123
140
  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
141
+ # SUM(CASE WHEN "age" >= 60 THEN 1 ELSE 0 END) AS "seniors"
125
142
  ```
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. The
111
- result is parenthesized, its precedence being unknown, and so is an
112
- expression on either side, so a dug value cannot be re-grouped out from
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, and
131
- is parenthesized where it stands inside a larger expression — its precedence
132
- is whatever was written — but comes out bare at the top of a select list,
133
- where parentheses would refuse an alias written into the string:
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
@@ -1,4 +1,4 @@
1
- # ActiveRecord::Refined
1
+ # activerecord-refined
2
2
 
3
3
  Adding clean and powerful query syntax on Active Record using refinements.
4
4
 
@@ -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 operators,
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
  ```
@@ -58,17 +58,17 @@ show("an aggregate over an expression",
58
58
  LineItem.select { sum(:price * :quantity).as(:total) },
59
59
  LineItem.select { sum(:price * :quantity).as(:total) }.first.total)
60
60
 
61
- # The number may stand on the left; only a column or an expression on the
62
- # right builds a query, so Ruby's own arithmetic is untouched. BigDecimal
63
- # is a number here too -- what a decimal column's values are.
64
- show("the number on the left, and a BigDecimal",
65
- LineItem.select { [:sku, (12 - :quantity).as(:to_the_dozen)] },
66
- LineItem.select { [:sku, (12 - :quantity).as(:to_the_dozen)] }.
61
+ # A number may stand on the left of an expression; before a bare column it
62
+ # is written with value(). BigDecimal is a number here too -- what a
63
+ # decimal column's values are.
64
+ show("the number on the left",
65
+ LineItem.select { [:sku, (value(12) - :quantity).as(:to_the_dozen)] },
66
+ LineItem.select { [:sku, (value(12) - :quantity).as(:to_the_dozen)] }.
67
67
  map { |i| [i.sku, i.to_the_dozen] })
68
68
 
69
69
  show("a tax through BigDecimal, exact on the wire",
70
- LineItem.select { [:sku, (BigDecimal("1.1") * :price).as(:taxed)] },
71
- LineItem.select { [:sku, (BigDecimal("1.1") * :price).as(:taxed)] }.
70
+ LineItem.select { [:sku, (BigDecimal("1.1") * (:price * :quantity)).as(:taxed)] },
71
+ LineItem.select { [:sku, (BigDecimal("1.1") * (:price * :quantity)).as(:taxed)] }.
72
72
  map { |i| [i.sku, i.taxed] })
73
73
 
74
74
  # A duration moves a date. Each adapter spells the move its own way; SQLite
@@ -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 operators. & and | are AND and OR between conditions, which is
86
- # what leaves them free here. Each expression parenthesises itself, so the
87
- # grouping is Ruby's rather than the adapter's.
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 & 4 > 0 },
90
- LineItem.where { :flags & 4 > 0 }.pluck(:sku))
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 & (:price == 1) }
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)] },
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Refined
5
+ module AST
6
+ # The arithmetic and the bitwise operations on a column or an
7
+ # expression. Ruby puts the operators above the comparisons, so
8
+ # `:price * :quantity > 100` groups the way it reads. A number may
9
+ # stand on the left of an expression, `20 - (:a + :b)`, but before a
10
+ # bare column it is written as a value: `value(20) - :quantity`.
11
+ #
12
+ # Bitwise AND and OR are named, not spelled `&` and `|`: those two are
13
+ # AND and OR between conditions and mean nothing else anywhere. The
14
+ # rest keep their operators, with {#bitwise_xor} and {#bitwise_not}
15
+ # beside `^` and `~` for a reader who would rather have the name.
16
+ # Oracle, which has no bitwise operators at all, refuses every one.
17
+ #
18
+ # @example
19
+ # LineItem.where { :price * :quantity > 1000 }
20
+ # LineItem.select { :flags.bitwise_and(4).as(:featured) }
21
+ #
22
+ # Arithmetic builders shared by symbols, qualified columns and
23
+ # expressions. Imported into the Symbol refinement like Predications,
24
+ # so every method must be defined with def.
25
+ module Arithmetics
26
+ # `+`; with an Active Support duration on the right, a date moved: `:due_on + 3.days`.
27
+ # @return [AST::Arithmetic]
28
+ def +(other)
29
+ Arithmetic.new(self, :+, other)
30
+ end
31
+
32
+ # `-`; with a duration on the right, a date moved back.
33
+ # @return [AST::Arithmetic]
34
+ def -(other)
35
+ Arithmetic.new(self, :-, other)
36
+ end
37
+
38
+ # `*`.
39
+ # @return [AST::Arithmetic]
40
+ def *(other)
41
+ Arithmetic.new(self, :*, other)
42
+ end
43
+
44
+ # `/`.
45
+ # @return [AST::Arithmetic]
46
+ def /(other)
47
+ Arithmetic.new(self, :/, other)
48
+ end
49
+
50
+ # `&` is AND, and a column is not a condition, so this refuses:
51
+ # {#bitwise_and} is the SQL operator, and `.true?` makes a boolean
52
+ # column a condition.
53
+ # @raise [ArgumentError]
54
+ def &(other)
55
+ AST.refuse_logical(:&, "AND", "bitwise_and", other)
56
+ end
57
+
58
+ # `|` is OR, and refuses here as `&` does.
59
+ # @raise [ArgumentError]
60
+ def |(other)
61
+ AST.refuse_logical(:|, "OR", "bitwise_or", other)
62
+ end
63
+
64
+ # Bitwise AND: `"flags" & 4`. It is spelled as a name because `&`
65
+ # is AND, and a method binds tighter than any comparison, so nothing
66
+ # has to be parenthesised to be compared.
67
+ # @return [AST::Bitwise]
68
+ # @example
69
+ # Post.where { :flags.bitwise_and(4) > 0 }
70
+ def bitwise_and(other)
71
+ Bitwise.new(self, :&, other)
72
+ end
73
+
74
+ # Bitwise OR.
75
+ # @return [AST::Bitwise]
76
+ def bitwise_or(other)
77
+ Bitwise.new(self, :|, other)
78
+ end
79
+
80
+ # Bitwise XOR: `#` on PostgreSQL, `^` on MySQL, and the two operations it is made of on SQLite.
81
+ # @return [AST::Bitwise]
82
+ def ^(other)
83
+ Bitwise.new(self, :^, other)
84
+ end
85
+
86
+ # {#^} under a name.
87
+ # @return [AST::Bitwise]
88
+ def bitwise_xor(other)
89
+ Bitwise.new(self, :^, other)
90
+ end
91
+
92
+ # A shift left.
93
+ # @return [AST::Bitwise]
94
+ def <<(other)
95
+ Bitwise.new(self, :<<, other)
96
+ end
97
+
98
+ # A shift right.
99
+ # @return [AST::Bitwise]
100
+ def >>(other)
101
+ Bitwise.new(self, :>>, other)
102
+ end
103
+
104
+ # Bitwise NOT.
105
+ # @return [AST::BitwiseNot]
106
+ def ~
107
+ BitwiseNot.new(self)
108
+ end
109
+
110
+ # {#~} under a name. `!` is not the one to reach for: it is Ruby's
111
+ # own on a column and answers `false`, which no query ever wanted.
112
+ # @return [AST::BitwiseNot]
113
+ def bitwise_not
114
+ BitwiseNot.new(self)
115
+ end
116
+ end
117
+
118
+ # The named bitwise operations for a number on the left, imported into
119
+ # the numeric refinements: 4.bitwise_and(:flags). The operators reach
120
+ # an expression on the right through {Node#coerce} instead.
121
+ # @private
122
+ module NumericArithmetics
123
+ def bitwise_and(other)
124
+ Bitwise.new(self, :&, other)
125
+ end
126
+
127
+ def bitwise_or(other)
128
+ Bitwise.new(self, :|, other)
129
+ end
130
+
131
+ def bitwise_xor(other)
132
+ Bitwise.new(self, :^, other)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end