arel 0.2.1 → 0.3.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.
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