activerecord-refined 0.3.2 → 0.4.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.
@@ -1,15 +1,38 @@
1
1
  module ActiveRecord
2
2
  module Refined
3
3
  module AST
4
+ # Column aliases and function names are written into the SQL as given,
5
+ # where a value would have been quoted, so anything that is not a plain
6
+ # name is refused. Quoting them instead would need the model's adapter,
7
+ # which does not reach this far, and quoting with the wrong one would be
8
+ # a bug of its own -- MySQL does not read "x" as an identifier.
9
+ NAME = /[[:alpha:]_][[:alnum:]_$]*/
10
+ ALIAS_NAME = /\A#{NAME}\z/
11
+ FUNCTION_NAME = /\A#{NAME}(\.#{NAME})?\z/
12
+
13
+ def self.check_name(name, pattern, what)
14
+ return name if pattern.match?(name.to_s)
15
+ raise ArgumentError, "#{name.inspect} is not a plain #{what}"
16
+ end
17
+
4
18
  # Predicate builders shared by symbols, qualified columns and
5
19
  # expressions. Imported into the Symbol refinement with
6
20
  # Refinement#import_methods, so every method must be defined with def.
7
21
  module Predications
22
+ # == and != mean SQL = and <>, and = NULL is never true there, so nil
23
+ # is rejected rather than silently rewritten to IS NULL. null? builds
24
+ # its node directly and stays clear of this check.
8
25
  def ==(other)
26
+ if other.nil?
27
+ raise ArgumentError, "== does not take nil; use null? instead"
28
+ end
9
29
  Comparison.new(self, :==, other)
10
30
  end
11
31
 
12
32
  def !=(other)
33
+ if other.nil?
34
+ raise ArgumentError, "!= does not take nil; use !null? instead"
35
+ end
13
36
  Comparison.new(self, :!=, other)
14
37
  end
15
38
 
@@ -53,17 +76,118 @@ module ActiveRecord
53
76
  Like.new(self, pattern)
54
77
  end
55
78
 
56
- def start_with?(prefix)
57
- Like.new(self, "#{Like.escape(prefix)}%", Like::ESCAPE)
79
+ def ilike?(pattern)
80
+ Like.new(self, pattern, nil, case_sensitive: false)
81
+ end
82
+
83
+ # Case-insensitive equality, folded on both sides rather than left to
84
+ # the collation, so it means the same thing on every adapter.
85
+ def casecmp?(value)
86
+ if value.nil?
87
+ raise ArgumentError, "casecmp? does not take nil; use null? instead"
88
+ end
89
+ Comparison.new(Function.new("LOWER", [self]), :==,
90
+ Function.new("LOWER", [value]))
91
+ end
92
+
93
+ # Null-safe comparison: unlike = and <>, these treat NULL as a value,
94
+ # so not_distinct_from? is the one equality that may take nil.
95
+ def distinct_from?(value)
96
+ DistinctFrom.new(self, value, negated: true)
97
+ end
98
+
99
+ def not_distinct_from?(value)
100
+ DistinctFrom.new(self, value)
101
+ end
102
+
103
+ def start_with?(*prefixes)
104
+ if prefixes.empty?
105
+ raise ArgumentError, "start_with? needs at least one prefix"
106
+ end
107
+ Like.any(self, prefixes.map {|prefix| "#{Like.escape(prefix)}%" })
58
108
  end
59
109
 
60
- def end_with?(suffix)
61
- Like.new(self, "%#{Like.escape(suffix)}", Like::ESCAPE)
110
+ def end_with?(*suffixes)
111
+ if suffixes.empty?
112
+ raise ArgumentError, "end_with? needs at least one suffix"
113
+ end
114
+ Like.any(self, suffixes.map {|suffix| "%#{Like.escape(suffix)}" })
62
115
  end
63
116
 
64
117
  def include?(substring)
65
118
  Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
66
119
  end
