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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f77f34a31719d6697c0d77c378392a6bffa95f033345272e981c51f3fb6f977
4
- data.tar.gz: bed64c1983fd76634d924a28a690e3b54705f9a948bf06735314ddd91bed0037
3
+ metadata.gz: 8ee6cc682434a9d10c523f5df80085c460ba44fc6fa8653304a38fbd0cf85c83
4
+ data.tar.gz: 55185eb38377c3ba4cb57136a68157531527f8ec7ea4e5cc568c95be88cdf2a1
5
5
  SHA512:
6
- metadata.gz: cd03b6d8647c0aef54927c5c68f6c7145ebfcdfa1223abc9b9650905dbaf21281b05678cd708889297a75dfc388e17b886358ebd6db445948b209d1e84d9f6f1
7
- data.tar.gz: 2d971fb7ea3f757569b3a8e0127c52693afd20a5eeb15523dd58d2747376f2e426833ef71a343c880e9d225fb9f1a37f1a3f16f1998ac96fb6341e7c5f3f8fb6
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/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
- 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
26
  `bitwise_and`, `bitwise_or`, `^`, `~`, `<<` and `>>` are SQL's bitwise
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
 
@@ -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
@@ -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