arel 5.0.1.20140414130214 → 6.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -2
  3. data/History.txt +9 -4
  4. data/Manifest.txt +9 -7
  5. data/README.markdown +85 -8
  6. data/Rakefile +1 -1
  7. data/arel.gemspec +15 -16
  8. data/lib/arel.rb +1 -12
  9. data/lib/arel/collectors/bind.rb +36 -0
  10. data/lib/arel/collectors/plain_string.rb +18 -0
  11. data/lib/arel/collectors/sql_string.rb +18 -0
  12. data/lib/arel/factory_methods.rb +1 -1
  13. data/lib/arel/insert_manager.rb +5 -1
  14. data/lib/arel/nodes.rb +41 -0
  15. data/lib/arel/nodes/and.rb +1 -5
  16. data/lib/arel/nodes/binary.rb +2 -0
  17. data/lib/arel/nodes/extract.rb +0 -2
  18. data/lib/arel/nodes/full_outer_join.rb +6 -0
  19. data/lib/arel/nodes/function.rb +0 -1
  20. data/lib/arel/nodes/insert_statement.rb +5 -2
  21. data/lib/arel/nodes/node.rb +5 -1
  22. data/lib/arel/nodes/right_outer_join.rb +6 -0
  23. data/lib/arel/nodes/window.rb +23 -5
  24. data/lib/arel/predications.rb +41 -33
  25. data/lib/arel/select_manager.rb +13 -37
  26. data/lib/arel/table.rb +13 -9
  27. data/lib/arel/tree_manager.rb +8 -2
  28. data/lib/arel/update_manager.rb +2 -2
  29. data/lib/arel/visitors.rb +0 -2
  30. data/lib/arel/visitors/bind_substitute.rb +9 -0
  31. data/lib/arel/visitors/bind_visitor.rb +10 -5
  32. data/lib/arel/visitors/depth_first.rb +60 -57
  33. data/lib/arel/visitors/dot.rb +84 -80
  34. data/lib/arel/visitors/ibm_db.rb +4 -2
  35. data/lib/arel/visitors/informix.rb +39 -21
  36. data/lib/arel/visitors/mssql.rb +41 -23
  37. data/lib/arel/visitors/mysql.rb +48 -22
  38. data/lib/arel/visitors/oracle.rb +33 -24
  39. data/lib/arel/visitors/postgresql.rb +15 -8
  40. data/lib/arel/visitors/reduce.rb +25 -0
  41. data/lib/arel/visitors/sqlite.rb +3 -2
  42. data/lib/arel/visitors/to_sql.rb +455 -248
  43. data/lib/arel/visitors/visitor.rb +2 -2
  44. data/lib/arel/visitors/where_sql.rb +3 -2
  45. data/test/attributes/test_attribute.rb +12 -3
  46. data/test/collectors/test_bind_collector.rb +70 -0
  47. data/test/collectors/test_sql_string.rb +38 -0
  48. data/test/helper.rb +10 -1
  49. data/test/nodes/test_bin.rb +2 -2
  50. data/test/nodes/test_count.rb +0 -6
  51. data/test/nodes/test_equality.rb +1 -1
  52. data/test/nodes/test_grouping.rb +1 -1
  53. data/test/nodes/test_infix_operation.rb +1 -1
  54. data/test/nodes/test_select_core.rb +7 -7
  55. data/test/nodes/test_sql_literal.rb +10 -6
  56. data/test/nodes/test_window.rb +9 -3
  57. data/test/support/fake_record.rb +16 -4
  58. data/test/test_factory_methods.rb +1 -1
  59. data/test/test_insert_manager.rb +33 -4
  60. data/test/test_select_manager.rb +164 -92
  61. data/test/test_table.rb +49 -4
  62. data/test/visitors/test_bind_visitor.rb +18 -10
  63. data/test/visitors/test_depth_first.rb +12 -0
  64. data/test/visitors/test_dot.rb +4 -4
  65. data/test/visitors/test_ibm_db.rb +11 -5
  66. data/test/visitors/test_informix.rb +14 -8
  67. data/test/visitors/test_mssql.rb +12 -8
  68. data/test/visitors/test_mysql.rb +17 -12
  69. data/test/visitors/test_oracle.rb +25 -21
  70. data/test/visitors/test_postgres.rb +50 -12
  71. data/test/visitors/test_sqlite.rb +2 -2
  72. data/test/visitors/test_to_sql.rb +177 -81
  73. metadata +24 -19
  74. data/lib/arel/deprecated.rb +0 -4
  75. data/lib/arel/expression.rb +0 -5
  76. data/lib/arel/sql/engine.rb +0 -10
  77. data/lib/arel/sql_literal.rb +0 -4
  78. data/lib/arel/visitors/join_sql.rb +0 -19
  79. data/lib/arel/visitors/order_clauses.rb +0 -11
  80. data/test/visitors/test_join_sql.rb +0 -42