120
+
121
+ # The array comparisons carry the meaning of their Ruby namesakes.
122
+ # member? is Enumerable's element test, so an Array argument is
123
+ # rejected rather than quietly meaning something Array#member? does
124
+ # not; whole-array comparisons go by the Set and Array names.
125
+ def member?(element)
126
+ if element.is_a?(::Array) || element.is_a?(::Set)
127
+ raise ArgumentError,
128
+ "member? takes a single element; use superset? to require every element"
129
+ end
130
+ ArrayPredicate.new(self, :"@>", [element])
131
+ end
132
+
133
+ def superset?(elements)
134
+ ArrayPredicate.new(self, :"@>", ArrayPredicate.elements(elements, "superset?"))
135
+ end
136
+
137
+ def subset?(elements)
138
+ ArrayPredicate.new(self, :"<@", ArrayPredicate.elements(elements, "subset?"))
139
+ end
140
+
141
+ def intersect?(elements)
142
+ ArrayPredicate.new(self, :"&&", ArrayPredicate.elements(elements, "intersect?"))
143
+ end
144
+ end
145
+
146
+ # Arithmetic builders shared by symbols, qualified columns and
147
+ # expressions. Imported into the Symbol refinement like Predications,
148
+ # so every method must be defined with def.
149
+ module Arithmetics
150
+ def +(other)
151
+ Arithmetic.new(self, :+, other)
152
+ end
153
+
154
+ def -(other)
155
+ Arithmetic.new(self, :-, other)
156
+ end
157
+
158
+ def *(other)
159
+ Arithmetic.new(self, :*, other)
160
+ end
161
+
162
+ def /(other)
163
+ Arithmetic.new(self, :/, other)
164
+ end
165
+ end
166
+
167
+ # Aggregate builders shared by symbols, qualified columns and
168
+ # expressions. Imported into the Symbol refinement like Predications,
169
+ # so every method must be defined with def.
170
+ module Aggregations
171
+ # DISTINCT is Arel's only aggregate modifier, and only for count.
172
+ def count(distinct: false)
173
+ Aggregate.new(self, :count, distinct: distinct)
174
+ end
175
+
176
+ def sum
177
+ Aggregate.new(self, :sum)
178
+ end
179
+
180
+ def average
181
+ Aggregate.new(self, :average)
182
+ end
183
+
184
+ def maximum
185
+ Aggregate.new(self, :maximum)
186
+ end
187
+
188
+ def minimum
189
+ Aggregate.new(self, :minimum)
190
+ end
67
191
  end
68
192
 
69
193
  class Node
@@ -112,6 +236,8 @@ module ActiveRecord
112
236
 
113
237
  class Column < Node
114
238
  include Predications
239
+ include Arithmetics
240
+ include Aggregations
115
241
 
116
242
  attr_reader :table_name, :column_name
117
243
 
@@ -123,24 +249,55 @@ module ActiveRecord
123
249
  def to_arel(_table)
124
250
  Arel::Table.new(table_name)[column_name]
125
251
  end
252
+ end
253
+
254
+ # Arithmetic on columns and expressions. Ruby's precedence puts these
255
+ # above the comparison operators, so :price * :quantity > 100 groups the
256
+ # way it reads.
257
+ class Arithmetic < Node
258
+ include Predications
259
+ include Arithmetics
260
+ include Aggregations
126
261
 
127
- %i[count sum average maximum minimum].each do |func|
128
- define_method(func) { Aggregate.new(self, func) }
262
+ attr_reader :left, :operator, :right
263
+
264
+ def initialize(left, operator, right)
265
+ @left = left
266
+ @operator = operator
267
+ @right = right
268
+ end
269
+
270
+ def to_arel(table)
271
+ to_arel_operand(left, table).
272
+ public_send(operator, to_arel_operand(right, table))
129
273
  end
130
274
  end
131
275
 
132
276
  class Aggregate < Node
133
277
  include Predications
278
+ include Arithmetics
134
279
 
135
- attr_reader :operand, :function
280
+ attr_reader :operand, :function, :distinct
136
281
 
137
- def initialize(operand, function)
282
+ def initialize(operand, function, distinct: false)
283
+ if distinct && function != :count
284
+ raise ArgumentError, "#{function} does not take distinct"
285
+ end
286
+ if distinct && operand == :*
287
+ raise ArgumentError, "count(:*) does not take distinct; name a column"
288
+ end
138
289
  @operand = operand
139
290
  @function = function
291
+ @distinct = distinct
140
292
  end
141
293
 
142
294
  def to_arel(table)
143
- to_arel_operand(operand, table).public_send(function)
295
+ arel_operand = to_arel_operand(operand, table)
296
+ if function == :count
297
+ arel_operand.count(distinct)
298
+ else
299
+ arel_operand.public_send(function)
300
+ end
144
301
  end
145
302
  end
146
303
 
