activerecord-refined 0.3.0 → 0.3.2
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/.github/workflows/push_gem.yml +45 -0
- data/.github/workflows/test.yml +23 -0
- data/.gitignore +2 -0
- data/Gemfile +8 -0
- data/README.md +104 -8
- data/Rakefile +17 -0
- data/activerecord-refined.gemspec +1 -1
- data/examples/aggregations.rb +91 -0
- data/examples/complex_joins.rb +69 -0
- data/lib/active_record/refined/ast.rb +171 -54
- data/lib/active_record/refined.rb +1 -7
- data/lib/activerecord-refined/version.rb +1 -1
- data/test/test_block_syntax.rb +217 -72
- data/test/test_helper.rb +85 -2
- metadata +6 -2
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
module ActiveRecord
|
|
2
2
|
module Refined
|
|
3
3
|
module AST
|
|
4
|
+
# Predicate builders shared by symbols, qualified columns and
|
|
5
|
+
# expressions. Imported into the Symbol refinement with
|
|
6
|
+
# Refinement#import_methods, so every method must be defined with def.
|
|
7
|
+
module Predications
|
|
8
|
+
def ==(other)
|
|
9
|
+
Comparison.new(self, :==, other)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def !=(other)
|
|
13
|
+
Comparison.new(self, :!=, other)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def >(other)
|
|
17
|
+
Comparison.new(self, :>, other)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def >=(other)
|
|
21
|
+
Comparison.new(self, :>=, other)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def <(other)
|
|
25
|
+
Comparison.new(self, :<, other)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def <=(other)
|
|
29
|
+
Comparison.new(self, :<=, other)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def =~(pattern)
|
|
33
|
+
Match.new(self, pattern)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def !~(pattern)
|
|
37
|
+
Match.new(self, pattern, negated: true)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def null?
|
|
41
|
+
Comparison.new(self, :==, nil)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def in?(values)
|
|
45
|
+
In.new(self, values)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def between?(min, max)
|
|
49
|
+
In.new(self, min..max)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def like?(pattern)
|
|
53
|
+
Like.new(self, pattern)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def start_with?(prefix)
|
|
57
|
+
Like.new(self, "#{Like.escape(prefix)}%", Like::ESCAPE)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def end_with?(suffix)
|
|
61
|
+
Like.new(self, "%#{Like.escape(suffix)}", Like::ESCAPE)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def include?(substring)
|
|
65
|
+
Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
4
69
|
class Node
|
|
5
70
|
def to_arel(table)
|
|
6
71
|
raise ScriptError, "subclass must override this method"
|
|
@@ -17,6 +82,18 @@ module ActiveRecord
|
|
|
17
82
|
def desc
|
|
18
83
|
Ordering.new(self, :desc)
|
|
19
84
|
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Resolves an operand denoting a column or an expression.
|
|
89
|
+
def to_arel_operand(operand, table)
|
|
90
|
+
case operand
|
|
91
|
+
when Node then operand.to_arel(table)
|
|
92
|
+
when :* then Arel.star
|
|
93
|
+
when Symbol then table[operand]
|
|
94
|
+
else operand
|
|
95
|
+
end
|
|
96
|
+
end
|
|
20
97
|
end
|
|
21
98
|
|
|
22
99
|
class Predicate < Node
|
|
@@ -34,6 +111,8 @@ module ActiveRecord
|
|
|
34
111
|
end
|
|
35
112
|
|
|
36
113
|
class Column < Node
|
|
114
|
+
include Predications
|
|
115
|
+
|
|
37
116
|
attr_reader :table_name, :column_name
|
|
38
117
|
|
|
39
118
|
def initialize(table_name, column_name)
|
|
@@ -45,20 +124,14 @@ module ActiveRecord
|
|
|
45
124
|
Arel::Table.new(table_name)[column_name]
|
|
46
125
|
end
|
|
47
126
|
|
|
48
|
-
%i[== != =~ !~ > >= < <=].each do |op|
|
|
49
|
-
define_method(op) {|val| Comparison.new(self, op, val) }
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def null?
|
|
53
|
-
Comparison.new(self, :==, nil)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
127
|
%i[count sum average maximum minimum].each do |func|
|
|
57
128
|
define_method(func) { Aggregate.new(self, func) }
|
|
58
129
|
end
|
|
59
130
|
end
|
|
60
131
|
|
|
61
132
|
class Aggregate < Node
|
|
133
|
+
include Predications
|
|
134
|
+
|
|
62
135
|
attr_reader :operand, :function
|
|
63
136
|
|
|
64
137
|
def initialize(operand, function)
|
|
@@ -67,15 +140,7 @@ module ActiveRecord
|
|
|
67
140
|
end
|
|
68
141
|
|
|
69
142
|
def to_arel(table)
|
|
70
|
-
|
|
71
|
-
when Node then operand.to_arel(table)
|
|
72
|
-
else table[operand]
|
|
73
|
-
end
|
|
74
|
-
arel_operand.public_send(function)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
%i[== != =~ !~ > >= < <=].each do |op|
|
|
78
|
-
define_method(op) {|val| Comparison.new(self, op, val) }
|
|
143
|
+
to_arel_operand(operand, table).public_send(function)
|
|
79
144
|
end
|
|
80
145
|
end
|
|
81
146
|
|
|
@@ -88,12 +153,7 @@ module ActiveRecord
|
|
|
88
153
|
end
|
|
89
154
|
|
|
90
155
|
def to_arel(table)
|
|
91
|
-
|
|
92
|
-
when Node then operand.to_arel(table)
|
|
93
|
-
when Symbol then table[operand]
|
|
94
|
-
else operand
|
|
95
|
-
end
|
|
96
|
-
arel_operand.as(alias_name.to_s)
|
|
156
|
+
to_arel_operand(operand, table).as(alias_name.to_s)
|
|
97
157
|
end
|
|
98
158
|
end
|
|
99
159
|
|
|
@@ -106,16 +166,13 @@ module ActiveRecord
|
|
|
106
166
|
end
|
|
107
167
|
|
|
108
168
|
def to_arel(table)
|
|
109
|
-
|
|
110
|
-
when Node then operand.to_arel(table)
|
|
111
|
-
when Symbol then table[operand]
|
|
112
|
-
else operand
|
|
113
|
-
end
|
|
114
|
-
arel_operand.public_send(direction)
|
|
169
|
+
to_arel_operand(operand, table).public_send(direction)
|
|
115
170
|
end
|
|
116
171
|
end
|
|
117
172
|
|
|
118
173
|
class Function < Node
|
|
174
|
+
include Predications
|
|
175
|
+
|
|
119
176
|
attr_reader :name, :args
|
|
120
177
|
|
|
121
178
|
def initialize(name, args)
|
|
@@ -126,22 +183,20 @@ module ActiveRecord
|
|
|
126
183
|
def to_arel(table)
|
|
127
184
|
arel_args = args.map do |arg|
|
|
128
185
|
case arg
|
|
129
|
-
when Node then arg
|
|
130
|
-
when Symbol then table[arg]
|
|
186
|
+
when Node, Symbol then to_arel_operand(arg, table)
|
|
131
187
|
else Arel::Nodes.build_quoted(arg)
|
|
132
188
|
end
|
|
133
189
|
end
|
|
134
190
|
Arel::Nodes::NamedFunction.new(name, arel_args)
|
|
135
191
|
end
|
|
136
|
-
|
|
137
|
-
%i[== != =~ !~ > >= < <=].each do |op|
|
|
138
|
-
define_method(op) {|val| Comparison.new(self, op, val) }
|
|
139
|
-
end
|
|
140
192
|
end
|
|
141
193
|
|
|
194
|
+
# A plain SQL comparison. The value is passed through as it is, so a Range
|
|
195
|
+
# or an Array compares against a PostgreSQL range or array column, the way
|
|
196
|
+
# ActiveRecord's own force_equality? types do.
|
|
142
197
|
class Comparison < Predicate
|
|
143
198
|
OPERATOR_MAP = {
|
|
144
|
-
:== => :eq, :!= => :not_eq,
|
|
199
|
+
:== => :eq, :!= => :not_eq,
|
|
145
200
|
:> => :gt, :>= => :gteq, :< => :lt, :<= => :lteq
|
|
146
201
|
}.freeze
|
|
147
202
|
|
|
@@ -154,26 +209,88 @@ module ActiveRecord
|
|
|
154
209
|
end
|
|
155
210
|
|
|
156
211
|
def to_arel(table)
|
|
157
|
-
arel_column =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
212
|
+
arel_column = to_arel_operand(column, table)
|
|
213
|
+
arel_value = value.is_a?(Node) ? value.to_arel(table) : value
|
|
214
|
+
arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# IN for a list of values, BETWEEN for a range.
|
|
219
|
+
class In < Predicate
|
|
220
|
+
attr_reader :operand, :values
|
|
221
|
+
|
|
222
|
+
def initialize(operand, values)
|
|
223
|
+
@operand = operand
|
|
224
|
+
@values = values
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def to_arel(table)
|
|
228
|
+
arel_operand = to_arel_operand(operand, table)
|
|
229
|
+
case values
|
|
230
|
+
when Range then arel_operand.between(values)
|
|
231
|
+
else arel_operand.in(values)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
class Like < Predicate
|
|
237
|
+
ESCAPE = "\\".freeze
|
|
238
|
+
|
|
239
|
+
# Escapes % and _ so that they match literally. The pattern built from
|
|
240
|
+
# the result must be used with ESCAPE, since SQLite has no default
|
|
241
|
+
# escape character.
|
|
242
|
+
def self.escape(string)
|
|
243
|
+
ActiveRecord::Base.sanitize_sql_like(string, ESCAPE)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
attr_reader :operand, :pattern, :escape
|
|
247
|
+
|
|
248
|
+
def initialize(operand, pattern, escape = nil)
|
|
249
|
+
@operand = operand
|
|
250
|
+
@pattern = pattern
|
|
251
|
+
@escape = escape
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def to_arel(table)
|
|
255
|
+
# Arel matches case-insensitively unless told otherwise, which turns
|
|
256
|
+
# into ILIKE on PostgreSQL. like? means SQL LIKE on every adapter.
|
|
257
|
+
to_arel_operand(operand, table).matches(pattern, escape, true)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Regular expression match: REGEXP on MySQL, ~ on PostgreSQL. SQLite has
|
|
262
|
+
# no regexp operator built in, so Arel raises NotImplementedError there.
|
|
263
|
+
class Match < Predicate
|
|
264
|
+
attr_reader :operand, :pattern, :negated
|
|
265
|
+
|
|
266
|
+
def initialize(operand, pattern, negated: false)
|
|
267
|
+
@operand = operand
|
|
268
|
+
@pattern = pattern.is_a?(Regexp) ? regexp_source(pattern) : pattern
|
|
269
|
+
@negated = negated
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def to_arel(table)
|
|
273
|
+
arel_operand = to_arel_operand(operand, table)
|
|
274
|
+
if negated
|
|
275
|
+
arel_operand.does_not_match_regexp(pattern)
|
|
174
276
|
else
|
|
175
|
-
|
|
277
|
+
arel_operand.matches_regexp(pattern)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
|
|
283
|
+
# A Regexp literal reads naturally with =~, but only its source crosses
|
|
284
|
+
# over; the database has its own dialect and no notion of Ruby's flags.
|
|
285
|
+
# Dropping a flag would silently change what the query matches, so
|
|
286
|
+
# anything beyond a plain literal is refused rather than ignored.
|
|
287
|
+
def regexp_source(regexp)
|
|
288
|
+
unless regexp.options.zero?
|
|
289
|
+
raise ArgumentError,
|
|
290
|
+
"#{regexp.inspect} has options that SQL cannot express; " \
|
|
291
|
+
"pass the pattern as a string instead"
|
|
176
292
|
end
|
|
293
|
+
regexp.source
|
|
177
294
|
end
|
|
178
295
|
end
|
|
179
296
|
|
|
@@ -2,13 +2,7 @@ module ActiveRecord
|
|
|
2
2
|
module Refined
|
|
3
3
|
module BlockSyntax
|
|
4
4
|
refine Symbol do
|
|
5
|
-
|
|
6
|
-
define_method(op) {|val| AST::Comparison.new(self, op, val) }
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def null?
|
|
10
|
-
AST::Comparison.new(self, :==, nil)
|
|
11
|
-
end
|
|
5
|
+
import_methods AST::Predications
|
|
12
6
|
|
|
13
7
|
%i[count sum average maximum minimum].each do |func|
|
|
14
8
|
define_method(func) { AST::Aggregate.new(self, func) }
|