arel 6.0.0.beta1 → 6.0.0.beta2

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.
@@ -155,6 +155,15 @@ module Arel
155
155
  self
156
156
  end
157
157
 
158
+ def distinct_on(value)
159
+ if value
160
+ @ctx.set_quantifier = Arel::Nodes::DistinctOn.new(value)
161
+ else
162
+ @ctx.set_quantifier = nil
163
+ end
164
+ self
165
+ end
166
+
158
167
  def order *expr
159
168
  # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
160
169
  @ast.orders.concat expr.map { |x|
@@ -3,6 +3,7 @@ module Arel
3
3
  class DepthFirst < Arel::Visitors::Visitor
4
4
  def initialize block = nil
5
5
  @block = block || Proc.new
6
+ super()
6
7
  end
7
8
 
8
9
  private
@@ -86,6 +87,7 @@ module Arel
86
87
  alias :visit_Arel_Nodes_RightOuterJoin :binary
87
88
  alias :visit_Arel_Nodes_TableAlias :binary
88
89
  alias :visit_Arel_Nodes_Values :binary
90
+ alias :visit_Arel_Nodes_Union :binary
89
91
 
90
92
  def visit_Arel_Nodes_StringJoin o
91
93
  visit o.left
@@ -116,6 +118,8 @@ module Arel
116
118
  alias :visit_Arel_Nodes_SqlLiteral :terminal
117
119
  alias :visit_Arel_Nodes_BindParam :terminal
118
120
  alias :visit_Arel_Nodes_Window :terminal
121
+ alias :visit_Arel_Nodes_True :terminal
122
+ alias :visit_Arel_Nodes_False :terminal
119
123
  alias :visit_BigDecimal :terminal
120
124
  alias :visit_Bignum :terminal
121
125
  alias :visit_Class :terminal
@@ -164,6 +168,7 @@ module Arel
164
168
  def visit_Array o
165
169
  o.each { |i| visit i }
166
170
  end
171
+ alias :visit_Set :visit_Array
167
172
 
168
173
  def visit_Hash o
169
174
  o.each { |k,v| visit(k); visit(v) }
@@ -15,6 +15,7 @@ module Arel
15
15
  end
16
16
 
17
17
  def initialize
18
+ super()
18
19
  @nodes = []
19
20
  @edges = []
20
21
  @node_stack = []
@@ -216,6 +217,7 @@ module Arel
216
217
  edge(i) { visit x }
217
218
  end
218
219
  end
220
+ alias :visit_Set :visit_Array
219
221
 
220
222
  def visit_edge o, method
221
223
  edge(method) { visit o.send(method) }
@@ -41,7 +41,7 @@ module Arel
41
41
  visit o.expr, collector
42
42
  end
43
43
  def visit_Arel_Nodes_Limit o, collector
44
- collector << "LIMIT "
44
+ collector << "FIRST "
45
45
  visit o.expr, collector
46
46
  collector << " "
47
47
  end
@@ -69,12 +69,7 @@ module Arel
69
69
  collector = inject_join o.orders, collector, ', '
70
70
  end
71
71
 
72
- if o.limit
73
- collector << " "
74
- visit(o.limit, collector)
75
- else
76
- collector
77
- end
72
+ maybe_visit o.limit, collector
78
73
  end
79
74
 
80
75
  end
@@ -8,7 +8,7 @@ module Arel
8
8
 
9
9
  # if need to select first records without ORDER BY and GROUP BY and without DISTINCT
10
10
  # then can use simple ROWNUM in WHERE clause
11
- if o.limit && o.orders.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/
11
+ if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/
12
12
  o.cores.last.wheres.push Nodes::LessThanOrEqual.new(
13
13
  Nodes::SqlLiteral.new('ROWNUM'), o.limit.expr
14
14
  )
@@ -10,14 +10,14 @@ module Arel
10
10
  private
11
11
 
12
12
  def visit object, collector
13
- send dispatch[object.class], object, collector
13
+ send dispatch[object.class.name], object, collector
14
14
  rescue NoMethodError => e
15
- raise e if respond_to?(dispatch[object.class], true)
15
+ raise e if respond_to?(dispatch[object.class.name], true)
16
16
  superklass = object.class.ancestors.find { |klass|
17
- respond_to?(dispatch[klass], true)
17
+ respond_to?(dispatch[klass.name], true)
18
18
  }
19
19
  raise(TypeError, "Cannot visit #{object.class}") unless superklass
20
- dispatch[object.class] = dispatch[superklass]
20
+ dispatch[object.class.name] = dispatch[superklass.name]
21
21
  retry
22
22
  end
23
23
  end
@@ -59,10 +59,8 @@ module Arel
59
59
  DISTINCT = 'DISTINCT' # :nodoc:
60
60
 
61
61
  def initialize connection
62
+ super()
62
63
  @connection = connection
63
- @schema_cache = connection.schema_cache
64
- @quoted_tables = {}
65
- @quoted_columns = {}
66
64
  end
67
65
 
68
66
  def compile node, &block
@@ -71,6 +69,10 @@ module Arel
71
69
 
72
70
  private
73
71
 
72
+ def schema_cache
73
+ @connection.schema_cache
74
+ end
75
+
74
76
  def visit_Arel_Nodes_DeleteStatement o, collector
75
77
  collector << "DELETE FROM "
76
78
  collector = visit o.relation, collector
@@ -162,7 +164,7 @@ module Arel
162
164
  end
163
165
 
164
166
  def table_exists? name
165
- @schema_cache.table_exists? name
167
+ schema_cache.table_exists? name
166
168
  end
167
169
 
168
170
  def column_for attr
@@ -176,7 +178,7 @@ module Arel
176
178
  end
177
179
 
178
180
  def column_cache(table)
179
- @schema_cache.columns_hash(table)
181
+ schema_cache.columns_hash(table)
180
182
  end
181
183
 
182
184
  def visit_Arel_Nodes_Values o, collector
@@ -227,15 +229,9 @@ module Arel
227
229
  def visit_Arel_Nodes_SelectCore o, collector
228
230
  collector << "SELECT"
229
231
 
230
- if o.top
231
- collector << " "
232
- collector = visit o.top, collector
233
- end
232
+ collector = maybe_visit o.top, collector
234
233
 
235
- if o.set_quantifier
236
- collector << " "
237
- collector = visit o.set_quantifier, collector
238
- end
234
+ collector = maybe_visit o.set_quantifier, collector
239
235
 
240
236
  unless o.projections.empty?
241
237
  collector << SPACE
@@ -269,10 +265,7 @@ module Arel
269
265
  end
270
266
  end
271
267
 
272
- if o.having
273
- collector << " "
274
- collector = visit(o.having, collector)
275
- end
268
+ collector = maybe_visit o.having, collector
276
269
 
277
270
  unless o.windows.empty?
278
271
  collector << WINDOW
@@ -436,8 +429,12 @@ module Arel
436
429
  end
437
430
 
438
431
  def visit_Arel_Nodes_Grouping o, collector
439
- collector << "("
440
- visit(o.expr, collector) << ")"
432
+ if o.expr.is_a? Nodes::Grouping
433
+ visit(o.expr, collector)
434
+ else
435
+ collector << "("
436
+ visit(o.expr, collector) << ")"
437
+ end
441
438
  end
442
439
 
443
440
  def visit_Arel_SelectManager o, collector
@@ -471,14 +468,7 @@ module Arel
471
468
 
472
469
  def visit_Arel_Nodes_Extract o, collector
473
470
  collector << "EXTRACT(#{o.field.to_s.upcase} FROM "
474
- collector = visit o.expr, collector
475
- collector << ")"
476
- if o.alias
477
- collector << " AS "
478
- visit o.alias, collector
479
- else
480
- collector
481
- end
471
+ visit(o.expr, collector) << ")"
482
472
  end
483
473
 
484
474
  def visit_Arel_Nodes_Count o, collector
@@ -540,13 +530,25 @@ module Arel
540
530
  def visit_Arel_Nodes_Matches o, collector
541
531
  collector = visit o.left, collector
542
532
  collector << " LIKE "
543
- visit o.right, collector
533
+ collector = visit o.right, collector
534
+ if o.escape
535
+ collector << ' ESCAPE '
536
+ visit o.escape, collector
537
+ else
538
+ collector
539
+ end
544
540
  end
545
541
 
546
542
  def visit_Arel_Nodes_DoesNotMatch o, collector
547
543
  collector = visit o.left, collector
548
544
  collector << " NOT LIKE "
549
- visit o.right, collector
545
+ collector = visit o.right, collector
546
+ if o.escape
547
+ collector << ' ESCAPE '
548
+ visit o.escape, collector
549
+ else
550
+ collector
551
+ end
550
552
  end
551
553
 
552
554
  def visit_Arel_Nodes_JoinSource o, collector
@@ -755,6 +757,7 @@ module Arel
755
757
  def visit_Array o, collector
756
758
  inject_join o, collector, ", "
757
759
  end
760
+ alias :visit_Set :visit_Array
758
761
 
759
762
  def quote value, column = nil
760
763
  return value if Arel::Nodes::SqlLiteral === value
@@ -763,11 +766,12 @@ module Arel
763
766
 
764
767
  def quote_table_name name
765
768
  return name if Arel::Nodes::SqlLiteral === name
766
- @quoted_tables[name] ||= @connection.quote_table_name(name)
769
+ @connection.quote_table_name(name)
767
770
  end
768
771
 
769
772
  def quote_column_name name
770
- @quoted_columns[name] ||= Arel::Nodes::SqlLiteral === name ? name : @connection.quote_column_name(name)
773
+ return name if Arel::Nodes::SqlLiteral === name
774
+ @connection.quote_column_name(name)
771
775
  end
772
776
 
773
777
  def maybe_visit thing, collector
@@ -1,32 +1,39 @@
1
1
  module Arel
2
2
  module Visitors
3
3
  class Visitor
4
+ def initialize
5
+ @dispatch = Hash.new do |hash, class_name|
6
+ raise if class_name == 'Arel::Nodes::Union'
7
+ hash[class_name] = "visit_#{(class_name || '').gsub('::', '_')}"
8
+ end
9
+
10
+ # pre-populate cache. FIXME: this should be passed in to each
11
+ # instance, but we can do that later.
12
+ self.class.private_instance_methods.sort.each do |name|
13
+ next unless name =~ /^visit_(.*)$/
14
+ @dispatch[$1.gsub('_', '::')] = name
15
+ end
16
+ end
17
+
4
18
  def accept object
5
19
  visit object
6
20
  end
7
21
 
8
22
  private
9
23
 
10
- DISPATCH = Hash.new do |hash, visitor_class|
11
- hash[visitor_class] =
12
- Hash.new do |method_hash, node_class|
13
- method_hash[node_class] = "visit_#{(node_class.name || '').gsub('::', '_')}"
14
- end
15
- end
16
-
17
24
  def dispatch
18
- DISPATCH[self.class]
25
+ @dispatch
19
26
  end
20
27
 
21
28
  def visit object
22
- send dispatch[object.class], object
29
+ send dispatch[object.class.name], object
23
30
  rescue NoMethodError => e
24
- raise e if respond_to?(dispatch[object.class], true)
31
+ raise e if respond_to?(dispatch[object.class.name], true)
25
32
  superklass = object.class.ancestors.find { |klass|
26
- respond_to?(dispatch[klass], true)
33
+ respond_to?(dispatch[klass.name], true)
27
34
  }
28
35
  raise(TypeError, "Cannot visit #{object.class}") unless superklass
29
- dispatch[object.class] = dispatch[superklass]
36
+ dispatch[object.class.name] = dispatch[superklass.name]
30
37
  retry
31
38
  end
32
39
  end
@@ -1,6 +1,8 @@
1
1
  module Arel
2
2
  module Visitors
3
3
  class WhereSql < Arel::Visitors::ToSql
4
+ private
5
+
4
6
  def visit_Arel_Nodes_SelectCore o, collector
5
7
  collector << "WHERE "
6
8
  inject_join o.wheres, collector, ' AND '
@@ -66,7 +66,7 @@ module Arel
66
66
  relation[:id].gt(10).must_be_kind_of Nodes::GreaterThan
67
67
  end
68
68
 
69
- it 'should generate >= in sql' do
69
+ it 'should generate > in sql' do
70
70
  relation = Table.new(:users)
71
71
  mgr = relation.project relation[:id]
72
72
  mgr.where relation[:id].gt(10)
@@ -85,6 +85,17 @@ module Arel
85
85
  SELECT * FROM "users" WHERE "users"."karma" > (SELECT AVG("users"."karma") AS avg_id FROM "users")
86
86
  }
