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.
- 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/ctes.md +2 -2
- data/docs/expressions.md +37 -20
- data/docs/functions.md +14 -10
- data/docs/index.md +2 -17
- data/docs/windows.md +2 -2
- data/examples/expressions.rb +21 -15
- 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 +39 -2364
- 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/dialect/mysql_compat.rb +16 -0
- data/lib/active_record/refined/dialect/mysqlish_json_functions.rb +45 -0
- data/lib/active_record/refined/dialect/oracle.rb +1 -7
- data/lib/active_record/refined/dialect/postgresql.rb +8 -0
- data/lib/active_record/refined/dialect/sql_server.rb +29 -5
- data/lib/active_record/refined/dialect/sqlite.rb +23 -0
- data/lib/active_record/refined/dialect.rb +132 -66
- data/lib/active_record/refined/query_methods.rb +444 -0
- data/lib/{activerecord-refined → active_record/refined}/version.rb +3 -2
- data/lib/active_record/refined/writes.rb +72 -0
- data/lib/active_record/refined.rb +7 -1270
- data/lib/activerecord-refined.rb +0 -4
- metadata +36 -5
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/refined/ast/predications"
|
|
4
|
+
require "active_record/refined/ast/arithmetics"
|
|
5
|
+
|
|
6
|
+
module ActiveRecord
|
|
7
|
+
module Refined
|
|
8
|
+
module AST
|
|
9
|
+
# An expression a block has built, whatever it was built from. The
|
|
10
|
+
# methods here are what every one takes; most subclasses add the
|
|
11
|
+
# conditions of {Predications} and the operators of {Arithmetics}.
|
|
12
|
+
class Node
|
|
13
|
+
# The model travels with the table because some SQL cannot be written
|
|
14
|
+
# without knowing the adapter, and a node is built before anything
|
|
15
|
+
# knows which one it will be rendered for -- a symbol becomes a node
|
|
16
|
+
# inside a refinement, where there is no model to ask. Most nodes
|
|
17
|
+
# never look at it and only pass it on.
|
|
18
|
+
# @private
|
|
19
|
+
def to_arel(table, model)
|
|
20
|
+
raise ScriptError, "subclass must override this method"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The expression under an alias, as {BlockSyntax#as} gives a column
|
|
24
|
+
# one.
|
|
25
|
+
# @return [AST::As]
|
|
26
|
+
def as(alias_name, quote: true)
|
|
27
|
+
As.new(self, alias_name, quote: quote)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# An ascending ordering by the expression.
|
|
31
|
+
# @return [AST::Ordering]
|
|
32
|
+
def asc
|
|
33
|
+
Ordering.new(self, :asc)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A descending ordering by the expression.
|
|
37
|
+
# @return [AST::Ordering]
|
|
38
|
+
def desc
|
|
39
|
+
Ordering.new(self, :desc)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The expression under a collation, as {BlockSyntax#collate}.
|
|
43
|
+
# @return [AST::Collate]
|
|
44
|
+
def collate(name)
|
|
45
|
+
Collate.new(self, name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# What lets a number stand on the left of an expression: Ruby hands
|
|
49
|
+
# `20 - (:a + :b)` over to the expression, which takes the number as
|
|
50
|
+
# a {Value} and builds what `value(20) - (:a + :b)` builds.
|
|
51
|
+
# @private
|
|
52
|
+
#
|
|
53
|
+
# Ruby calls this from C, where no refinement is seen, so a bare
|
|
54
|
+
# symbol on the right cannot answer it; refining Integer#- and its
|
|
55
|
+
# kin instead would turn off the interpreter's fast path for every
|
|
56
|
+
# integer in the process.
|
|
57
|
+
def coerce(number)
|
|
58
|
+
[Value.new(number), self]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
# Resolves an operand denoting a column or an expression. A number
|
|
63
|
+
# rides along for Arel to write out, which it can do for Integer and
|
|
64
|
+
# Float alone: a BigDecimal is quoted, which the adapter spells as
|
|
65
|
+
# the exact decimal, and a Rational, which no decimal spells exactly,
|
|
66
|
+
# is refused.
|
|
67
|
+
def to_arel_operand(operand, table, model)
|
|
68
|
+
case operand
|
|
69
|
+
when Node then operand.to_arel(table, model)
|
|
70
|
+
when :* then Arel.star
|
|
71
|
+
when Symbol then table[operand]
|
|
72
|
+
when ::BigDecimal, ::Rational then quote_number(operand)
|
|
73
|
+
else operand
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A bare symbol is a column in every position, the value side of a
|
|
78
|
+
# comparison included. The name is checked against the model, since
|
|
79
|
+
# a name it has no column for is almost always an enum value spelled
|
|
80
|
+
# as a symbol -- which, taken as a column, would quietly compare
|
|
81
|
+
# against nothing anyone meant.
|
|
82
|
+
def column_operand(name, table, model)
|
|
83
|
+
unless model.column_names.include?(name.to_s)
|
|
84
|
+
raise ArgumentError,
|
|
85
|
+
"#{name.inspect} is no column of #{model.table_name}; an enum " \
|
|
86
|
+
"value is written as its string, a column of another table " \
|
|
87
|
+
"qualified"
|
|
88
|
+
end
|
|
89
|
+
table[name]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A number compares as itself, the way a bound ? does: the typed path
|
|
93
|
+
# would cast 99.5 against an integer column to 99 and quietly move
|
|
94
|
+
# the boundary. Everything else keeps the column's own
|
|
95
|
+
# serialization -- an enum's name, a time's zone, a custom type's
|
|
96
|
+
# scaling.
|
|
97
|
+
def quote_number(value)
|
|
98
|
+
case value
|
|
99
|
+
when ::Rational
|
|
100
|
+
raise ArgumentError,
|
|
101
|
+
"a Rational has no exact SQL spelling; to_d says the decimal meant"
|
|
102
|
+
when ::Integer, ::Float, ::BigDecimal
|
|
103
|
+
Arel::Nodes.build_quoted(value)
|
|
104
|
+
else value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Resolves a function argument: a column or an expression as above,
|
|
109
|
+
# anything else a value to be quoted.
|
|
110
|
+
def to_arel_argument(arg, table, model)
|
|
111
|
+
case arg
|
|
112
|
+
when Node, Symbol, ::Rational then to_arel_operand(arg, table, model)
|
|
113
|
+
else Arel::Nodes.build_quoted(arg)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# `&`, `|` and `!` -- AND, OR and NOT, what joins conditions into one
|
|
119
|
+
# and negates them. A predicate carries them, and so do {Sql} and
|
|
120
|
+
# {Operation}, whose SQL is read as a condition the moment it is
|
|
121
|
+
# combined like one.
|
|
122
|
+
#
|
|
123
|
+
# Included in those three and never imported into a refinement, as
|
|
124
|
+
# {Predications} is: a bare symbol is a column, and a column is not a
|
|
125
|
+
# condition.
|
|
126
|
+
module Connectives
|
|
127
|
+
# `AND`.
|
|
128
|
+
# @return [AST::Predicate]
|
|
129
|
+
# @example
|
|
130
|
+
# Author.where { :age.between?(20, 40) & (:country == "JP") }
|
|
131
|
+
# Post.where { sql("score > 0") & (:published == true) }
|
|
132
|
+
def &(other)
|
|
133
|
+
And.new(self, other)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# `OR`.
|
|
137
|
+
# @return [AST::Predicate]
|
|
138
|
+
def |(other)
|
|
139
|
+
Or.new(self, other)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# `NOT (condition)`, negating anything; the negations SQL spells for
|
|
143
|
+
# itself, `IS NOT NULL` and its kin, have names of their own under
|
|
144
|
+
# {Predications}.
|
|
145
|
+
# @return [AST::Predicate]
|
|
146
|
+
# @example
|
|
147
|
+
# Author.where { !:name.like?("A%") }
|
|
148
|
+
#
|
|
149
|
+
# Being here rather than only on Predicate is what keeps `!` on Sql
|
|
150
|
+
# and Operation from falling through to Ruby's own, which would
|
|
151
|
+
# quietly answer false.
|
|
152
|
+
def !
|
|
153
|
+
Not.new(self)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# A condition: what a comparison or one of the tests gives back, and
|
|
158
|
+
# what `where`, `having` and a join's block hand the relation.
|
|
159
|
+
class Predicate < Node
|
|
160
|
+
include Connectives
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# A literal standing where an expression would: `select { value(0).as(:depth) }`.
|
|
164
|
+
#
|
|
165
|
+
# Values reach the SQL quoted wherever they appear as an operand, but a
|
|
166
|
+
# bare Ruby literal at the top of a select list is refused as saying
|
|
167
|
+
# nothing. `value` is the spelling that quotes it there, and it carries
|
|
168
|
+
# the predications with it, so a literal can be compared and combined
|
|
169
|
+
# like anything else.
|
|
170
|
+
class Value < Node
|
|
171
|
+
include Predications
|
|
172
|
+
include Arithmetics
|
|
173
|
+
|
|
174
|
+
# @private
|
|
175
|
+
attr_reader :value
|
|
176
|
+
|
|
177
|
+
def initialize(value)
|
|
178
|
+
@value = value
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @private
|
|
182
|
+
def to_arel(_table, _model)
|
|
183
|
+
Arel::Nodes.build_quoted(value)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# SQL as written: `sql("length(name) > ?", 10)`. The ? and :name
|
|
188
|
+
# placeholders take quoted values through sanitize_sql_array, which
|
|
189
|
+
# needs the connection, so the binds wait here until the model is
|
|
190
|
+
# known. Without binds the statement passes untouched -- which is
|
|
191
|
+
# what leaves PostgreSQL's ? operators writable, since only the
|
|
192
|
+
# positional-bind rewrite reads ? as a placeholder.
|
|
193
|
+
#
|
|
194
|
+
# As an operand the statement is parenthesized: its precedence is
|
|
195
|
+
# whatever was written inside. The top of a select list gets it bare,
|
|
196
|
+
# through field_arel, where parentheses would refuse an alias written
|
|
197
|
+
# into the string.
|
|
198
|
+
class Sql < Node
|
|
199
|
+
include Predications
|
|
200
|
+
include Arithmetics
|
|
201
|
+
# After Arithmetics, whose & and | refuse: here they are AND and OR.
|
|
202
|
+
include Connectives
|
|
203
|
+
|
|
204
|
+
# @private
|
|
205
|
+
attr_reader :statement, :binds
|
|
206
|
+
|
|
207
|
+
def initialize(statement, binds)
|
|
208
|
+
unless statement.is_a?(::String)
|
|
209
|
+
raise ArgumentError,
|
|
210
|
+
"sql takes the statement as a string, not #{statement.inspect}"
|
|
211
|
+
end
|
|
212
|
+
@statement = statement
|
|
213
|
+
@binds = binds
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @private
|
|
217
|
+
def to_arel(_table, model)
|
|
218
|
+
Arel::Nodes::Grouping.new(field_arel(model))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# @private
|
|
222
|
+
def field_arel(model)
|
|
223
|
+
return Arel.sql(statement) if binds.empty?
|
|
224
|
+
|
|
225
|
+
Arel.sql(model.sanitize_sql_array([statement, *binds]))
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# A column of a named table, which is what `:posts[:author_id]` builds
|
|
230
|
+
# (see {BlockSyntax#[]}): the spelling for another table's column in a
|
|
231
|
+
# join's ON or a query over one.
|
|
232
|
+
class Column < Node
|
|
233
|
+
include Predications
|
|
234
|
+
include Arithmetics
|
|
235
|
+
|
|
236
|
+
# @private
|
|
237
|
+
attr_reader :table_name, :column_name
|
|
238
|
+
|
|
239
|
+
def initialize(table_name, column_name)
|
|
240
|
+
@table_name = table_name
|
|
241
|
+
@column_name = column_name
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# @private
|
|
245
|
+
def to_arel(_table, _model)
|
|
246
|
+
Arel::Table.new(table_name)[column_name]
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Escape hatch for operators without a spelling of their own, the way
|
|
251
|
+
# fn is for functions. The operator is emitted as written -- whether
|
|
252
|
+
# the adapter has it is the caller's assertion, as fn's names are --
|
|
253
|
+
# and the values ride as quoted literals, so on PostgreSQL an untyped
|
|
254
|
+
# one takes the type of the operand beside it.
|
|
255
|
+
class Operation < Node
|
|
256
|
+
include Predications
|
|
257
|
+
include Arithmetics
|
|
258
|
+
# After Arithmetics, whose & and | refuse: here they are AND and OR.
|
|
259
|
+
include Connectives
|
|
260
|
+
|
|
261
|
+
# @private
|
|
262
|
+
attr_reader :operator, :left, :right
|
|
263
|
+
|
|
264
|
+
def initialize(operator, left, right)
|
|
265
|
+
@operator = AST.check_name(operator, OPERATOR, "operator").to_s
|
|
266
|
+
@left = check_side(left)
|
|
267
|
+
@right = check_side(right)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# @private
|
|
271
|
+
def to_arel(table, model)
|
|
272
|
+
Arel::Nodes::Grouping.new(
|
|
273
|
+
Arel::Nodes::InfixOperation.new(
|
|
274
|
+
operator, side(left, table, model), side(right, table, model)))
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private
|
|
278
|
+
# An expression operand is parenthesized: an unknown operator's
|
|
279
|
+
# precedence is unknown too, and PostgreSQL reads its named
|
|
280
|
+
# operators from the left, so a bare infix on the right would take
|
|
281
|
+
# the new operator's left side into its own.
|
|
282
|
+
def side(operand, table, model)
|
|
283
|
+
arel = to_arel_argument(operand, table, model)
|
|
284
|
+
operand.is_a?(Node) ? Arel::Nodes::Grouping.new(arel) : arel
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def check_side(operand)
|
|
288
|
+
if operand.is_a?(::Hash) || operand.is_a?(::Array) || operand.is_a?(::Set)
|
|
289
|
+
raise ArgumentError,
|
|
290
|
+
"#{operand.inspect} has no one SQL spelling; a string says it " \
|
|
291
|
+
"in the adapter's own, to_json for a document"
|
|
292
|
+
end
|
|
293
|
+
operand
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/refined/ast/node"
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
module Refined
|
|
7
|
+
module AST
|
|
8
|
+
# A column alias, quoted by the adapter, so that the name asked for is
|
|
9
|
+
# the name that comes back: unquoted, PostgreSQL folds a capital away
|
|
10
|
+
# and the other two keep it, which is one block meaning two things.
|
|
11
|
+
# Quoting also leaves nothing to refuse -- a name that would have been
|
|
12
|
+
# SQL is an identifier with a strange name instead.
|
|
13
|
+
#
|
|
14
|
+
# `quote: false` asks for the name as written, for a schema that wants
|
|
15
|
+
# the folding.
|
|
16
|
+
class As < Node
|
|
17
|
+
# @private
|
|
18
|
+
attr_reader :operand, :alias_name, :quote
|
|
19
|
+
|
|
20
|
+
def initialize(operand, alias_name, quote: true)
|
|
21
|
+
# Checked here rather than where the SQL is built, so that a name
|
|
22
|
+
# the adapter is not being asked to quote is refused where it was
|
|
23
|
+
# written.
|
|
24
|
+
AST.check_name(alias_name, ALIAS_NAME, "column alias") unless quote
|
|
25
|
+
@operand = operand
|
|
26
|
+
@alias_name = alias_name
|
|
27
|
+
@quote = quote
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @private
|
|
31
|
+
def to_arel(table, model)
|
|
32
|
+
to_arel_operand(operand, table, model).as(alias_sql(model))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
def alias_sql(model)
|
|
37
|
+
name = alias_name.to_s
|
|
38
|
+
return name unless quote
|
|
39
|
+
model.with_connection { |connection| connection.quote_column_name(name) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A collation named for a comparison or an ordering: `:name.collate(:ci)`.
|
|
44
|
+
# It stands as an expression -- compared, ordered by, selected -- and
|
|
45
|
+
# gives back one of its own, so the collation carries through.
|
|
46
|
+
#
|
|
47
|
+
# The name follows COLLATE as a bare identifier with no Arel node of its
|
|
48
|
+
# own. What names are safe turns on whether the family quotes it -- only
|
|
49
|
+
# PostgreSQL does -- so the dialect checks the name as it builds the
|
|
50
|
+
# clause, rather than this node holding one rule for all of them.
|
|
51
|
+
class Collate < Node
|
|
52
|
+
include Predications
|
|
53
|
+
|
|
54
|
+
# @private
|
|
55
|
+
attr_reader :operand, :name
|
|
56
|
+
|
|
57
|
+
def initialize(operand, name)
|
|
58
|
+
@name = name.to_s
|
|
59
|
+
@operand = operand
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @private
|
|
63
|
+
def to_arel(table, model)
|
|
64
|
+
Dialect.for(model).collate(to_arel_operand(operand, table, model), name, model)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# An ordering, `"age" DESC`: a direction on a column or an expression,
|
|
69
|
+
# and through {#nulls_first} and {#nulls_last} a place for the NULLs.
|
|
70
|
+
class Ordering < Node
|
|
71
|
+
# @private
|
|
72
|
+
attr_reader :operand, :direction, :nulls
|
|
73
|
+
|
|
74
|
+
def initialize(operand, direction, nulls = nil)
|
|
75
|
+
@operand = operand
|
|
76
|
+
@direction = direction
|
|
77
|
+
@nulls = nulls
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# `NULLS FIRST`; portable, since Arel emulates it where MySQL has
|
|
81
|
+
# none.
|
|
82
|
+
# @return [AST::Ordering]
|
|
83
|
+
#
|
|
84
|
+
# MySQL has no NULLS FIRST/LAST, but Arel emulates it there with a
|
|
85
|
+
# leading IS NULL ordering, so these are portable.
|
|
86
|
+
def nulls_first
|
|
87
|
+
Ordering.new(operand, direction, :nulls_first)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# `NULLS LAST`.
|
|
91
|
+
# @return [AST::Ordering]
|
|
92
|
+
def nulls_last
|
|
93
|
+
Ordering.new(operand, direction, :nulls_last)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @private
|
|
97
|
+
def to_arel(table, model)
|
|
98
|
+
ordering = to_arel_operand(operand, table, model).public_send(direction)
|
|
99
|
+
nulls ? ordering.public_send(nulls) : ordering
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|