@@ -149,7 +306,7 @@ module ActiveRecord
149
306
 
150
307
  def initialize(operand, alias_name)
151
308
  @operand = operand
152
- @alias_name = alias_name
309
+ @alias_name = AST.check_name(alias_name, ALIAS_NAME, "column alias")
153
310
  end
154
311
 
155
312
  def to_arel(table)
@@ -158,20 +315,33 @@ module ActiveRecord
158
315
  end
159
316
 
160
317
  class Ordering < Node
161
- attr_reader :operand, :direction
318
+ attr_reader :operand, :direction, :nulls
162
319
 
163
- def initialize(operand, direction)
320
+ def initialize(operand, direction, nulls = nil)
164
321
  @operand = operand
165
322
  @direction = direction
323
+ @nulls = nulls
324
+ end
325
+
326
+ # MySQL has no NULLS FIRST/LAST, but Arel emulates it there with a
327
+ # leading IS NULL ordering, so these are portable.
328
+ def nulls_first
329
+ Ordering.new(operand, direction, :nulls_first)
330
+ end
331
+
332
+ def nulls_last
333
+ Ordering.new(operand, direction, :nulls_last)
166
334
  end
167
335
 
168
336
  def to_arel(table)
169
- to_arel_operand(operand, table).public_send(direction)
337
+ ordering = to_arel_operand(operand, table).public_send(direction)
338
+ nulls ? ordering.public_send(nulls) : ordering
170
339
  end
171
340
  end
172
341
 
173
342
  class Function < Node
174
343
  include Predications
344
+ include Arithmetics
175
345
 
176
346
  attr_reader :name, :args
177
347
 
@@ -210,12 +380,31 @@ module ActiveRecord
210
380
 
211
381
  def to_arel(table)
212
382
  arel_column = to_arel_operand(column, table)
213
- arel_value = value.is_a?(Node) ? value.to_arel(table) : value
383
+ arel_value =
384
+ case value
385
+ when Node then value.to_arel(table)
386
+ when ActiveRecord::Relation then scalar_subquery(value)
387
+ else value
388
+ end
214
389
  arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
215
390
  end
391
+
392
+ private
393
+
394
+ # A relation compared against a column has to yield a single value, so
395
+ # unlike In there is no sensible default select list to fall back on.
396
+ def scalar_subquery(relation)
397
+ if relation.select_values.empty?
398
+ raise ArgumentError,
399
+ "#{operator} needs a subquery selecting one value; add a select"
400
+ end
401
+ relation = relation.send(:apply_join_dependency) if relation.eager_loading?
402
+ relation.arel
403
+ end
216
404
  end
217
405
 
218
- # IN for a list of values, BETWEEN for a range.
406
+ # IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
407
+ # relation.
219
408
  class In < Predicate
220
409
  attr_reader :operand, :values
221
410
 
@@ -228,9 +417,49 @@ module ActiveRecord
228
417
  arel_operand = to_arel_operand(operand, table)
229
418
  case values
230
419
  when Range then arel_operand.between(values)
420
+ when ActiveRecord::Relation then arel_operand.in(subquery(values))
231
421
  else arel_operand.in(values)
232
422
  end
233
423
  end
424
+
425
+ private
426
+
427
+ # The same treatment ActiveRecord's own RelationHandler gives a
428
+ # relation used as a value: without an explicit select list the
429
+ # subquery selects the model's primary key.
430
+ def subquery(relation)
431
+ if relation.eager_loading?
432
+ relation = relation.send(:apply_join_dependency)
433
+ end
434
+ if relation.select_values.empty?
435
+ model = relation.model
436
+ if model.composite_primary_key?
437
+ raise ArgumentError,
438
+ "Cannot map composite primary key #{model.primary_key} to IN"
439
+ end
440
+ relation = relation.select(relation.table[model.primary_key])
441
+ end
442
+ relation.arel
443
+ end
444
+ end
445
+
446
+ # EXISTS (SELECT ...) for a relation. Correlate the subquery with the
447
+ # outer table through qualified columns. EXISTS only asks whether a row
448
+ # comes back, so unlike In there is no select list to fix up.
449
+ class Exists < Predicate
450
+ attr_reader :relation
451
+
452
+ def initialize(relation)
453
+ @relation = relation
454
+ end
455
+
456
+ def to_arel(_table)
457
+ subquery = relation
458
+ if subquery.eager_loading?
459
+ subquery = subquery.send(:apply_join_dependency)
460
+ end
461
+ subquery.arel.exists
462
+ end
234
463
  end