87
87
  end
88
+
89
+ it 'should accept various data types.' do
90
+ relation = Table.new(:users)
91
+ mgr = relation.project relation[:id]
92
+ mgr.where relation[:name].gt('fake_name')
93
+ mgr.to_sql.must_match %{"users"."name" > 'fake_name'}
94
+
95
+ current_time = ::Time.now
96
+ mgr.where relation[:created_at].gt(current_time)
97
+ mgr.to_sql.must_match %{"users"."created_at" > '#{current_time}'}
98
+ end
88
99
  end
89
100
 
90
101
  describe '#gt_any' do
@@ -133,6 +144,17 @@ module Arel
133
144
  SELECT "users"."id" FROM "users" WHERE "users"."id" >= 10
134
145
  }
135
146
  end
147
+
148
+ it 'should accept various data types.' do
149
+ relation = Table.new(:users)
150
+ mgr = relation.project relation[:id]
151
+ mgr.where relation[:name].gteq('fake_name')
152
+ mgr.to_sql.must_match %{"users"."name" >= 'fake_name'}
153
+
154
+ current_time = ::Time.now
155
+ mgr.where relation[:created_at].gteq(current_time)
156
+ mgr.to_sql.must_match %{"users"."created_at" >= '#{current_time}'}
157
+ end
136
158
  end