@@ -3,8 +3,10 @@ module Arel
3
3
  class IBM_DB < Arel::Visitors::ToSql
4
4
  private
5
5
 
6
- def visit_Arel_Nodes_Limit o, a
7
- "FETCH FIRST #{visit o.expr, a} ROWS ONLY"
6
+ def visit_Arel_Nodes_Limit o, collector
7
+ collector << "FETCH FIRST "
8
+ collector = visit o.expr, collector
9
+ collector << " ROWS ONLY"
8
10
  end
9
11
 
10
12
  end
@@ -2,30 +2,48 @@ module Arel
2
2
  module Visitors
3
3
  class Informix < Arel::Visitors::ToSql
4
4
  private
5
- def visit_Arel_Nodes_SelectStatement o, a
6
- [
7
- "SELECT",
8
- (visit(o.offset, a) if o.offset),
9
- (visit(o.limit, a) if o.limit),
10
- o.cores.map { |x| visit_Arel_Nodes_SelectCore x, a }.join,
11
- ("ORDER BY #{o.orders.map { |x| visit x, a }.join(', ')}" unless o.orders.empty?),
12
- (visit(o.lock, a) if o.lock),
13
- ].compact.join ' '
5
+ def visit_Arel_Nodes_SelectStatement o, collector
6
+ collector << "SELECT "
7
+ collector = maybe_visit o.offset, collector
8
+ collector = maybe_visit o.limit, collector
9
+ collector = o.cores.inject(collector) { |c,x|
10
+ visit_Arel_Nodes_SelectCore x, c
11
+ }
12
+ if o.orders.any?
13
+ collector << "ORDER BY "
14
+ collector = inject_join o.orders, collector, ", "
15
+ end
16
+ collector = maybe_visit o.lock, collector
14
17
  end
15
- def visit_Arel_Nodes_SelectCore o, a
16
- [
17
- "#{o.projections.map { |x| visit x, a }.join ', '}",
18
- ("FROM #{visit(o.source, a)}" if o.source && !o.source.empty?),
19
- ("WHERE #{o.wheres.map { |x| visit x, a }.join ' AND ' }" unless o.wheres.empty?),
20
- ("GROUP BY #{o.groups.map { |x| visit x, a }.join ', ' }" unless o.groups.empty?),
21
- (visit(o.having, a) if o.having),
22
- ].compact.join ' '
18
+ def visit_Arel_Nodes_SelectCore o, collector
19
+ collector = inject_join o.projections, collector, ", "
20
+ froms = false
21
+ if o.source && !o.source.empty?
22
+ froms = true
23
+ collector << " FROM "
24
+ collector = visit o.source, collector
25
+ end
26
+
27
+ if o.wheres.any?
28
+ collector << " WHERE "
29
+ collector = inject_join o.wheres, collector, " AND "
30
+ end
31
+
32
+ if o.groups.any?
33
+ collector << "GROUP BY "
34
+ collector = inject_join o.groups, collector, ", "
35
+ end
36
+
37
+ maybe_visit o.having, collector
23
38
  end