235
464
 
236
465
  class Like < Predicate
@@ -243,18 +472,104 @@ module ActiveRecord
243
472
  ActiveRecord::Base.sanitize_sql_like(string, ESCAPE)
244
473
  end
245
474
 
246
- attr_reader :operand, :pattern, :escape
475
+ # ORs one LIKE per pattern, for the shortcuts that accept several
476
+ # literals the way String#start_with? does.
477
+ def self.any(operand, patterns)
478
+ patterns.map {|pattern| new(operand, pattern, ESCAPE) }.
479
+ inject {|left, right| Or.new(left, right) }
480
+ end
481
+
482
+ attr_reader :operand, :pattern, :escape, :case_sensitive
247
483
 
248
- def initialize(operand, pattern, escape = nil)
484
+ def initialize(operand, pattern, escape = nil, case_sensitive: true)
249
485
  @operand = operand
250
486
  @pattern = pattern
251
487
  @escape = escape
488
+ @case_sensitive = case_sensitive
489
+ end
490
+
491
+ def to_arel(table)
492
+ # Arel matches case-insensitively unless told otherwise, which is
493
+ # what picks ILIKE over LIKE on PostgreSQL.
494
+ to_arel_operand(operand, table).matches(pattern, escape, case_sensitive)
495
+ end
496
+ end
497
+
498
+ # IS [NOT] DISTINCT FROM, spelled IS / IS NOT on SQLite and <=> on
499
+ # MySQL. NULL compares as a value here, which is what separates these
500
+ # from = and <>.
501
+ class DistinctFrom < Predicate
502
+ attr_reader :operand, :value, :negated
503
+
504
+ def initialize(operand, value, negated: false)
505
+ @operand = operand
506
+ @value = value
507
+ @negated = negated
252
508
  end
253
509
 
254
510
  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)
511
+ arel_operand = to_arel_operand(operand, table)
512
+ arel_value = value.is_a?(Node) ? value.to_arel(table) : value
513
+ if negated
514
+ arel_operand.is_distinct_from(arel_value)
515
+ else
516
+ arel_operand.is_not_distinct_from(arel_value)
517
+ end
518
+ end
519
+ end
520
+
521
+ # Comparisons against a PostgreSQL array column, named after the Ruby
522
+ # methods that mean the same thing: member? is Enumerable's element
523
+ # test, superset? and subset? are Set's whole-array containment, and
524
+ # intersect? is Array's "any element in common". Each name maps to one
525
+ # operator; the elements are rendered as an array literal, which
526
+ # PostgreSQL coerces to the column's element type, so any expression
527
+ # works as the operand and no schema lookup is needed.
528
+ class ArrayPredicate < Predicate
529
+ # The whole-array comparisons take the collection kinds their
530
+ # namesakes compare against: an Array, or a Set for the Set methods.
531
+ def self.elements(arg, method_name)
532
+ case arg
533
+ when ::Array then arg
534
+ when ::Set then arg.to_a
535
+ else
536
+ raise ArgumentError, "#{method_name} takes an Array or Set of elements"
537
+ end
538
+ end
539
+
540
+ attr_reader :operand, :operator, :elements
541
+
542
+ def initialize(operand, operator, elements)
543
+ @operand = operand
544
+ @operator = operator
545
+ @elements = elements
546
+ end
547
+
548
+ def to_arel(table)
549
+ arel_operand = to_arel_operand(operand, table)
550
+ quoted = Arel::Nodes.build_quoted(array_literal)
551
+ case operator
552
+ when :"@>" then Arel::Nodes::Contains.new(arel_operand, quoted)
553
+ when :"&&" then Arel::Nodes::Overlaps.new(arel_operand, quoted)
554
+ else Arel::Nodes::InfixOperation.new(operator, arel_operand, quoted)
555
+ end
556
+ end
557
+
558
+ private
559
+
560
+ # PostgreSQL array input syntax: elements joined by commas inside
561
+ # braces, and an element is double-quoted whenever it is empty, spells
562
+ # NULL, or contains a character the parser treats specially.
563
+ def array_literal
564
+ encoded = elements.map do |value|
565
+ s = value.to_s
566
+ if s.empty? || s.casecmp?("null") || s.match?(/[\s{},"\\]/)
567
+ "\"#{s.gsub(/["\\]/) {|c| "\\#{c}" }}\""
568
+ else
569
+ s
570
+ end
571
+ end
572
+ "{#{encoded.join(',')}}"
258
573
  end
