activerecord-refined 0.11.0 → 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.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/README.md +19 -22
- data/activerecord-refined.gemspec +4 -3
- data/docs/conditions.md +3 -1
- data/docs/expressions.md +10 -7
- data/docs/index.md +1 -1
- data/examples/expressions.rb +8 -8
- data/lib/active_record/refined/ast/arithmetics.rb +137 -0
- data/lib/active_record/refined/ast/conditions.rb +451 -0
- data/lib/active_record/refined/ast/expressions.rb +304 -0
- data/lib/active_record/refined/ast/functions.rb +254 -0
- data/lib/active_record/refined/ast/grouping.rb +63 -0
- data/lib/active_record/refined/ast/json.rb +530 -0
- data/lib/active_record/refined/ast/node.rb +298 -0
- data/lib/active_record/refined/ast/ordering.rb +104 -0
- data/lib/active_record/refined/ast/predications.rb +384 -0
- data/lib/active_record/refined/ast/windows.rb +126 -0
- data/lib/active_record/refined/ast.rb +10 -2610
- data/lib/active_record/refined/block_context.rb +671 -0
- data/lib/active_record/refined/block_syntax.rb +128 -0
- data/lib/active_record/refined/query_methods.rb +444 -0
- data/lib/active_record/refined/version.rb +8 -0
- data/lib/active_record/refined/writes.rb +72 -0
- data/lib/active_record/refined.rb +7 -1307
- data/lib/activerecord-refined.rb +0 -4
- metadata +35 -5
- data/lib/activerecord-refined/version.rb +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ee6cc682434a9d10c523f5df80085c460ba44fc6fa8653304a38fbd0cf85c83
|
|
4
|
+
data.tar.gz: 55185eb38377c3ba4cb57136a68157531527f8ec7ea4e5cc568c95be88cdf2a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5dc45baffd355d00a918c144ae802c4f8f5ab2288b792bf1660f16978d65fd909e449b9c65a14fff834a89011eb632b2428c1fd9abaf5bdc5705f557e336551a
|
|
7
|
+
data.tar.gz: b6b35bebfc29eaba5c793268fc7e343aed5323e7078c81a25a1add336969e415b0951fe6d92bc8abe4e513151bfe3c94acadefd2d59a5a320fd2bb2f5f9778bc
|
data/.yardopts
CHANGED
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# activerecord-refined
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/activerecord-refined)
|
|
4
4
|
[](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
|
-
|
|
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.
|
|
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 |
|
|
101
|
-
| range (BETWEEN) | — |
|
|
102
|
-
| LIKE |
|
|
103
|
-
| compound AND/OR |
|
|
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,
|
|
110
|
-
| compound AND/OR |
|
|
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
|
|
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 (
|
|
127
|
-
simple-equality block,
|
|
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 "
|
|
6
|
+
require "active_record/refined/version"
|
|
7
7
|
|
|
8
8
|
Gem::Specification.new do |gem|
|
|
9
9
|
gem.name = "activerecord-refined"
|
|
10
|
-
gem.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.
|
|
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/expressions.md
CHANGED
|
@@ -9,15 +9,18 @@ Item.where { :price * :quantity > 1000 }
|
|
|
9
9
|
Item.select { sum(:price * :quantity).as(:total) }
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
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
26
|
`bitwise_and`, `bitwise_or`, `^`, `~`, `<<` and `>>` are SQL's bitwise
|
data/docs/index.md
CHANGED
data/examples/expressions.rb
CHANGED
|
@@ -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
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
show("the number on the left
|
|
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
|
|
@@ -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
|