24
- def visit_Arel_Nodes_Offset o, a
25
- "SKIP #{visit o.expr, a}"
39
+ def visit_Arel_Nodes_Offset o, collector
40
+ collector << "SKIP "
41
+ visit o.expr, collector
26
42
  end
27
- def visit_Arel_Nodes_Limit o, a
28
- "LIMIT #{visit o.expr, a}"
43
+ def visit_Arel_Nodes_Limit o, collector
44
+ collector << "LIMIT "
45
+ visit o.expr, collector
46
+ collector << " "
29
47
  end
30
48
  end
31
49
  end
@@ -1,39 +1,54 @@
1
1
  module Arel
2
2
  module Visitors
3
3
  class MSSQL < Arel::Visitors::ToSql
4
+ RowNumber = Struct.new :children
5
+
4
6
  private
5
7
 
6
8
  # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate
7
9
  # "select top 10 distinct first_name from users", which is invalid query! it should be
8
10
  # "select distinct top 10 first_name from users"
9
- def visit_Arel_Nodes_Top o, a
11
+ def visit_Arel_Nodes_Top o
10
12
  ""
11
13
  end
12
14
 
13
- def visit_Arel_Nodes_SelectStatement o, a
15
+ def visit_Arel_Visitors_MSSQL_RowNumber o, collector
16
+ collector << "ROW_NUMBER() OVER (ORDER BY "
17
+ inject_join(o.children, collector, ', ') << ") as _row_num"
18
+ end
19
+
20
+ def visit_Arel_Nodes_SelectStatement o, collector
14
21
  if !o.limit && !o.offset
15
- return super o, a
22
+ return super
16
23
  end
17
24
 
18
- select_order_by = "ORDER BY #{o.orders.map { |x| visit x, a }.join(', ')}" unless o.orders.empty?
19
-
20
25
  is_select_count = false
21
- sql = o.cores.map { |x|
22
- core_order_by = select_order_by || determine_order_by(x, a)
26
+ o.cores.each { |x|
27
+ core_order_by = row_num_literal determine_order_by(o.orders, x)
23
28
  if select_count? x
24
- x.projections = [row_num_literal(core_order_by)]
29
+ x.projections = [core_order_by]
25
30
  is_select_count = true
26
31
  else
27
- x.projections << row_num_literal(core_order_by)
32
+ x.projections << core_order_by
28
33
  end
34
+ }
29
35
 
30
- visit_Arel_Nodes_SelectCore x, a
31
- }.join
36
+ if is_select_count
37
+ # fixme count distinct wouldn't work with limit or offset
38
+ collector << "SELECT COUNT(1) as count_id FROM ("
39
+ end
40
+
41
+ collector << "SELECT _t.* FROM ("
42
+ collector = o.cores.inject(collector) { |c,x|
43
+ visit_Arel_Nodes_SelectCore x, c
44
+ }
45
+ collector << ") as _t WHERE #{get_offset_limit_clause(o)}"
32
46
 
33
- sql = "SELECT _t.* FROM (#{sql}) as _t WHERE #{get_offset_limit_clause(o)}"
34
- # fixme count distinct wouldn't work with limit or offset
35
- sql = "SELECT COUNT(1) as count_id FROM (#{sql}) AS subquery" if is_select_count
36
- sql
47
+ if is_select_count
48
+ collector << ") AS subquery"
49
+ else
50
+ collector
51
+ end
37
52
  end
38
53
 
39
54
  def get_offset_limit_clause o
@@ -46,16 +61,19 @@ module Arel
46
61
  end
47
62
  end
48
63
 
49
- def determine_order_by x, a
50
- unless x.groups.empty?
51
- "ORDER BY #{x.groups.map { |g| visit g, a }.join ', ' }"
64
+ def determine_order_by orders, x
65
+ if orders.any?
66
+ orders
67
+ elsif x.groups.any?
68
+ x.groups
52
69
  else
