arel 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/History.txt +8 -0
  2. data/README.markdown +0 -2
  3. data/Rakefile +2 -2
  4. data/arel.gemspec +13 -3
  5. data/lib/arel.rb +2 -2
  6. data/lib/arel/algebra/relations/relation.rb +3 -2
  7. data/lib/arel/engines/sql/compilers/ibm_db_compiler.rb +62 -0
  8. data/lib/arel/engines/sql/compilers/mysql_compiler.rb +11 -0
  9. data/lib/arel/engines/sql/compilers/oracle_compiler.rb +95 -0
  10. data/lib/arel/engines/sql/compilers/postgresql_compiler.rb +42 -0
  11. data/lib/arel/engines/sql/compilers/sqlite_compiler.rb +9 -0
  12. data/lib/arel/engines/sql/engine.rb +17 -3
  13. data/lib/arel/engines/sql/formatters.rb +3 -3
  14. data/lib/arel/engines/sql/relations.rb +1 -0
  15. data/lib/arel/engines/sql/relations/compiler.rb +118 -0
  16. data/lib/arel/engines/sql/relations/relation.rb +38 -63
  17. data/lib/arel/engines/sql/relations/table.rb +23 -3
  18. data/lib/arel/engines/sql/relations/utilities/externalization.rb +1 -1
  19. data/lib/arel/engines/sql/relations/writes.rb +4 -71
  20. data/spec/arel/algebra/unit/relations/relation_spec.rb +1 -2
  21. data/spec/arel/algebra/unit/relations/table_spec.rb +0 -1
  22. data/spec/arel/engines/memory/integration/joins/cross_engine_spec.rb +9 -4
  23. data/spec/arel/engines/sql/integration/joins/with_adjacency_spec.rb +68 -19
  24. data/spec/arel/engines/sql/integration/joins/with_aggregations_spec.rb +74 -20
  25. data/spec/arel/engines/sql/integration/joins/with_compounds_spec.rb +33 -3
  26. data/spec/arel/engines/sql/unit/predicates/binary_spec.rb +22 -2
  27. data/spec/arel/engines/sql/unit/predicates/equality_spec.rb +15 -3
  28. data/spec/arel/engines/sql/unit/predicates/in_spec.rb +59 -5
  29. data/spec/arel/engines/sql/unit/predicates/predicates_spec.rb +12 -0
  30. data/spec/arel/engines/sql/unit/primitives/attribute_spec.rb +24 -1
  31. data/spec/arel/engines/sql/unit/primitives/expression_spec.rb +5 -1
  32. data/spec/arel/engines/sql/unit/primitives/literal_spec.rb +10 -2
  33. data/spec/arel/engines/sql/unit/relations/alias_spec.rb +11 -1
  34. data/spec/arel/engines/sql/unit/relations/delete_spec.rb +23 -3
  35. data/spec/arel/engines/sql/unit/relations/from_spec.rb +16 -2
  36. data/spec/arel/engines/sql/unit/relations/group_spec.rb +18 -2
  37. data/spec/arel/engines/sql/unit/relations/having_spec.rb +12 -3
  38. data/spec/arel/engines/sql/unit/relations/insert_spec.rb +37 -1
  39. data/spec/arel/engines/sql/unit/relations/join_spec.rb +53 -11
  40. data/spec/arel/engines/sql/unit/relations/lock_spec.rb +25 -0
  41. data/spec/arel/engines/sql/unit/relations/order_spec.rb +52 -4
  42. data/spec/arel/engines/sql/unit/relations/project_spec.rb +38 -5
  43. data/spec/arel/engines/sql/unit/relations/skip_spec.rb +10 -1
  44. data/spec/arel/engines/sql/unit/relations/table_spec.rb +26 -5
  45. data/spec/arel/engines/sql/unit/relations/take_spec.rb +18 -1
  46. data/spec/arel/engines/sql/unit/relations/update_spec.rb +47 -1
  47. data/spec/arel/engines/sql/unit/relations/where_spec.rb +18 -2
  48. data/spec/connections/oracle_connection.rb +19 -0
  49. data/spec/schemas/mysql_schema.rb +2 -1
  50. data/spec/schemas/oracle_schema.rb +20 -0
  51. data/spec/schemas/postgresql_schema.rb +2 -1
  52. data/spec/schemas/sqlite3_schema.rb +2 -1
  53. data/spec/spec_helper.rb +16 -7
  54. metadata +31 -9
