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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/README.md +19 -22
  4. data/activerecord-refined.gemspec +4 -3
  5. data/docs/conditions.md +3 -1
  6. data/docs/ctes.md +2 -2
  7. data/docs/expressions.md +37 -20
  8. data/docs/functions.md +14 -10
  9. data/docs/index.md +2 -17
  10. data/docs/windows.md +2 -2
  11. data/examples/expressions.rb +21 -15
  12. data/lib/active_record/refined/ast/arithmetics.rb +137 -0
  13. data/lib/active_record/refined/ast/conditions.rb +451 -0
  14. data/lib/active_record/refined/ast/expressions.rb +304 -0
  15. data/lib/active_record/refined/ast/functions.rb +254 -0
  16. data/lib/active_record/refined/ast/grouping.rb +63 -0
  17. data/lib/active_record/refined/ast/json.rb +530 -0
  18. data/lib/active_record/refined/ast/node.rb +298 -0
  19. data/lib/active_record/refined/ast/ordering.rb +104 -0
  20. data/lib/active_record/refined/ast/predications.rb +384 -0
  21. data/lib/active_record/refined/ast/windows.rb +126 -0
  22. data/lib/active_record/refined/ast.rb +39 -2364
  23. data/lib/active_record/refined/block_context.rb +671 -0
  24. data/lib/active_record/refined/block_syntax.rb +128 -0
  25. data/lib/active_record/refined/dialect/mysql_compat.rb +16 -0
  26. data/lib/active_record/refined/dialect/mysqlish_json_functions.rb +45 -0
  27. data/lib/active_record/refined/dialect/oracle.rb +1 -7
  28. data/lib/active_record/refined/dialect/postgresql.rb +8 -0
  29. data/lib/active_record/refined/dialect/sql_server.rb +29 -5
  30. data/lib/active_record/refined/dialect/sqlite.rb +23 -0
  31. data/lib/active_record/refined/dialect.rb +132 -66
  32. data/lib/active_record/refined/query_methods.rb +444 -0
  33. data/lib/{activerecord-refined → active_record/refined}/version.rb +3 -2
  34. data/lib/active_record/refined/writes.rb +72 -0
  35. data/lib/active_record/refined.rb +7 -1270
  36. data/lib/activerecord-refined.rb +0 -4
  37. 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