53
- "ORDER BY #{find_left_table_pk(x.froms, a)}"
70
+ pk = find_left_table_pk(x.froms)
71
+ pk ? [pk] : []
54
72
  end
55
73
  end
56
74
 
57
75
  def row_num_literal order_by
58
- Nodes::SqlLiteral.new("ROW_NUMBER() OVER (#{order_by}) as _row_num")
76
+ RowNumber.new order_by
59
77
  end
60
78
 
61
79
  def select_count? x
@@ -64,9 +82,9 @@ module Arel
64
82
 
65
83
  # FIXME raise exception of there is no pk?
66
84
  # FIXME!! Table.primary_key will be deprecated. What is the replacement??
67
- def find_left_table_pk o, a
68
- return visit o.primary_key, a if o.instance_of? Arel::Table
69
- find_left_table_pk o.left, a if o.kind_of? Arel::Nodes::Join
85
+ def find_left_table_pk o
86
+ return o.primary_key if o.instance_of? Arel::Table
87
+ find_left_table_pk o.left if o.kind_of? Arel::Nodes::Join
70
88
  end
71
89
  end
72
90
  end
@@ -2,53 +2,79 @@ module Arel
2
2
  module Visitors
3
3
  class MySQL < Arel::Visitors::ToSql
4
4
  private
5
- def visit_Arel_Nodes_Union o, a, suppress_parens = false
6
- left_result = case o.left
5
+ def visit_Arel_Nodes_Union o, collector, suppress_parens = false
6
+ unless suppress_parens
7
+ collector << "( "
8
+ end
9
+
10
+ collector = case o.left
7
11
  when Arel::Nodes::Union
8
- visit_Arel_Nodes_Union o.left, a, true
12
+ visit_Arel_Nodes_Union o.left, collector, true
9
13
  else
10
- visit o.left, a
14
+ visit o.left, collector
11
15
  end
12
16
 
13
- right_result = case o.right
17
+ collector << " UNION "
18
+
19
+ collector = case o.right
14
20
  when Arel::Nodes::Union
15
- visit_Arel_Nodes_Union o.right, a, true
21
+ visit_Arel_Nodes_Union o.right, collector, true
16
22
  else
17
- visit o.right, a
23
+ visit o.right, collector
18
24
  end
19
25
 
20
26
  if suppress_parens
21
- "#{left_result} UNION #{right_result}"
27
+ collector
22
28
  else
23
- "( #{left_result} UNION #{right_result} )"
29
+ collector << " )"
24
30
  end
25
31
  end
26
32
 
27
- def visit_Arel_Nodes_Bin o, a
28
- "BINARY #{visit o.expr, a}"
33
+ def visit_Arel_Nodes_Bin o, collector
34
+ collector << "BINARY "
35
+ visit o.expr, collector
29
36
  end
30
37
 
31
38
  ###
32
39
  # :'(
33
40
  # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
34
- def visit_Arel_Nodes_SelectStatement o, a
35
- o.limit = Arel::Nodes::Limit.new(18446744073709551615) if o.offset && !o.limit
41
+ def visit_Arel_Nodes_SelectStatement o, collector
42
+ if o.offset && !o.limit
43
+ o.limit = Arel::Nodes::Limit.new(Nodes.build_quoted(18446744073709551615))
44
+ end
36
45
  super
37
46
  end
38
47
 
39
- def visit_Arel_Nodes_SelectCore o, a
48
+ def visit_Arel_Nodes_SelectCore o, collector
40
49
  o.froms ||= Arel.sql('DUAL')
41
50
  super
42
51
  end
43
52
 