137
159
 
138
160
  describe '#gteq_any' do
@@ -181,6 +203,17 @@ module Arel
181
203
  SELECT "users"."id" FROM "users" WHERE "users"."id" < 10
182
204
  }
183
205
  end
206
+
207
+ it 'should accept various data types.' do
208
+ relation = Table.new(:users)
209
+ mgr = relation.project relation[:id]
210
+ mgr.where relation[:name].lt('fake_name')
211
+ mgr.to_sql.must_match %{"users"."name" < 'fake_name'}
212
+
213
+ current_time = ::Time.now
214
+ mgr.where relation[:created_at].lt(current_time)
215
+ mgr.to_sql.must_match %{"users"."created_at" < '#{current_time}'}
216
+ end
184
217
  end
185
218
 
186
219
  describe '#lt_any' do
@@ -229,6 +262,17 @@ module Arel
229
262
  SELECT "users"."id" FROM "users" WHERE "users"."id" <= 10
230
263
  }
231
264
  end
265
+
266
+ it 'should accept various data types.' do
267
+ relation = Table.new(:users)
268
+ mgr = relation.project relation[:id]
269
+ mgr.where relation[:name].lteq('fake_name')
270
+ mgr.to_sql.must_match %{"users"."name" <= 'fake_name'}
271
+
272
+ current_time = ::Time.now
273
+ mgr.where relation[:created_at].lteq(current_time)
274
+ mgr.to_sql.must_match %{"users"."created_at" <= '#{current_time}'}
275
+ end
232
276
  end
