arel 6.0.0.beta1 → 6.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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