44
- def visit_Arel_Nodes_UpdateStatement o, a
45
- [
46
- "UPDATE #{visit o.relation, a}",
47
- ("SET #{o.values.map { |value| visit value, a }.join ', '}" unless o.values.empty?),
48
- ("WHERE #{o.wheres.map { |x| visit x, a }.join ' AND '}" unless o.wheres.empty?),
49
- ("ORDER BY #{o.orders.map { |x| visit x, a }.join(', ')}" unless o.orders.empty?),
50
- (visit(o.limit, a) if o.limit),
51
- ].compact.join ' '
53
+ def visit_Arel_Nodes_UpdateStatement o, collector
54
+ collector << "UPDATE "
55
+ collector = visit o.relation, collector
56
+
57
+ unless o.values.empty?
58
+ collector << " SET "
59
+ collector = inject_join o.values, collector, ', '
60
+ end
61
+
62
+ unless o.wheres.empty?
63
+ collector << " WHERE "
64
+ collector = inject_join o.wheres, collector, ' AND '
65
+ end
66
+
67
+ unless o.orders.empty?
68
+ collector << " ORDER BY "
69
+ collector = inject_join o.orders, collector, ', '
70
+ end
71
+
72
+ if o.limit
73
+ collector << " "
74
+ visit(o.limit, collector)
75
+ else
76
+ collector
77
+ end
52
78
  end
53
79
 
54
80
  end
@@ -3,8 +3,8 @@ module Arel
3
3
  class Oracle < Arel::Visitors::ToSql
4
4
  private
5
5
 
6
- def visit_Arel_Nodes_SelectStatement o, a
7
- o = order_hacks(o, a)
6
+ def visit_Arel_Nodes_SelectStatement o, collector
7
+ o = order_hacks(o)
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
@@ -17,55 +17,64 @@ module Arel
17
17
 
18
18
  if o.limit && o.offset
19
19
  o = o.dup
20
- limit = o.limit.expr.to_i
20
+ limit = o.limit.expr.expr
21
21
  offset = o.offset
22
22
  o.offset = nil