233
277
 
234
278
  describe '#lteq_any' do
@@ -507,15 +551,109 @@ module Arel
507
551
  end
508
552
  end
509
553
 
554
+ describe 'with a range' do
555
+ it 'can be constructed with a standard range' do
556
+ attribute = Attribute.new nil, nil
557
+ node = attribute.between(1..3)
558
+
559
+ node.must_equal Nodes::Between.new(
560
+ attribute,
561
+ Nodes::And.new([
562
+ Nodes::Casted.new(1, attribute),
563
+ Nodes::Casted.new(3, attribute)
564
+ ])
565
+ )
566
+ end
567
+
568
+ it 'can be constructed with a range starting from -Infinity' do
569
+ attribute = Attribute.new nil, nil
570
+ node = attribute.between(-::Float::INFINITY..3)
571
+
572
+ node.must_equal Nodes::LessThanOrEqual.new(
573
+ attribute,
574
+ Nodes::Casted.new(3, attribute)
575
+ )
576
+ end
577
+
578
+ it 'can be constructed with an exclusive range starting from -Infinity' do
579
+ attribute = Attribute.new nil, nil
580
+ node = attribute.between(-::Float::INFINITY...3)
581
+
582
+ node.must_equal Nodes::LessThan.new(
583
+ attribute,
584
+ Nodes::Casted.new(3, attribute)
585
+ )
586
+ end
587
+
588
+ it 'can be constructed with an infinite range' do
589
+ attribute = Attribute.new nil, nil
590
+ node = attribute.between(-::Float::INFINITY..::Float::INFINITY)
591
+
592
+ node.must_equal Nodes::NotIn.new(attribute, [])
593
+ end
594
+
595
+ it 'can be constructed with a range ending at Infinity' do
596
+ attribute = Attribute.new nil, nil
597
+ node = attribute.between(0..::Float::INFINITY)
598
+
599
+ node.must_equal Nodes::GreaterThanOrEqual.new(
600
+ attribute,
601
+ Nodes::Casted.new(0, attribute)
602
+ )
603
+ end
604
+
605
+ it 'can be constructed with an exclusive range' do
606
+ attribute = Attribute.new nil, nil
607
+ node = attribute.between(0...3)
608
+
609
+ node.must_equal Nodes::And.new([
610
+ Nodes::GreaterThanOrEqual.new(
611
+ attribute,
612
+ Nodes::Casted.new(0, attribute)
613
+ ),
614
+ Nodes::LessThan.new(
615
+ attribute,
616
+ Nodes::Casted.new(3, attribute)
617
+ )
618
+ ])
619
+ end
620
+ end
621
+
510
622
  describe '#in' do