@@ -0,0 +1,118 @@
1
+ module Arel
2
+ module SqlCompiler
3
+ class GenericCompiler
4
+ attr_reader :relation
5
+
6
+ def initialize(relation)
7
+ @relation = relation
8
+ end
9
+
10
+ def select_sql
11
+ build_query \
12
+ "SELECT #{select_clauses.join(', ')}",
13
+ "FROM #{from_clauses}",
14
+ (joins(self) unless joins(self).blank? ),
15
+ ("WHERE #{where_clauses.join(" AND ")}" unless wheres.blank? ),
16
+ ("GROUP BY #{group_clauses.join(', ')}" unless groupings.blank? ),
17
+ ("HAVING #{having_clauses.join(', ')}" unless havings.blank? ),
18
+ ("ORDER BY #{order_clauses.join(', ')}" unless orders.blank? ),
19
+ ("LIMIT #{taken}" unless taken.blank? ),
20
+ ("OFFSET #{skipped}" unless skipped.blank? ),
21
+ ("#{locked}" unless locked.blank?)
22
+ end
23
+
24
+ def delete_sql
25
+ build_query \
26
+ "DELETE",
27
+ "FROM #{table_sql}",
28
+ ("WHERE #{wheres.collect(&:to_sql).join(' AND ')}" unless wheres.blank? ),
29
+ (add_limit_on_delete(taken) unless taken.blank? )
30
+ end
31
+
32
+ def add_limit_on_delete(taken)
33
+ "LIMIT #{taken}"
34
+ end
35
+
36
+ def insert_sql(include_returning = true)
37
+ insertion_attributes_values_sql = if record.is_a?(Value)
38
+ record.value
39
+ else
40
+ attributes = record.keys.sort_by do |attribute|
41
+ attribute.name.to_s
42
+ end
43
+
44
+ first = attributes.collect do |key|
45
+ engine.quote_column_name(key.name)
46
+ end.join(', ')
47
+
48
+ second = attributes.collect do |key|
49
+ key.format(record[key])
50
+ end.join(', ')
51
+
52
+ build_query "(#{first})", "VALUES (#{second})"
53
+ end
54
+
55
+ build_query \
56
+ "INSERT",
57
+ "INTO #{table_sql}",
58
+ insertion_attributes_values_sql,
59
+ ("RETURNING #{engine.quote_column_name(primary_key)}" if include_returning && compiler.supports_insert_with_returning?)
60
+ end
61
+
62
+ def supports_insert_with_returning?
63
+ false
64
+ end
65
+
66
+ def update_sql
67
+ build_query \
68
+ "UPDATE #{table_sql} SET",
69
+ assignment_sql,
70
+ build_update_conditions_sql
71
+ end
72
+
73
+ protected
74
+ def method_missing(method, *args, &block)
75
+ relation.send(method, *args, &block)
76
+ end
77
+
78
+ def build_query(*parts)
79
+ parts.compact.join(" ")
80
+ end
81
+
82
+ def assignment_sql
83
+ if assignments.respond_to?(:collect)
84
+ attributes = assignments.keys.sort_by do |attribute|
85
+ attribute.name.to_s
86
+ end
87
+
88
+ attributes.map do |attribute|
89
+ value = assignments[attribute]
90
+ "#{engine.quote_column_name(attribute.name)} = #{attribute.format(value)}"
91
+ end.join(", ")
92
+ else
93
+ assignments.value
94
+ end
95
+ end
96
+
97
+ def build_update_conditions_sql
98
+ conditions = ""
99
+ conditions << " WHERE #{wheres.collect(&:to_sql).join(' AND ')}" unless wheres.blank?
100
+ conditions << " ORDER BY #{order_clauses.join(', ')}" unless orders.blank?
101
+
102
+ unless taken.blank?
103
+ conditions = limited_update_conditions(conditions, taken)
104
+ end
105
+
106
+ conditions
107
+ end
108
+
109
+ def limited_update_conditions(conditions, taken)
110
+ conditions << " LIMIT #{taken}"
111
+ quoted_primary_key = engine.quote_column_name(primary_key)
112
+ "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{engine.connection.quote_table_name table.name} #{conditions})"
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+ end
@@ -1,86 +1,61 @@
1
1
  module Arel
