activerecord-refined 0.4.0 → 0.5.1
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/sandbox.yml +158 -0
- data/README.md +128 -13
- data/activerecord-refined.gemspec +4 -1
- data/benchmark/query_building.rb +12 -12
- data/examples/ctes.rb +39 -8
- data/examples/predicates.rb +11 -11
- data/examples/subqueries.rb +5 -5
- data/lib/active_record/refined/ast.rb +144 -15
- data/lib/active_record/refined.rb +109 -10
- data/lib/activerecord-refined/version.rb +1 -1
- data/lib/activerecord-refined.rb +4 -0
- data/test/test_block_syntax.rb +349 -38
- metadata +2 -1
|
@@ -9,6 +9,9 @@ module ActiveRecord
|
|
|
9
9
|
NAME = /[[:alpha:]_][[:alnum:]_$]*/
|
|
10
10
|
ALIAS_NAME = /\A#{NAME}\z/
|
|
11
11
|
FUNCTION_NAME = /\A#{NAME}(\.#{NAME})?\z/
|
|
12
|
+
# A SQL type as cast writes it: words, at most parenthesized with
|
|
13
|
+
# lengths -- double precision, decimal(10,2).
|
|
14
|
+
TYPE_NAME = /\A[[:alpha:]_][[:alnum:]_ ]*(\(\d+(, ?\d+)?\))?\z/
|
|
12
15
|
|
|
13
16
|
def self.check_name(name, pattern, what)
|
|
14
17
|
return name if pattern.match?(name.to_s)
|
|
@@ -60,26 +63,50 @@ module ActiveRecord
|
|
|
60
63
|
Match.new(self, pattern, negated: true)
|
|
61
64
|
end
|
|
62
65
|
|
|
66
|
+
# `!` negates any predicate, so these are here for the four that SQL
|
|
67
|
+
# spells for itself: IS NOT NULL rather than NOT (... IS NULL), and
|
|
68
|
+
# likewise NOT IN and NOT LIKE. They mean the same thing either way,
|
|
69
|
+
# including when the column is NULL; what they save is the reading.
|
|
63
70
|
def null?
|
|
64
71
|
Comparison.new(self, :==, nil)
|
|
65
72
|
end
|
|
66
73
|
|
|
74
|
+
def not_null?
|
|
75
|
+
Comparison.new(self, :!=, nil)
|
|
76
|
+
end
|
|
77
|
+
|
|
67
78
|
def in?(values)
|
|
68
79
|
In.new(self, values)
|
|
69
80
|
end
|
|
70
81
|
|
|
82
|
+
def not_in?(values)
|
|
83
|
+
In.new(self, values, negated: true)
|
|
84
|
+
end
|
|
85
|
+
|
|
71
86
|
def between?(min, max)
|
|
72
87
|
In.new(self, min..max)
|
|
73
88
|
end
|
|
74
89
|
|
|
90
|
+
def not_between?(min, max)
|
|
91
|
+
In.new(self, min..max, negated: true)
|
|
92
|
+
end
|
|
93
|
+
|
|
75
94
|
def like?(pattern)
|
|
76
95
|
Like.new(self, pattern)
|
|
77
96
|
end
|
|
78
97
|
|
|
98
|
+
def not_like?(pattern)
|
|
99
|
+
Like.new(self, pattern, negated: true)
|
|
100
|
+
end
|
|
101
|
+
|
|
79
102
|
def ilike?(pattern)
|
|
80
103
|
Like.new(self, pattern, nil, case_sensitive: false)
|
|
81
104
|
end
|
|
82
105
|
|
|
106
|
+
def not_ilike?(pattern)
|
|
107
|
+
Like.new(self, pattern, nil, case_sensitive: false, negated: true)
|
|
108
|
+
end
|
|
109
|
+
|
|
83
110
|
# Case-insensitive equality, folded on both sides rather than left to
|
|
84
111
|
# the collation, so it means the same thing on every adapter.
|
|
85
112
|
def casecmp?(value)
|
|
@@ -218,6 +245,15 @@ module ActiveRecord
|
|
|
218
245
|
else operand
|
|
219
246
|
end
|
|
220
247
|
end
|
|
248
|
+
|
|
249
|
+
# Resolves a function argument: a column or an expression as above,
|
|
250
|
+
# anything else a value to be quoted.
|
|
251
|
+
def to_arel_argument(arg, table)
|
|
252
|
+
case arg
|
|
253
|
+
when Node, Symbol then to_arel_operand(arg, table)
|
|
254
|
+
else Arel::Nodes.build_quoted(arg)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
221
257
|
end
|
|
222
258
|
|
|
223
259
|
class Predicate < Node
|
|
@@ -234,6 +270,29 @@ module ActiveRecord
|
|
|
234
270
|
end
|
|
235
271
|
end
|
|
236
272
|
|
|
273
|
+
# A literal standing where an expression would: `select { value(0).as(:depth) }`.
|
|
274
|
+
#
|
|
275
|
+
# Values reach the SQL quoted wherever they appear as an operand, but the
|
|
276
|
+
# top of a select list is ActiveRecord's, and a bare string there is SQL
|
|
277
|
+
# rather than a string. Saying `value` is how you ask for the other
|
|
278
|
+
# meaning, and it carries the predications with it, so a literal can be
|
|
279
|
+
# compared and combined like anything else.
|
|
280
|
+
class Value < Node
|
|
281
|
+
include Predications
|
|
282
|
+
include Arithmetics
|
|
283
|
+
include Aggregations
|
|
284
|
+
|
|
285
|
+
attr_reader :value
|
|
286
|
+
|
|
287
|
+
def initialize(value)
|
|
288
|
+
@value = value
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def to_arel(_table)
|
|
292
|
+
Arel::Nodes.build_quoted(value)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
237
296
|
class Column < Node
|
|
238
297
|
include Predications
|
|
239
298
|
include Arithmetics
|
|
@@ -351,16 +410,80 @@ module ActiveRecord
|
|
|
351
410
|
end
|
|
352
411
|
|
|
353
412
|
def to_arel(table)
|
|
354
|
-
arel_args = args.map
|
|
355
|
-
case arg
|
|
356
|
-
when Node, Symbol then to_arel_operand(arg, table)
|
|
357
|
-
else Arel::Nodes.build_quoted(arg)
|
|
358
|
-
end
|
|
359
|
-
end
|
|
413
|
+
arel_args = args.map {|arg| to_arel_argument(arg, table) }
|
|
360
414
|
Arel::Nodes::NamedFunction.new(name, arel_args)
|
|
361
415
|
end
|
|
362
416
|
end
|
|
363
417
|
|
|
418
|
+
# EXTRACT(field FROM expr). The field is grammar rather than a value --
|
|
419
|
+
# a keyword the adapter reads bare -- so it has to be a plain name,
|
|
420
|
+
# which Arel upcases on the way out.
|
|
421
|
+
class Extract < Node
|
|
422
|
+
include Predications
|
|
423
|
+
include Arithmetics
|
|
424
|
+
|
|
425
|
+
attr_reader :field, :operand
|
|
426
|
+
|
|
427
|
+
def initialize(field, operand)
|
|
428
|
+
@field = AST.check_name(field, ALIAS_NAME, "extract field")
|
|
429
|
+
@operand = operand
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def to_arel(table)
|
|
433
|
+
Arel::Nodes::Extract.new(to_arel_argument(operand, table), field.to_s)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# CAST(expr AS type). The type is grammar too, written into the SQL as
|
|
438
|
+
# given -- it is the adapter's own name for the type, and whether it
|
|
439
|
+
# exists is the database's to say -- so it has to look like one:
|
|
440
|
+
# a plain name, at most parenthesized with lengths.
|
|
441
|
+
class Cast < Node
|
|
442
|
+
include Predications
|
|
443
|
+
include Arithmetics
|
|
444
|
+
|
|
445
|
+
attr_reader :operand, :sql_type
|
|
446
|
+
|
|
447
|
+
def initialize(operand, sql_type)
|
|
448
|
+
@operand = operand
|
|
449
|
+
@sql_type = AST.check_name(sql_type, TYPE_NAME, "SQL type")
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def to_arel(table)
|
|
453
|
+
Arel::Nodes::NamedFunction.new(
|
|
454
|
+
"CAST",
|
|
455
|
+
[Arel::Nodes::As.new(to_arel_argument(operand, table),
|
|
456
|
+
Arel::Nodes::SqlLiteral.new(sql_type.to_s))])
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# CURRENT_TIMESTAMP and its relatives, what the SQL grammar calls a
|
|
461
|
+
# datetime value function. The grammar has them bare, and PostgreSQL
|
|
462
|
+
# and SQLite reject them written as calls, so unlike Function the name
|
|
463
|
+
# is emitted without parentheses. A precision is the one thing that
|
|
464
|
+
# does go into parentheses, and it is written into the SQL as given, so
|
|
465
|
+
# only an Integer is accepted.
|
|
466
|
+
class DatetimeValueFunction < Node
|
|
467
|
+
include Predications
|
|
468
|
+
include Arithmetics
|
|
469
|
+
|
|
470
|
+
attr_reader :name, :precision
|
|
471
|
+
|
|
472
|
+
def initialize(name, precision = nil)
|
|
473
|
+
unless precision.nil? || precision.is_a?(Integer)
|
|
474
|
+
raise ArgumentError,
|
|
475
|
+
"#{precision.inspect} is not an Integer precision"
|
|
476
|
+
end
|
|
477
|
+
@name = name
|
|
478
|
+
@precision = precision
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def to_arel(_table)
|
|
482
|
+
Arel::Nodes::SqlLiteral.new(
|
|
483
|
+
precision ? "#{name}(#{precision})" : name)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
364
487
|
# A plain SQL comparison. The value is passed through as it is, so a Range
|
|
365
488
|
# or an Array compares against a PostgreSQL range or array column, the way
|
|
366
489
|
# ActiveRecord's own force_equality? types do.
|
|
@@ -406,19 +529,21 @@ module ActiveRecord
|
|
|
406
529
|
# IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
|
|
407
530
|
# relation.
|
|
408
531
|
class In < Predicate
|
|
409
|
-
attr_reader :operand, :values
|
|
532
|
+
attr_reader :operand, :values, :negated
|
|
410
533
|
|
|
411
|
-
def initialize(operand, values)
|
|
534
|
+
def initialize(operand, values, negated: false)
|
|
412
535
|
@operand = operand
|
|
413
536
|
@values = values
|
|
537
|
+
@negated = negated
|
|
414
538
|
end
|
|
415
539
|
|
|
416
540
|
def to_arel(table)
|
|
417
541
|
arel_operand = to_arel_operand(operand, table)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
542
|
+
if values.is_a?(Range)
|
|
543
|
+
arel_operand.public_send(negated ? :not_between : :between, values)
|
|
544
|
+
else
|
|
545
|
+
arg = values.is_a?(ActiveRecord::Relation) ? subquery(values) : values
|
|
546
|
+
arel_operand.public_send(negated ? :not_in : :in, arg)
|
|
422
547
|
end
|
|
423
548
|
end
|
|
424
549
|
|
|
@@ -479,19 +604,23 @@ module ActiveRecord
|
|
|
479
604
|
inject {|left, right| Or.new(left, right) }
|
|
480
605
|
end
|
|
481
606
|
|
|
482
|
-
attr_reader :operand, :pattern, :escape, :case_sensitive
|
|
607
|
+
attr_reader :operand, :pattern, :escape, :case_sensitive, :negated
|
|
483
608
|
|
|
484
|
-
def initialize(operand, pattern, escape = nil, case_sensitive: true
|
|
609
|
+
def initialize(operand, pattern, escape = nil, case_sensitive: true,
|
|
610
|
+
negated: false)
|
|
485
611
|
@operand = operand
|
|
486
612
|
@pattern = pattern
|
|
487
613
|
@escape = escape
|
|
488
614
|
@case_sensitive = case_sensitive
|
|
615
|
+
@negated = negated
|
|
489
616
|
end
|
|
490
617
|
|
|
491
618
|
def to_arel(table)
|
|
492
619
|
# Arel matches case-insensitively unless told otherwise, which is
|
|
493
620
|
# what picks ILIKE over LIKE on PostgreSQL.
|
|
494
|
-
to_arel_operand(operand, table).
|
|
621
|
+
to_arel_operand(operand, table).
|
|
622
|
+
public_send(negated ? :does_not_match : :matches,
|
|
623
|
+
pattern, escape, case_sensitive)
|
|
495
624
|
end
|
|
496
625
|
end
|
|
497
626
|
|
|
@@ -22,6 +22,18 @@ module ActiveRecord
|
|
|
22
22
|
AST::Column.new(self, column_name)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
# Shorthand for `value(0).as(:depth)` and the like. Numbers only: a
|
|
27
|
+
# string in a select list already means SQL rather than a string, so
|
|
28
|
+
# giving String this would make the same literal mean two things
|
|
29
|
+
# depending on whether it had been sent a message.
|
|
30
|
+
[Integer, Float].each do |klass|
|
|
31
|
+
refine klass do
|
|
32
|
+
def as(alias_name)
|
|
33
|
+
AST::As.new(AST::Value.new(self), alias_name)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
25
37
|
end
|
|
26
38
|
|
|
27
39
|
class BlockContext
|
|
@@ -55,13 +67,20 @@ module ActiveRecord
|
|
|
55
67
|
# Availability was checked by calling each one; the SQLite figures
|
|
56
68
|
# assume the math functions its build usually enables.
|
|
57
69
|
SCALAR_FUNCTIONS = {
|
|
58
|
-
abs: {},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
abs: {}, acos: {}, asin: {}, atan: {}, atan2: {}, ceil: {},
|
|
71
|
+
coalesce: {}, concat: {}, cos: {}, degrees: {}, exp: {}, floor: {},
|
|
72
|
+
length: {}, ln: {}, log: {}, log10: {}, lower: {}, ltrim: {},
|
|
73
|
+
mod: {}, nullif: {}, pi: {}, power: {}, radians: {}, replace: {},
|
|
74
|
+
round: {}, rtrim: {}, sign: {}, sin: {}, sqrt: {}, substr: {},
|
|
75
|
+
tan: {}, trim: {}, upper: {},
|
|
62
76
|
char_length: {sqlite: 'LENGTH'},
|
|
63
77
|
greatest: {sqlite: 'MAX'},
|
|
64
78
|
least: {sqlite: 'MIN'},
|
|
79
|
+
# PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
|
|
80
|
+
log2: {postgresql: nil},
|
|
81
|
+
# MySQL's TRUNCATE insists on the second argument, where the others
|
|
82
|
+
# default it to zero; SQLite's trunc takes only the one.
|
|
83
|
+
trunc: {mysql: 'TRUNCATE'},
|
|
65
84
|
now: {sqlite: nil},
|
|
66
85
|
date_trunc: {sqlite: nil, mysql: nil},
|
|
67
86
|
# Named for Kernel#rand, which it also takes back: a block calling
|
|
@@ -83,7 +102,65 @@ module ActiveRecord
|
|
|
83
102
|
}.freeze
|
|
84
103
|
|
|
85
104
|
SCALAR_FUNCTIONS.each_key do |name|
|
|
86
|
-
define_method(name)
|
|
105
|
+
define_method(name) do |*args|
|
|
106
|
+
AST::Function.new(function_name(name, SCALAR_FUNCTIONS), args)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The datetime value functions, as the SQL grammar calls them. These
|
|
111
|
+
# the grammar has bare -- PostgreSQL and SQLite reject them written with
|
|
112
|
+
# parentheses -- and the one thing that does go into parentheses is an
|
|
113
|
+
# optional precision, current_timestamp(3), which current_date never
|
|
114
|
+
# takes and SQLite never accepts. The table reads like
|
|
115
|
+
# SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what
|
|
116
|
+
# now means, reaching SQLite where now does not.
|
|
117
|
+
DATETIME_VALUE_FUNCTIONS = {
|
|
118
|
+
current_date: {},
|
|
119
|
+
current_time: {},
|
|
120
|
+
current_timestamp: {},
|
|
121
|
+
localtime: {sqlite: nil},
|
|
122
|
+
localtimestamp: {sqlite: nil},
|
|
123
|
+
}.freeze
|
|
124
|
+
|
|
125
|
+
def current_date
|
|
126
|
+
AST::DatetimeValueFunction.new(
|
|
127
|
+
function_name(:current_date, DATETIME_VALUE_FUNCTIONS))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
(DATETIME_VALUE_FUNCTIONS.keys - [:current_date]).each do |name|
|
|
131
|
+
define_method(name) do |precision = nil|
|
|
132
|
+
# Built first so that a precision of the wrong type is an
|
|
133
|
+
# ArgumentError on every adapter, before SQLite gets to say it takes
|
|
134
|
+
# none at all.
|
|
135
|
+
node = AST::DatetimeValueFunction.new(
|
|
136
|
+
function_name(name, DATETIME_VALUE_FUNCTIONS), precision)
|
|
137
|
+
if precision && adapter_family == :sqlite
|
|
138
|
+
raise NotImplementedError,
|
|
139
|
+
"#{name} takes no precision on #{@model.connection_db_config.adapter}"
|
|
140
|
+
end
|
|
141
|
+
node
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# EXTRACT(field FROM expr). The field is a keyword, not a value, so it
|
|
146
|
+
# has to be a plain name; the node checks it. SQLite spells all of
|
|
147
|
+
# this as strftime formats, which no renaming carries, so it raises
|
|
148
|
+
# there -- after the node is built, so that a bad field is an
|
|
149
|
+
# ArgumentError on every adapter.
|
|
150
|
+
def extract(field, expr)
|
|
151
|
+
node = AST::Extract.new(field, expr)
|
|
152
|
+
if adapter_family == :sqlite
|
|
153
|
+
raise NotImplementedError,
|
|
154
|
+
"extract has no equivalent on #{@model.connection_db_config.adapter}"
|
|
155
|
+
end
|
|
156
|
+
node
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# CAST(expr AS type). The type is the adapter's own name for it,
|
|
160
|
+
# checked for shape by the node; whether it exists is the database's to
|
|
161
|
+
# say.
|
|
162
|
+
def cast(expr, type)
|
|
163
|
+
AST::Cast.new(expr, type)
|
|
87
164
|
end
|
|
88
165
|
|
|
89
166
|
# Escape hatch for functions without a method of their own. The name is
|
|
@@ -99,10 +176,21 @@ module ActiveRecord
|
|
|
99
176
|
AST::Exists.new(relation)
|
|
100
177
|
end
|
|
101
178
|
|
|
179
|
+
# A literal where an expression is expected, quoted like any other value:
|
|
180
|
+
#
|
|
181
|
+
# select { [:id, value(0).as(:depth)] }
|
|
182
|
+
#
|
|
183
|
+
# Needed because the top of a select list is ActiveRecord's, and a bare
|
|
184
|
+
# string there is SQL rather than a string. Numbers have a shorthand --
|
|
185
|
+
# `0.as(:depth)` -- since nothing else could be meant by one.
|
|
186
|
+
def value(literal)
|
|
187
|
+
AST::Value.new(literal)
|
|
188
|
+
end
|
|
189
|
+
|
|
102
190
|
private
|
|
103
191
|
|
|
104
|
-
def function_name(name)
|
|
105
|
-
spellings =
|
|
192
|
+
def function_name(name, functions)
|
|
193
|
+
spellings = functions.fetch(name)
|
|
106
194
|
return name.to_s.upcase unless spellings.key?(adapter_family)
|
|
107
195
|
spellings.fetch(adapter_family) ||
|
|
108
196
|
raise(NotImplementedError,
|
|
@@ -165,9 +253,8 @@ module ActiveRecord
|
|
|
165
253
|
end
|
|
166
254
|
|
|
167
255
|
# A symbol names a table, which ActiveRecord's own from only takes as a
|
|
168
|
-
# string. With `as` it is selected under another name
|
|
169
|
-
#
|
|
170
|
-
# with_recursive(tree: [...]).from(:tree, as: :nodes)
|
|
256
|
+
# string. With `as` it is selected under another name; when that name
|
|
257
|
+
# is the model's own, from_cte says the same thing without repeating it.
|
|
171
258
|
def from(value, subquery_name = nil, as: nil)
|
|
172
259
|
unless value.is_a?(Symbol)
|
|
173
260
|
if as
|
|
@@ -180,6 +267,18 @@ module ActiveRecord
|
|
|
180
267
|
super(arel_table, subquery_name)
|
|
181
268
|
end
|
|
182
269
|
|
|
270
|
+
# Selects a CTE in place of the model's own table. The alias is not a
|
|
271
|
+
# choice -- ActiveRecord keeps qualifying columns with the table name,
|
|
272
|
+
# so the model's is the only name that works -- which is why it is
|
|
273
|
+
# taken from the model rather than asked for:
|
|
274
|
+
# with_recursive(tree: [...]).from_cte(:tree)
|
|
275
|
+
def from_cte(name)
|
|
276
|
+
unless name.is_a?(Symbol)
|
|
277
|
+
raise ArgumentError, "from_cte takes the CTE's name as a symbol"
|
|
278
|
+
end
|
|
279
|
+
from(name, as: klass.table_name)
|
|
280
|
+
end
|
|
281
|
+
|
|
183
282
|
# `as` names the table within the query, which is what makes a self
|
|
184
283
|
# join expressible: joins(:employees, as: :managers) { ... }.
|
|
185
284
|
def joins(*args, as: nil, &block)
|
data/lib/activerecord-refined.rb
CHANGED
|
@@ -5,3 +5,7 @@ require 'active_record/refined/ast'
|
|
|
5
5
|
require 'active_record/refined'
|
|
6
6
|
|
|
7
7
|
ActiveRecord::QueryMethods.prepend ActiveRecord::Refined::QueryMethods
|
|
8
|
+
|
|
9
|
+
# The methods above are ActiveRecord's own, so a model already forwards them
|
|
10
|
+
# to its relation. from_cte is new, and has to be added to that list itself.
|
|
11
|
+
ActiveRecord::Base.singleton_class.delegate :from_cte, to: :all
|