623
+ it 'can be constructed with a subquery' do
624
+ relation = Table.new(:users)
625
+ mgr = relation.project relation[:id]
626
+ mgr.where relation[:name].does_not_match_all(['%chunky%','%bacon%'])
627
+ attribute = Attribute.new nil, nil
628
+
629
+ node = attribute.in(mgr)
630
+
631
+ node.must_equal Nodes::In.new(attribute, mgr.ast)
632
+ end
633
+
511
634
  it 'can be constructed with a list' do
635
+ attribute = Attribute.new nil, nil
636
+ node = attribute.in([1, 2, 3])
637
+
638
+ node.must_equal Nodes::In.new(
639
+ attribute,
640
+ [
641
+ Nodes::Casted.new(1, attribute),
642
+ Nodes::Casted.new(2, attribute),
643
+ Nodes::Casted.new(3, attribute),
644
+ ]
645
+ )
512
646
  end
513
647
 
514
- it 'should return an in node' do
648
+ it 'can be constructed with a random object' do
515
649
  attribute = Attribute.new nil, nil
516
- node = Nodes::In.new attribute, [1,2,3]
517
- node.left.must_equal attribute
518
- node.right.must_equal [1, 2, 3]
650
+ random_object = Object.new
651
+ node = attribute.in(random_object)
652
+
653
+ node.must_equal Nodes::In.new(
654
+ attribute,
655
+ Nodes::Casted.new(random_object, attribute)
656
+ )
519
657
  end
520
658
 
521
659
  it 'should generate IN in sql' do
@@ -560,13 +698,112 @@ module Arel
560
698
  end
561
699
  end
562
700
 