2
2
  class Relation
3
- def to_sql(formatter = Sql::SelectStatement.new(self))
4
- formatter.select select_sql, self
5
- end
6
-
7
- def select_sql
8
- if engine.adapter_name == "PostgreSQL" && !orders.blank? && using_distinct_on?
9
- # PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this
10
- # by wrapping the +sql+ string as a sub-select and ordering in that query.
11
- order = order_clauses.join(', ').split(',').map { |s| s.strip }.reject(&:blank?)
12
- order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{'DESC' if s =~ /\bdesc$/i}" }.join(', ')
13
-
14
- query = build_query \
15
- "SELECT #{select_clauses.kind_of?(::Array) ? select_clauses.join("") : select_clauses.to_s}",
16
- "FROM #{from_clauses}",
17
- (joins(self) unless joins(self).blank? ),
18
- ("WHERE #{where_clauses.join(" AND ")}" unless wheres.blank? ),
19
- ("GROUP BY #{group_clauses.join(', ')}" unless groupings.blank? ),
20
- ("HAVING #{having_clauses.join(', ')}" unless havings.blank? ),
21
- ("#{locked}" unless locked.blank? )
3
+ @@connection_tables_primary_keys = {}
22
4
 
23
- build_query \
24
- "SELECT * FROM (#{query}) AS id_list",
25
- "ORDER BY #{order}",
26
- ("LIMIT #{taken}" unless taken.blank? ),
27
- ("OFFSET #{skipped}" unless skipped.blank? )
28
-
29
- else
30
- build_query \
31
- "SELECT #{select_clauses.join(', ')}",
32
- "FROM #{from_clauses}",
33
- (joins(self) unless joins(self).blank? ),
34
- ("WHERE #{where_clauses.join(" AND ")}" unless wheres.blank? ),
35
- ("GROUP BY #{group_clauses.join(', ')}" unless groupings.blank? ),
36
- ("HAVING #{having_clauses.join(', ')}" unless havings.blank? ),
37
- ("ORDER BY #{order_clauses.join(', ')}" unless orders.blank? ),
38
- ("LIMIT #{taken}" unless taken.blank? ),
39
- ("OFFSET #{skipped}" unless skipped.blank? ),
40
- ("#{locked}" unless engine.adapter_name =~ /SQLite/ || locked.blank?)
5
+ def compiler
6
+ @compiler ||= begin
7
+ "Arel::SqlCompiler::#{engine.adapter_name}Compiler".constantize.new(self)
8
+ rescue
9
+ Arel::SqlCompiler::GenericCompiler.new(self)
41
10
  end
42
11
  end
43
12
 
44
- def inclusion_predicate_sql
45
- "IN"
13
+ def to_sql(formatter = Sql::SelectStatement.new(self))
14
+ formatter.select compiler.select_sql, self
46
15
  end
47
16
 
48
17
  def christener
49
18
  @christener ||= Sql::Christener.new
50
19
  end
51
20
 
52
- protected
53
-
54
- def build_query(*parts)
55
- parts.compact.join(" ")
21
+ def inclusion_predicate_sql
22
+ "IN"
56
23
  end
57
24
 
58
- def from_clauses
59
- sources.blank? ? table_sql(Sql::TableReference.new(self)) : sources
25
+ def primary_key
26
+ connection_id = engine.connection.object_id
27
+ if @@connection_tables_primary_keys[connection_id] && @@connection_tables_primary_keys[connection_id].has_key?(table.name)
28
+ @@connection_tables_primary_keys[connection_id][table.name]
29
+ else
30
+ @@connection_tables_primary_keys[connection_id] ||= {}
31
+ @@connection_tables_primary_keys[connection_id][table.name] = engine.connection.primary_key(table.name)
32
+ end
60
33
  end
61
34
 
62
- def select_clauses
63
- attributes.collect { |a| a.to_sql(Sql::SelectClause.new(self)) }
64
- end
35
+ protected
65
36
 
66
- def where_clauses
67
- wheres.collect { |w| w.to_sql(Sql::WhereClause.new(self)) }
68
- end
37
+ def from_clauses
38
+ sources.blank? ? table_sql(Sql::TableReference.new(self)) : sources
39
+ end
69
40
 
