diff options
Diffstat (limited to 'lib/arel/engines')
30 files changed, 787 insertions, 0 deletions
diff --git a/lib/arel/engines/memory.rb b/lib/arel/engines/memory.rb new file mode 100644 index 0000000000..9e7193ef13 --- /dev/null +++ b/lib/arel/engines/memory.rb @@ -0,0 +1,4 @@ +require 'arel/engines/memory/relations' +require 'arel/engines/memory/primitives' +require 'arel/engines/memory/engine' +require 'arel/engines/memory/predicates' diff --git a/lib/arel/engines/memory/engine.rb b/lib/arel/engines/memory/engine.rb new file mode 100644 index 0000000000..c7ac9422d4 --- /dev/null +++ b/lib/arel/engines/memory/engine.rb @@ -0,0 +1,16 @@ +module Arel + module Memory + class Engine + module CRUD + def read(relation) + relation.eval + end + + def create(relation) + relation.eval + end + end + include CRUD + end + end +end diff --git a/lib/arel/engines/memory/predicates.rb b/lib/arel/engines/memory/predicates.rb new file mode 100644 index 0000000000..03d4f25b0a --- /dev/null +++ b/lib/arel/engines/memory/predicates.rb @@ -0,0 +1,35 @@ +module Arel + class Binary < Predicate + def eval(row) + operand1.eval(row).send(operator, operand2.eval(row)) + end + end + + class Equality < Binary + def operator; :== end + end + + class GreaterThanOrEqualTo < Binary + def operator; :>= end + end + + class GreaterThan < Binary + def operator; :> end + end + + class LessThanOrEqualTo < Binary + def operator; :<= end + end + + class LessThan < Binary + def operator; :< end + end + + class Match < Binary + def operator; :=~ end + end + + class In < Binary + def operator; :include? end + end +end diff --git a/lib/arel/engines/memory/primitives.rb b/lib/arel/engines/memory/primitives.rb new file mode 100644 index 0000000000..935b34f5ee --- /dev/null +++ b/lib/arel/engines/memory/primitives.rb @@ -0,0 +1,27 @@ +module Arel + class Attribute + def eval(row) + row[self] + end + end + + class Value + def eval(row) + value + end + end + + class Ordering + def eval(row1, row2) + (attribute.eval(row1) <=> attribute.eval(row2)) * direction + end + end + + class Descending < Ordering + def direction; -1 end + end + + class Ascending < Ordering + def direction; 1 end + end +end diff --git a/lib/arel/engines/memory/relations.rb b/lib/arel/engines/memory/relations.rb new file mode 100644 index 0000000000..c67af2d63b --- /dev/null +++ b/lib/arel/engines/memory/relations.rb @@ -0,0 +1,5 @@ +require 'arel/engines/memory/relations/array' +require 'arel/engines/memory/relations/operations' +require 'arel/engines/memory/relations/writes' +require 'arel/engines/memory/relations/compound' + diff --git a/lib/arel/engines/memory/relations/array.rb b/lib/arel/engines/memory/relations/array.rb new file mode 100644 index 0000000000..5e7c0a4ab1 --- /dev/null +++ b/lib/arel/engines/memory/relations/array.rb @@ -0,0 +1,25 @@ +module Arel + class Array < Relation + attributes :array, :attribute_names + include Recursion::BaseCase + deriving :==, :initialize + + def engine + @engine ||= Memory::Engine.new + end + + def attributes + @attributes ||= @attribute_names.collect do |name| + name.to_attribute(self) + end + end + + def format(attribute, value) + value + end + + def eval + @array.collect { |r| Row.new(self, r) } + end + end +end diff --git a/lib/arel/engines/memory/relations/compound.rb b/lib/arel/engines/memory/relations/compound.rb new file mode 100644 index 0000000000..6dda92a6a1 --- /dev/null +++ b/lib/arel/engines/memory/relations/compound.rb @@ -0,0 +1,9 @@ +module Arel + class Compound < Relation + delegate :array, :to => :relation + + def unoperated_rows + relation.call.collect { |row| row.bind(self) } + end + end +end diff --git a/lib/arel/engines/memory/relations/operations.rb b/lib/arel/engines/memory/relations/operations.rb new file mode 100644 index 0000000000..8e01938360 --- /dev/null +++ b/lib/arel/engines/memory/relations/operations.rb @@ -0,0 +1,61 @@ +module Arel + class Where < Compound + def eval + unoperated_rows.select { |row| predicate.eval(row) } + end + end + + class Order < Compound + def eval + unoperated_rows.sort do |row1, row2| + ordering = orderings.detect { |o| o.eval(row1, row2) != 0 } || orderings.last + ordering.eval(row1, row2) + end + end + end + + class Project < Compound + def eval + unoperated_rows.collect { |r| r.slice(*projections) } + end + end + + class Take < Compound + def eval + unoperated_rows[0, taken] + end + end + + class Skip < Compound + def eval + unoperated_rows[skipped..-1] + end + end + + class Group < Compound + def eval + raise NotImplementedError + end + end + + class Alias < Compound + def eval + unoperated_rows + end + end + + class Join < Relation + def eval + result = [] + relation1.call.each do |row1| + relation2.call.each do |row2| + combined_row = row1.combine(row2, self) + if predicates.all? { |p| p.eval(combined_row) } + result << combined_row + end + end + end + result + end + end +end diff --git a/lib/arel/engines/memory/relations/writes.rb b/lib/arel/engines/memory/relations/writes.rb new file mode 100644 index 0000000000..12c4f36c0d --- /dev/null +++ b/lib/arel/engines/memory/relations/writes.rb @@ -0,0 +1,7 @@ +module Arel + class Insert < Compound + def eval + unoperated_rows + [Row.new(self, record.values.collect(&:value))] + end + end +end diff --git a/lib/arel/engines/sql.rb b/lib/arel/engines/sql.rb new file mode 100644 index 0000000000..f31cfc7dac --- /dev/null +++ b/lib/arel/engines/sql.rb @@ -0,0 +1,7 @@ +require 'arel/engines/sql/engine' +require 'arel/engines/sql/relations' +require 'arel/engines/sql/primitives' +require 'arel/engines/sql/predicates' +require 'arel/engines/sql/formatters' +require 'arel/engines/sql/extensions' +require 'arel/engines/sql/christener' diff --git a/lib/arel/engines/sql/christener.rb b/lib/arel/engines/sql/christener.rb new file mode 100644 index 0000000000..c1c9325208 --- /dev/null +++ b/lib/arel/engines/sql/christener.rb @@ -0,0 +1,13 @@ +module Arel + module Sql + class Christener + def name_for(relation) + @used_names ||= Hash.new(0) + (@relation_names ||= Hash.new do |hash, relation| + @used_names[name = relation.name] += 1 + hash[relation] = name + (@used_names[name] > 1 ? "_#{@used_names[name]}" : '') + end)[relation.table] + end + end + end +end diff --git a/lib/arel/engines/sql/engine.rb b/lib/arel/engines/sql/engine.rb new file mode 100644 index 0000000000..7d2926040c --- /dev/null +++ b/lib/arel/engines/sql/engine.rb @@ -0,0 +1,37 @@ +module Arel + module Sql + class Engine + def initialize(ar = nil) + @ar = ar + end + + def connection + @ar.connection + end + + def method_missing(method, *args, &block) + @ar.connection.send(method, *args, &block) + end + + module CRUD + def create(relation) + connection.insert(relation.to_sql) + end + + def read(relation) + rows = connection.select_rows(relation.to_sql) + Array.new(rows, relation.attributes) + end + + def update(relation) + connection.update(relation.to_sql) + end + + def delete(relation) + connection.delete(relation.to_sql) + end + end + include CRUD + end + end +end diff --git a/lib/arel/engines/sql/extensions.rb b/lib/arel/engines/sql/extensions.rb new file mode 100644 index 0000000000..1ea31bc140 --- /dev/null +++ b/lib/arel/engines/sql/extensions.rb @@ -0,0 +1,4 @@ +require 'arel/engines/sql/extensions/object' +require 'arel/engines/sql/extensions/array' +require 'arel/engines/sql/extensions/range' +require 'arel/engines/sql/extensions/nil_class' diff --git a/lib/arel/engines/sql/extensions/array.rb b/lib/arel/engines/sql/extensions/array.rb new file mode 100644 index 0000000000..7f15179721 --- /dev/null +++ b/lib/arel/engines/sql/extensions/array.rb @@ -0,0 +1,16 @@ +module Arel + module Sql + module ArrayExtensions + def to_sql(formatter = nil) + "(" + collect { |e| e.to_sql(formatter) }.join(', ') + ")" + end + + def inclusion_predicate_sql + "IN" + end + + Array.send(:include, self) + end + end +end + diff --git a/lib/arel/engines/sql/extensions/nil_class.rb b/lib/arel/engines/sql/extensions/nil_class.rb new file mode 100644 index 0000000000..8c335f904a --- /dev/null +++ b/lib/arel/engines/sql/extensions/nil_class.rb @@ -0,0 +1,11 @@ +module Arel + module Sql + module NilClassExtensions + def equality_predicate_sql + 'IS' + end + + NilClass.send(:include, self) + end + end +end diff --git a/lib/arel/engines/sql/extensions/object.rb b/lib/arel/engines/sql/extensions/object.rb new file mode 100644 index 0000000000..8fe63dba2f --- /dev/null +++ b/lib/arel/engines/sql/extensions/object.rb @@ -0,0 +1,15 @@ +module Arel + module Sql + module ObjectExtensions + def to_sql(formatter) + formatter.scalar self + end + + def equality_predicate_sql + '=' + end + + Object.send(:include, self) + end + end +end diff --git a/lib/arel/engines/sql/extensions/range.rb b/lib/arel/engines/sql/extensions/range.rb new file mode 100644 index 0000000000..25bf1d01da --- /dev/null +++ b/lib/arel/engines/sql/extensions/range.rb @@ -0,0 +1,15 @@ +module Arel + module Sql + module RangeExtensions + def to_sql(formatter = nil) + formatter.range self.begin, self.end + end + + def inclusion_predicate_sql + "BETWEEN" + end + + Range.send(:include, self) + end + end +end diff --git a/lib/arel/engines/sql/formatters.rb b/lib/arel/engines/sql/formatters.rb new file mode 100644 index 0000000000..ae80feb18e --- /dev/null +++ b/lib/arel/engines/sql/formatters.rb @@ -0,0 +1,113 @@ +module Arel + module Sql + class Formatter + attr_reader :environment + delegate :christener, :engine, :to => :environment + delegate :name_for, :to => :christener + delegate :quote_table_name, :quote_column_name, :quote, :to => :engine + + def initialize(environment) + @environment = environment + end + end + + class SelectClause < Formatter + def attribute(attribute) + # FIXME this should check that the column exists + "#{quote_table_name(name_for(attribute.original_relation))}.#{quote_column_name(attribute.name)}" + + (attribute.alias ? " AS #{quote(attribute.alias.to_s)}" : "") + end + + def expression(expression) + if expression.function_sql == "DISTINCT" + "#{expression.function_sql} #{expression.attribute.to_sql(self)}" + + (expression.alias ? " AS #{quote_column_name(expression.alias)}" : '') + else + "#{expression.function_sql}(#{expression.attribute.to_sql(self)})" + + (expression.alias ? " AS #{quote_column_name(expression.alias)}" : " AS #{expression.function_sql.to_s.downcase}_id") + end + end + + def select(select_sql, table) + "(#{select_sql}) AS #{quote_table_name(name_for(table))}" + end + + def value(value) + value + end + end + + class PassThrough < Formatter + def value(value) + value + end + end + + class WhereClause < PassThrough + end + + class OrderClause < PassThrough + def ordering(ordering) + "#{quote_table_name(name_for(ordering.attribute.original_relation))}.#{quote_column_name(ordering.attribute.name)} #{ordering.direction_sql}" + end + end + + class GroupClause < PassThrough + def attribute(attribute) + "#{quote_table_name(name_for(attribute.original_relation))}.#{quote_column_name(attribute.name)}" + end + end + + class WhereCondition < Formatter + def attribute(attribute) + "#{quote_table_name(name_for(attribute.original_relation))}.#{quote_column_name(attribute.name)}" + end + + def expression(expression) + "#{expression.function_sql}(#{expression.attribute.to_sql(self)})" + end + + def value(value) + value.to_sql(self) + end + + def scalar(value, column = nil) + quote(value, column) + end + + def select(select_sql, table) + "(#{select_sql})" + end + end + + class SelectStatement < Formatter + def select(select_sql, table) + select_sql + end + end + + class TableReference < Formatter + def select(select_sql, table) + "(#{select_sql}) AS #{quote_table_name(name_for(table))}" + end + + def table(table) + quote_table_name(table.name) + + (table.name != name_for(table) ? " AS " + quote_table_name(name_for(table)) : '') + end + end + + class Attribute < WhereCondition + def scalar(scalar) + quote(scalar, environment.column) + end + + def range(left, right) + "#{left} AND #{right}" + end + end + + class Value < WhereCondition + end + end +end diff --git a/lib/arel/engines/sql/predicates.rb b/lib/arel/engines/sql/predicates.rb new file mode 100644 index 0000000000..b84c183c1d --- /dev/null +++ b/lib/arel/engines/sql/predicates.rb @@ -0,0 +1,51 @@ +module Arel + class Binary < Predicate + def to_sql(formatter = nil) + "#{operand1.to_sql} #{predicate_sql} #{operand1.format(operand2)}" + end + end + + class CompoundPredicate < Binary + def to_sql(formatter = nil) + "(#{operand1.to_sql(formatter)} #{predicate_sql} #{operand2.to_sql(formatter)})" + end + end + + class Or < CompoundPredicate + def predicate_sql; "OR" end + end + + class And < CompoundPredicate + def predicate_sql; "AND" end + end + + class Equality < Binary + def predicate_sql + operand2.equality_predicate_sql + end + end + + class GreaterThanOrEqualTo < Binary + def predicate_sql; '>=' end + end + + class GreaterThan < Binary + def predicate_sql; '>' end + end + + class LessThanOrEqualTo < Binary + def predicate_sql; '<=' end + end + + class LessThan < Binary + def predicate_sql; '<' end + end + + class Match < Binary + def predicate_sql; 'LIKE' end + end + + class In < Binary + def predicate_sql; operand2.inclusion_predicate_sql end + end +end diff --git a/lib/arel/engines/sql/primitives.rb b/lib/arel/engines/sql/primitives.rb new file mode 100644 index 0000000000..bb3bed78e6 --- /dev/null +++ b/lib/arel/engines/sql/primitives.rb @@ -0,0 +1,85 @@ +module Arel + class SqlLiteral < String + def relation + nil + end + + def to_sql(formatter = nil) + self + end + end + + class Attribute + def column + original_relation.column_for(self) + end + + def type_cast(value) + root.relation.format(self, value) + end + + def format(object) + object.to_sql(Sql::Attribute.new(self)) + end + + def to_sql(formatter = Sql::WhereCondition.new(relation)) + formatter.attribute self + end + end + + class Value + delegate :inclusion_predicate_sql, :equality_predicate_sql, :to => :value + + def to_sql(formatter = Sql::WhereCondition.new(relation)) + formatter.value value + end + + def format(object) + object.to_sql(Sql::Value.new(relation)) + end + end + + class Ordering + def to_sql(formatter = Sql::OrderClause.new(relation)) + formatter.ordering self + end + end + + class Ascending < Ordering + def direction_sql; 'ASC' end + end + + class Descending < Ordering + def direction_sql; 'DESC' end + end + + class Expression < Attribute + def to_sql(formatter = Sql::SelectClause.new(relation)) + formatter.expression self + end + end + + class Count < Expression + def function_sql; 'COUNT' end + end + + class Distinct < Expression + def function_sql; 'DISTINCT' end + end + + class Sum < Expression + def function_sql; 'SUM' end + end + + class Maximum < Expression + def function_sql; 'MAX' end + end + + class Minimum < Expression + def function_sql; 'MIN' end + end + + class Average < Expression + def function_sql; 'AVG' end + end +end diff --git a/lib/arel/engines/sql/relations.rb b/lib/arel/engines/sql/relations.rb new file mode 100644 index 0000000000..8360a1f806 --- /dev/null +++ b/lib/arel/engines/sql/relations.rb @@ -0,0 +1,9 @@ +require 'arel/engines/sql/relations/utilities/compound' +require 'arel/engines/sql/relations/utilities/recursion' +require 'arel/engines/sql/relations/utilities/externalization' +require 'arel/engines/sql/relations/utilities/nil' +require 'arel/engines/sql/relations/relation' +require 'arel/engines/sql/relations/table' +require 'arel/engines/sql/relations/operations/join' +require 'arel/engines/sql/relations/operations/alias' +require 'arel/engines/sql/relations/writes' diff --git a/lib/arel/engines/sql/relations/operations/alias.rb b/lib/arel/engines/sql/relations/operations/alias.rb new file mode 100644 index 0000000000..9b6a484463 --- /dev/null +++ b/lib/arel/engines/sql/relations/operations/alias.rb @@ -0,0 +1,5 @@ +module Arel + class Alias < Compound + include Recursion::BaseCase + end +end diff --git a/lib/arel/engines/sql/relations/operations/join.rb b/lib/arel/engines/sql/relations/operations/join.rb new file mode 100644 index 0000000000..7c5e13510a --- /dev/null +++ b/lib/arel/engines/sql/relations/operations/join.rb @@ -0,0 +1,33 @@ +module Arel + class Join < Relation + def table_sql(formatter = Sql::TableReference.new(self)) + relation1.externalize.table_sql(formatter) + end + + def joins(environment, formatter = Sql::TableReference.new(environment)) + @joins ||= begin + this_join = [ + join_sql, + relation2.externalize.table_sql(formatter), + ("ON" unless predicates.blank?), + (ons + relation2.externalize.wheres).collect { |p| p.bind(environment).to_sql(Sql::WhereClause.new(environment)) }.join(' AND ') + ].compact.join(" ") + [relation1.joins(environment), this_join, relation2.joins(environment)].compact.join(" ") + end + end + end + + class InnerJoin < Join + def join_sql; "INNER JOIN" end + end + + class OuterJoin < Join + def join_sql; "LEFT OUTER JOIN" end + end + + class StringJoin < Join + def joins(_, __ = nil) + relation2 + end + end +end diff --git a/lib/arel/engines/sql/relations/relation.rb b/lib/arel/engines/sql/relations/relation.rb new file mode 100644 index 0000000000..4cfb83a601 --- /dev/null +++ b/lib/arel/engines/sql/relations/relation.rb @@ -0,0 +1,50 @@ +module Arel + class Relation + def to_sql(formatter = Sql::SelectStatement.new(self)) + formatter.select select_sql, self + end + + def select_sql + build_query \ + "SELECT #{select_clauses.join(', ')}", + "FROM #{table_sql(Sql::TableReference.new(self))}", + (joins(self) unless joins(self).blank? ), + ("WHERE #{where_clauses.join("\n\tAND ")}" unless wheres.blank? ), + ("GROUP BY #{group_clauses.join(', ')}" unless groupings.blank? ), + ("ORDER BY #{order_clauses.join(', ')}" unless orders.blank? ), + ("LIMIT #{taken}" unless taken.blank? ), + ("OFFSET #{skipped}" unless skipped.blank? ) + end + + def inclusion_predicate_sql + "IN" + end + + def christener + @christener ||= Sql::Christener.new + end + + protected + + def build_query(*parts) + parts.compact.join("\n") + end + + def select_clauses + attributes.collect { |a| a.to_sql(Sql::SelectClause.new(self)) } + end + + def where_clauses + wheres.collect { |w| w.to_sql(Sql::WhereClause.new(self)) } + end + + def group_clauses + groupings.collect { |g| g.to_sql(Sql::GroupClause.new(self)) } + end + + def order_clauses + orders.collect { |o| o.to_sql(Sql::OrderClause.new(self)) } + end + + end +end diff --git a/lib/arel/engines/sql/relations/table.rb b/lib/arel/engines/sql/relations/table.rb new file mode 100644 index 0000000000..dd22f44226 --- /dev/null +++ b/lib/arel/engines/sql/relations/table.rb @@ -0,0 +1,52 @@ +module Arel + class Table < Relation + include Recursion::BaseCase + + cattr_accessor :engine + attr_reader :name, :engine + + def initialize(name, engine = Table.engine) + @name, @engine = name.to_s, engine + end + + def attributes + @attributes ||= columns.collect do |column| + Attribute.new(self, column.name.to_sym) + end + end + + def eql?(other) + self == other + end + + def hash + @hash ||= :name.hash + end + + def format(attribute, value) + attribute.column.type_cast(value) + end + + def column_for(attribute) + has_attribute?(attribute) and columns.detect { |c| c.name == attribute.name.to_s } + end + + def columns + @columns ||= engine.columns(name, "#{name} Columns") + end + + def reset + @attributes = @columns = nil + end + + def ==(other) + Table === other and + name == other.name + end + end +end + +def Table(name, engine = Arel::Table.engine) + Arel::Table.new(name, engine) +end + diff --git a/lib/arel/engines/sql/relations/utilities/compound.rb b/lib/arel/engines/sql/relations/utilities/compound.rb new file mode 100644 index 0000000000..f8ce4033fd --- /dev/null +++ b/lib/arel/engines/sql/relations/utilities/compound.rb @@ -0,0 +1,10 @@ +module Arel + class Compound < Relation + delegate :table, :table_sql, :to => :relation + + def build_query(*parts) + parts.compact.join("\n") + end + end +end + diff --git a/lib/arel/engines/sql/relations/utilities/externalization.rb b/lib/arel/engines/sql/relations/utilities/externalization.rb new file mode 100644 index 0000000000..7f937e8423 --- /dev/null +++ b/lib/arel/engines/sql/relations/utilities/externalization.rb @@ -0,0 +1,14 @@ +module Arel + class Externalization < Compound + include Recursion::BaseCase + + def table_sql(formatter = Sql::TableReference.new(relation)) + formatter.select relation.select_sql, self + end + + # REMOVEME + def name + relation.name + '_external' + end + end +end diff --git a/lib/arel/engines/sql/relations/utilities/nil.rb b/lib/arel/engines/sql/relations/utilities/nil.rb new file mode 100644 index 0000000000..519ea8acf1 --- /dev/null +++ b/lib/arel/engines/sql/relations/utilities/nil.rb @@ -0,0 +1,6 @@ +module Arel + class Nil < Relation + def table_sql(formatter = nil); '' end + def name; '' end + end +end diff --git a/lib/arel/engines/sql/relations/utilities/recursion.rb b/lib/arel/engines/sql/relations/utilities/recursion.rb new file mode 100644 index 0000000000..84a526f57c --- /dev/null +++ b/lib/arel/engines/sql/relations/utilities/recursion.rb @@ -0,0 +1,13 @@ +module Arel + module Recursion + module BaseCase + def table + self + end + + def table_sql(formatter = Sql::TableReference.new(self)) + formatter.table self + end + end + end +end diff --git a/lib/arel/engines/sql/relations/writes.rb b/lib/arel/engines/sql/relations/writes.rb new file mode 100644 index 0000000000..f1a9bfd2ac --- /dev/null +++ b/lib/arel/engines/sql/relations/writes.rb @@ -0,0 +1,39 @@ +module Arel + class Deletion < Compound + def to_sql(formatter = nil) + build_query \ + "DELETE", + "FROM #{table_sql}", + ("WHERE #{wheres.collect(&:to_sql).join('\n\tAND ')}" unless wheres.blank? ), + ("LIMIT #{taken}" unless taken.blank? ) + end + end + + class Insert < Compound + def to_sql(formatter = nil) + build_query \ + "INSERT", + "INTO #{table_sql}", + "(#{record.keys.collect { |key| engine.quote_column_name(key.name) }.join(', ')})", + "VALUES (#{record.collect { |key, value| key.format(value) }.join(', ')})" + end + end + + class Update < Compound + def to_sql(formatter = nil) + build_query \ + "UPDATE #{table_sql} SET", + assignment_sql, + ("WHERE #{wheres.collect(&:to_sql).join('\n\tAND ')}" unless wheres.blank? ), + ("LIMIT #{taken}" unless taken.blank? ) + end + + protected + + def assignment_sql + assignments.collect do |attribute, value| + "#{engine.quote_column_name(attribute.name)} = #{attribute.format(value)}" + end.join(",\n") + end + end +end |