259
574
  end
260
575
 
@@ -3,10 +3,8 @@ module ActiveRecord
3
3
  module BlockSyntax
4
4
  refine Symbol do
5
5
  import_methods AST::Predications
6
-
7
- %i[count sum average maximum minimum].each do |func|
8
- define_method(func) { AST::Aggregate.new(self, func) }
9
- end
6
+ import_methods AST::Arithmetics
7
+ import_methods AST::Aggregations
10
8
 
11
9
  def as(alias_name)
12
10
  AST::As.new(self, alias_name)
@@ -27,18 +25,95 @@ module ActiveRecord
27
25
  end
28
26
 
29
27
  class BlockContext
28
+ # The model is only consulted to learn which adapter the query is being
29
+ # built for, which is what decides how a scalar function is spelled.
30
+ def initialize(model)
31
+ @model = model
32
+ end
33
+
30
34
  AGGREGATE_FUNCTIONS = {
31
- count: :count, sum: :sum, avg: :average, min: :minimum, max: :maximum,
35
+ sum: :sum, avg: :average, min: :minimum, max: :maximum,
32
36
  }.freeze
33
37
 
34
38
  AGGREGATE_FUNCTIONS.each do |name, arel_func|
35
39
  define_method(name) {|column| AST::Aggregate.new(column, arel_func) }
36
40
  end
37
41
 