70
- def group_clauses
71
- groupings.collect { |g| g.to_sql(Sql::GroupClause.new(self)) }
72
- end
41
+ def select_clauses
42
+ attributes.collect { |a| a.to_sql(Sql::SelectClause.new(self)) }
43
+ end
73
44
 
74
- def having_clauses
75
- havings.collect { |g| g.to_sql(Sql::HavingClause.new(self)) }
76
- end
45
+ def where_clauses
46
+ wheres.collect { |w| w.to_sql(Sql::WhereClause.new(self)) }
47
+ end
77
48
 
78
- def order_clauses
79
- orders.collect { |o| o.to_sql(Sql::OrderClause.new(self)) }
80
- end
49
+ def group_clauses
50
+ groupings.collect { |g| g.to_sql(Sql::GroupClause.new(self)) }
51
+ end
81
52
 
82
- def using_distinct_on?
83
- select_clauses.any? { |x| x =~ /DISTINCT ON/ }
84
- end
53
+ def having_clauses
54
+ havings.collect { |g| g.to_sql(Sql::HavingClause.new(self)) }
55
+ end
56
+
57
+ def order_clauses
58
+ orders.collect { |o| o.to_sql(Sql::OrderClause.new(self)) }
59
+ end
85
60
  end
86
61
  end
@@ -2,7 +2,7 @@ module Arel
2
2
  class Table < Relation
3
3
  include Recursion::BaseCase
4
4
 
5
- cattr_accessor :engine
5
+ cattr_accessor :engine, :tables
6
6
  attr_reader :name, :engine, :table_alias, :options
7
7
 
8
8
  def initialize(name, options = {})
@@ -15,15 +15,35 @@ module Arel
15
15
  else
16
16
  @engine = options # Table.new('foo', engine)
17
17
  end
18
+
19
+ if @engine.connection
20
+ begin
21
+ require "arel/engines/sql/compilers/#{@engine.adapter_name.downcase}_compiler"
22
+ @@tables ||= engine.tables
23
+ rescue LoadError
24
+ raise "#{@engine.adapter_name} is not supported by Arel."
25
+ end
26
+ end
18
27
  end
19
28
 
20
29
  def as(table_alias)
21
30
  Table.new(name, options.merge(:as => table_alias))
22
31
  end
23
32
 
33
+ def table_exists?
34
+ if @table_exists
35
+ true
36
+ else
37
+ @table_exists = @@tables.include?(name) || engine.table_exists?(name)
38
+ end
39
+ end
40
+
24
41
  def attributes
25
- @attributes ||= columns.collect do |column|
26
- Attribute.new(self, column.name.to_sym)
42
+ return @attributes if defined?(@attributes)
43
+ if table_exists?
44
+ @attributes = columns.collect { |column| Attribute.new(self, column.name.to_sym) }
45
+ else
46
+ []
27
47
  end
28
48
  end
29
49
 
@@ -3,7 +3,7 @@ module Arel
3
3
  include Recursion::BaseCase
4
4
 
5
5
  def table_sql(formatter = Sql::TableReference.new(relation))
6
- formatter.select relation.select_sql, self
6
+ formatter.select relation.compiler.select_sql, self
7
7
  end
8
8
 
9
9
  # REMOVEME
@@ -1,86 +1,19 @@
1
1
  module Arel
2
2
  class Deletion < Compound
3
3
  def to_sql
4
- build_query \
5
- "DELETE",
6
- "FROM #{table_sql}",
7
- ("WHERE #{wheres.collect(&:to_sql).join(' AND ')}" unless wheres.blank? ),
8
- ("LIMIT #{taken}" unless taken.blank? )
4
+ compiler.delete_sql
9
5
  end
10
6
  end
11
7
 
12
8
  class Insert < Compound
13
- def to_sql
14
- insertion_attributes_values_sql = if record.is_a?(Value)
15
- record.value
16
- else
17
- attributes = record.keys.sort_by do |attribute|
18
- attribute.name.to_s
19
- end
20
-
21
- first = attributes.collect do |key|
22
- engine.quote_column_name(key.name)
23
- end.join(', ')
24
-
25
- second = attributes.collect do |key|
26
- key.format(record[key])
27
- end.join(', ')
28
-
29
- build_query "(#{first})", "VALUES (#{second})"
30
- end
31
-
32
- build_query \
33
- "INSERT",
34
- "INTO #{table_sql}",
35
- insertion_attributes_values_sql
9
+ def to_sql(include_returning = true)
10
+ compiler.insert_sql(include_returning)
36
11
  end