23
- sql = super(o, a)
24
- return <<-eosql
23
+ collector << "
25
24
  SELECT * FROM (
26
25
  SELECT raw_sql_.*, rownum raw_rnum_
27
- FROM (#{sql}) raw_sql_
26
+ FROM ("
27
+
28
+ collector = super(o, collector)
29
+ collector << ") raw_sql_
28
30
  WHERE rownum <= #{offset.expr.to_i + limit}
29
31
  )
30
- WHERE #{visit offset, a}
31
- eosql
32
+ WHERE "
33
+ return visit(offset, collector)
32
34
  end
33
35
 
34
36
  if o.limit
35
37
  o = o.dup
36
38
  limit = o.limit.expr
37
- return "SELECT * FROM (#{super(o, a)}) WHERE ROWNUM <= #{visit limit, a}"
39
+ collector << "SELECT * FROM ("
40
+ collector = super(o, collector)
41
+ collector << ") WHERE ROWNUM <= "
42
+ return visit limit, collector
38
43
  end
39
44
 
40
45
  if o.offset
41
46
  o = o.dup
42
47
  offset = o.offset
43
48
  o.offset = nil
44
- sql = super(o, a)
45
- return <<-eosql
46
- SELECT * FROM (
49
+ collector << "SELECT * FROM (
47
50
  SELECT raw_sql_.*, rownum raw_rnum_
48
- FROM (#{sql}) raw_sql_
51
+ FROM ("
52
+ collector = super(o, collector)
53
+ collector << ") raw_sql_
49
54
  )
50
- WHERE #{visit offset, a}
51
- eosql
55
+ WHERE "
56
+ return visit offset, collector
52
57
  end
53
58
 
54
59
  super
55
60
  end
56
61
 
57
- def visit_Arel_Nodes_Limit o, a
62
+ def visit_Arel_Nodes_Limit o, collector
63
+ collector
58
64
  end
59
65
 
60
- def visit_Arel_Nodes_Offset o, a
61
- "raw_rnum_ > #{visit o.expr, a}"
66
+ def visit_Arel_Nodes_Offset o, collector
67
+ collector << "raw_rnum_ > "
68
+ visit o.expr, collector
62
69
  end
63
70
 
64
- def visit_Arel_Nodes_Except o, a
65
- "( #{visit o.left, a} MINUS #{visit o.right, a} )"
71
+ def visit_Arel_Nodes_Except o, collector
72
+ collector << "( "
73
+ collector = infix_value o, collector, " MINUS "
74
+ collector << " )"
66
75
  end
67
76
 
68
- def visit_Arel_Nodes_UpdateStatement o, a
77
+ def visit_Arel_Nodes_UpdateStatement o, collector
69
78
  # Oracle does not allow ORDER BY/LIMIT in UPDATEs.
70
79
  if o.orders.any? && o.limit.nil?
71
80
  # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
@@ -79,7 +88,7 @@ module Arel
79
88
 
80
89
  ###
81
90
  # Hacks for the order clauses specific to Oracle
82
- def order_hacks o, a
91
+ def order_hacks o
83
92
  return o if o.orders.empty?
84
93
  return o unless o.cores.any? do |core|
85
94
  core.projections.any? do |projection|
@@ -89,9 +98,9 @@ module Arel
89
98
  # Previous version with join and split broke ORDER BY clause
90
99
  # if it contained functions with several arguments (separated by ',').
91
100
  #
92
- # orders = o.orders.map { |x| visit x, a }.join(', ').split(',')
101
+ # orders = o.orders.map { |x| visit x }.join(', ').split(',')
93
102
  orders = o.orders.map do |x|
94
- string = visit x, a
103
+ string = visit(x, Arel::Collectors::SQLString.new).value
95
104
  if string.include?(',')
96
105
  split_order_string(string)
97
106
  else
@@ -3,18 +3,25 @@ module Arel
3
3
  class PostgreSQL < Arel::Visitors::ToSql
4
4
  private
5
5
 
6
- def visit_Arel_Nodes_Matches o, a
7
- a = o.left if Arel::Attributes::Attribute === o.left
8
- "#{visit o.left, a} ILIKE #{visit o.right, a}"
6
+ def visit_Arel_Nodes_Matches o, collector
7
+ infix_value o, collector, ' ILIKE '
9
8
  end
10
9
 
11
- def visit_Arel_Nodes_DoesNotMatch o, a
12
- a = o.left if Arel::Attributes::Attribute === o.left
13
- "#{visit o.left, a} NOT ILIKE #{visit o.right, a}"
10
+ def visit_Arel_Nodes_DoesNotMatch o, collector
11
+ infix_value o, collector, ' NOT ILIKE '
14
12
  end
15
13
 
16
- def visit_Arel_Nodes_DistinctOn o, a
17
- "DISTINCT ON ( #{visit o.expr, a} )"
14
+ def visit_Arel_Nodes_Regexp o, collector
15
+ infix_value o, collector, ' ~ '
16
+ end
17
+
18
+ def visit_Arel_Nodes_NotRegexp o, collector
19
+ infix_value o, collector, ' !~ '
20
+ end
21
+
22
+ def visit_Arel_Nodes_DistinctOn o, collector
23
+ collector << "DISTINCT ON ( "
24
+ visit(o.expr, collector) << " )"
18
25
  end
19
26
  end
20
27
  end
@@ -0,0 +1,25 @@
1
+ require 'arel/visitors/visitor'
2
+
3
+ module Arel
4
+ module Visitors
5
+ class Reduce < Arel::Visitors::Visitor
6
+ def accept object, collector
7
+ visit object, collector
8
+ end
9
+
10
+ private
11
+
12
+ def visit object, collector
13
+ send dispatch[object.class], object, collector
14
+ rescue NoMethodError => e
15
+ raise e if respond_to?(dispatch[object.class], true)
16
+ superklass = object.class.ancestors.find { |klass|
17
+ respond_to?(dispatch[klass], true)
18
+ }
19
+ raise(TypeError, "Cannot visit #{object.class}") unless superklass
20
+ dispatch[object.class] = dispatch[superklass]
21
+ retry
22
+ end
23
+ end
24
+ end
25
+ end