701
+ describe 'with a range' do
702
+ it 'can be constructed with a standard range' do
703
+ attribute = Attribute.new nil, nil
704
+ node = attribute.not_between(1..3)
705
+
706
+ node.must_equal Nodes::Grouping.new(Nodes::Or.new(
707
+ Nodes::LessThan.new(
708
+ attribute,
709
+ Nodes::Casted.new(1, attribute)
710
+ ),
711
+ Nodes::GreaterThan.new(
712
+ attribute,
713
+ Nodes::Casted.new(3, attribute)
714
+ )
715
+ ))
716
+ end
717
+
718
+ it 'can be constructed with a range starting from -Infinity' do
719
+ attribute = Attribute.new nil, nil
720
+ node = attribute.not_between(-::Float::INFINITY..3)
721
+
722
+ node.must_equal Nodes::GreaterThan.new(
723
+ attribute,
724
+ Nodes::Casted.new(3, attribute)
725
+ )
726
+ end
727
+
728
+ it 'can be constructed with an exclusive range starting from -Infinity' do
729
+ attribute = Attribute.new nil, nil
730
+ node = attribute.not_between(-::Float::INFINITY...3)
731
+
732
+ node.must_equal Nodes::GreaterThanOrEqual.new(
733
+ attribute,
734
+ Nodes::Casted.new(3, attribute)
735
+ )
736
+ end
737
+
738
+ it 'can be constructed with an infinite range' do
739
+ attribute = Attribute.new nil, nil
740
+ node = attribute.not_between(-::Float::INFINITY..::Float::INFINITY)
741
+
742
+ node.must_equal Nodes::In.new(attribute, [])
743
+ end
744
+
745
+ it 'can be constructed with a range ending at Infinity' do
746
+ attribute = Attribute.new nil, nil
747
+ node = attribute.not_between(0..::Float::INFINITY)
748
+
749
+ node.must_equal Nodes::LessThan.new(
750
+ attribute,
751
+ Nodes::Casted.new(0, attribute)
752
+ )
753
+ end
754
+
755
+ it 'can be constructed with an exclusive range' do
756
+ attribute = Attribute.new nil, nil
757
+ node = attribute.not_between(0...3)
758
+
759
+ node.must_equal Nodes::Grouping.new(Nodes::Or.new(
760
+ Nodes::LessThan.new(
761
+ attribute,
762
+ Nodes::Casted.new(0, attribute)
763
+ ),
764
+ Nodes::GreaterThanOrEqual.new(
765
+ attribute,
766
+ Nodes::Casted.new(3, attribute)
767
+ )
768
+ ))
769
+ end
770
+ end
771
+
563
772
  describe '#not_in' do
773
+ it 'can be constructed with a subquery' do
774
+ relation = Table.new(:users)
775
+ mgr = relation.project relation[:id]
776
+ mgr.where relation[:name].does_not_match_all(['%chunky%','%bacon%'])
777
+ attribute = Attribute.new nil, nil
778
+
779
+ node = attribute.not_in(mgr)
564
780
 
565
- it 'should return a NotIn node' do
781
+ node.must_equal Nodes::NotIn.new(attribute, mgr.ast)
782
+ end
783
+
784
+ it 'can be constructed with a list' do
566
785
  attribute = Attribute.new nil, nil
567
- node = Nodes::NotIn.new attribute, [1,2,3]
568
- node.left.must_equal attribute
569
- node.right.must_equal [1, 2, 3]
786
+ node = attribute.not_in([1, 2, 3])
787
+
788
+ node.must_equal Nodes::NotIn.new(
789
+ attribute,
790
+ [
791
+ Nodes::Casted.new(1, attribute),
792
+ Nodes::Casted.new(2, attribute),
793
+ Nodes::Casted.new(3, attribute),
794
+ ]
795
+ )
796
+ end
797
+
798
+ it 'can be constructed with a random object' do
799
+ attribute = Attribute.new nil, nil
800
+ random_object = Object.new
801
+ node = attribute.not_in(random_object)
802
+
803
+ node.must_equal Nodes::NotIn.new(
804
+ attribute,
805
+ Nodes::Casted.new(random_object, attribute)
806
+ )
570
807
  end
571
808
 
572
809
  it 'should generate NOT IN in sql' do