38
- SCALAR_FUNCTIONS = %i[upper lower length trim coalesce abs round].freeze
42
+ def count(column, distinct: false)
43
+ AST::Aggregate.new(column, :count, distinct: distinct)
44
+ end
45
+
46
+ # Scalar functions, defined as real methods so that a typo is a
47
+ # NoMethodError and a name Kernel also answers to (format, hash, test)
48
+ # cannot quietly mean something else.
49
+ #
50
+ # The value lists the adapters that differ: a string is what the
51
+ # function is called there, nil says the adapter has no equivalent. An
52
+ # adapter that is not listed spells it like the method. The families
53
+ # are what the entries key on, so trilogy reads the mysql column.
54
+ #
55
+ # Availability was checked by calling each one; the SQLite figures
56
+ # assume the math functions its build usually enables.
57
+ SCALAR_FUNCTIONS = {
58
+ abs: {}, ceil: {}, coalesce: {}, concat: {}, exp: {}, floor: {},
59
+ length: {}, ln: {}, log: {}, lower: {}, ltrim: {}, mod: {},
60
+ nullif: {}, power: {}, replace: {}, round: {}, rtrim: {}, sqrt: {},
61
+ substr: {}, trim: {}, upper: {},
62
+ char_length: {sqlite: 'LENGTH'},
63
+ greatest: {sqlite: 'MAX'},
64
+ least: {sqlite: 'MIN'},
65
+ now: {sqlite: nil},
66
+ date_trunc: {sqlite: nil, mysql: nil},
67
+ # Named for Kernel#rand, which it also takes back: a block calling
68
+ # rand would otherwise get Ruby's and never reach the database.
69
+ rand: {sqlite: 'RANDOM', postgresql: 'RANDOM'},
70
+ # Two different functions share this name: printf formatting here, and
71
+ # on MySQL the one that puts separators in a number, which reads a
72
+ # printf template as the number zero rather than complaining. The
73
+ # name keeps the one meaning; fn(:format, ...) reaches MySQL's.
74
+ format: {mysql: nil},
75
+ }.freeze
76
+
77
+ ADAPTER_FAMILIES = {
78
+ 'sqlite3' => :sqlite,
79
+ 'postgresql' => :postgresql,
80
+ 'postgis' => :postgresql,
81
+ 'mysql2' => :mysql,
82
+ 'trilogy' => :mysql,
83
+ }.freeze
84
+
85
+ SCALAR_FUNCTIONS.each_key do |name|
86
+ define_method(name) {|*args| AST::Function.new(function_name(name), args) }
87
+ end
88
+
89
+ # Escape hatch for functions without a method of their own. The name is
90
+ # emitted as written, so a case-sensitive one can be spelled exactly,
91
+ # and for that reason it has to be a plain name, optionally qualified by
92
+ # a schema; anything else is refused rather than written into the SQL.
93
+ def fn(name, *args)
94
+ AST::Function.new(
95
+ AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
96
+ end
97
+
98
+ def exists?(relation)
99
+ AST::Exists.new(relation)
100
+ end
101
+
102
+ private
39
103
 
40
- SCALAR_FUNCTIONS.each do |name|
41
- define_method(name) {|*args| AST::Function.new(name.to_s.upcase, args) }
104
+ def function_name(name)
105
+ spellings = SCALAR_FUNCTIONS.fetch(name)
106
+ return name.to_s.upcase unless spellings.key?(adapter_family)
107
+ spellings.fetch(adapter_family) ||
108
+ raise(NotImplementedError,
109
+ "#{name} has no equivalent on #{@model.connection_db_config.adapter}")
110
+ end
111
+
112
+ # An adapter nobody has classified keeps the standard spellings, and is
113
+ # left to say for itself what it cannot do.
114
+ def adapter_family
115
+ @adapter_family ||=
116
+ ADAPTER_FAMILIES[@model.connection_db_config.adapter] || :unknown
42
117
  end
43
118
  end
44
119
 
@@ -89,19 +164,39 @@ module ActiveRecord
89
164
  end
90
165
  end
91
166
 
92
- def joins(*args, &block)
167
+ # A symbol names a table, which ActiveRecord's own from only takes as a
168
+ # string. With `as` it is selected under another name, which is how a
169
+ # CTE stands in for the model's own table:
170
+ # with_recursive(tree: [...]).from(:tree, as: :nodes)
171
+ def from(value, subquery_name = nil, as: nil)
172
+ unless value.is_a?(Symbol)
173
+ if as
174
+ raise ArgumentError, "as: needs the table named as a symbol"
175
+ end
176
+ return super(value, subquery_name)
177
+ end
178
+ arel_table = Arel::Table.new(value)
179
+ arel_table = arel_table.alias(as) if as
180
+ super(arel_table, subquery_name)
181
+ end
182
+
183
+ # `as` names the table within the query, which is what makes a self
184
+ # join expressible: joins(:employees, as: :managers) { ... }.
185
+ def joins(*args, as: nil, &block)
93
186
  if block
94
- super(build_join_node(args.first, Arel::Nodes::InnerJoin, &block))
187
+ super(build_join_node(args.first, Arel::Nodes::InnerJoin, as, &block))
95
188
  else
96
- super
189
+ reject_join_alias(as)
190
+ super(*args, &block)
97
191
  end
98
192
  end
99
193
 
100
- def left_outer_joins(*args, &block)
194
+ def left_outer_joins(*args, as: nil, &block)
101
195
  if block
102
- joins(build_join_node(args.first, Arel::Nodes::OuterJoin, &block))
196
+ joins(build_join_node(args.first, Arel::Nodes::OuterJoin, as, &block))
103
197
  else
104
- super
198
+ reject_join_alias(as)
199
+ super(*args, &block)
105
200
  end
106
201
  end
107
202
 
@@ -109,7 +204,7 @@ module ActiveRecord
109
204
 
110
205
  def evaluate_block(&block)
111
206
  refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
112
- BlockContext.new.instance_exec(&refined_block)
207
+ BlockContext.new(klass).instance_exec(&refined_block)
113
208
  end
114
209
 
115
210
  def to_arel_field(node)
@@ -120,12 +215,16 @@ module ActiveRecord
120
215
  end
121
216
  end
122
217
 
123
- def build_join_node(target_table, join_class, &block)
218
+ def reject_join_alias(alias_name)
219
+ return unless alias_name
220
+ raise ArgumentError, "as: needs a block to write the ON clause with"
221
+ end
222
+
223
+ def build_join_node(target_table, join_class, alias_name, &block)
124
224
  ast = evaluate_block(&block)
125
- join_class.new(
126
- Arel::Table.new(target_table),
127
- Arel::Nodes::On.new(ast.to_arel(table))
128
- )
225
+ arel_table = Arel::Table.new(target_table)
226
+ arel_table = arel_table.alias(alias_name) if alias_name
227
+ join_class.new(arel_table, Arel::Nodes::On.new(ast.to_arel(table)))
129
228
  end
130
229
  end
131
230
  end
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module Refined
3
- VERSION = '0.3.2'
3
+ VERSION = '0.4.0'
4
4
  end
5
5
  end