37
12
  end
38
13
 
39
14
  class Update < Compound
40
15
  def to_sql
41
- build_query \
42
- "UPDATE #{table_sql} SET",
43
- assignment_sql,
44
- build_update_conditions_sql
45
- end
46
-
47
- protected
48
-
49
- def assignment_sql
50
- if assignments.respond_to?(:collect)
51
- attributes = assignments.keys.sort_by do |attribute|
52
- attribute.name.to_s
53
- end
54
-
55
- attributes.map do |attribute|
56
- value = assignments[attribute]
57
- "#{engine.quote_column_name(attribute.name)} = #{attribute.format(value)}"
58
- end.join(", ")
59
- else
60
- assignments.value
61
- end
62
- end
63
-
64
- def build_update_conditions_sql
65
- conditions = ""
66
- conditions << " WHERE #{wheres.collect(&:to_sql).join(' AND ')}" unless wheres.blank?
67
- conditions << " ORDER BY #{order_clauses.join(', ')}" unless orders.blank?
68
-
69
- unless taken.blank?
70
- conditions << " LIMIT #{taken}"
71
-
72
- if engine.adapter_name != "MySQL"
73
- begin
74
- quote_primary_key = engine.quote_column_name(table.name.classify.constantize.primary_key)
75
- rescue NameError
76
- quote_primary_key = engine.quote_column_name("id")
77
- end
78
-
79
- conditions = "WHERE #{quote_primary_key} IN (SELECT #{quote_primary_key} FROM #{engine.connection.quote_table_name table.name} #{conditions})"
80
- end
81
- end
82
-
83
- conditions
16
+ compiler.update_sql
84
17
  end
85
18
  end
86
19
  end
@@ -16,10 +16,9 @@ module Arel
16
16
  end
17
17
 
18
18
  describe 'when given a', Symbol, String do
19
- it "returns the attribute with the same name, if it exists" do
19
+ it "returns the attribute with the same name" do
20
20
  check @relation[:id].should == @attribute1
21
21
  check @relation['id'].should == @attribute1
22
- @relation[:does_not_exist].should be_nil
23
22
  end
24
23
  end
25
24
  end
@@ -10,7 +10,6 @@ module Arel
10
10
  describe 'when given a', Symbol do
11
11
  it "manufactures an attribute if the symbol names an attribute within the relation" do
12
12
  check @relation[:id].should == Attribute.new(@relation, :id)
13
- @relation[:does_not_exist].should be_nil
14
13
  end
15
14
  end
16
15
 
@@ -12,6 +12,11 @@ module Arel
12
12
  @photos.delete
13
13
  @photos.insert(@photos[:id] => 1, @photos[:user_id] => 1, @photos[:camera_id] => 6)
14
14
  @photos.insert(@photos[:id] => 2, @photos[:user_id] => 2, @photos[:camera_id] => 42)
15
+ # Oracle adapter returns database integers as Ruby integers and not strings
16
+ @adapter_returns_integer = false
17
+ adapter_is :oracle do
18
+ @adapter_returns_integer = true
19
+ end
15
20
  end
16
21
 
17
22
  describe 'when the in memory relation is on the left' do
@@ -22,8 +27,8 @@ module Arel
22
27
  .project(@users[:name], @photos[:camera_id]) \
23
28
  .let do |relation|
24
29
  relation.call.should == [
25
- Row.new(relation, ['bryan', '6']),
26
- Row.new(relation, ['emilio', '42'])
30
+ Row.new(relation, ['bryan', @adapter_returns_integer ? 6 : '6']),
31
+ Row.new(relation, ['emilio', @adapter_returns_integer ? 42 : '42'])
27
32
  ]
28
33
  end
29
34
  end
@@ -37,8 +42,8 @@ module Arel
37
42
  .project(@users[:name], @photos[:camera_id]) \
38
43
  .let do |relation|
39
44
  relation.call.should == [
40
- Row.new(relation, ['bryan', '6']),
41
- Row.new(relation, ['emilio', '42'])
45
+ Row.new(relation, ['bryan', @adapter_returns_integer ? 6 : '6']),
46
+ Row.new(relation, ['emilio', @adapter_returns_integer ? 42 : '42'])
42
47
  ]
43
48
  end
44
49
  end