diff options
Diffstat (limited to 'lib')
60 files changed, 1629 insertions, 0 deletions
diff --git a/lib/arel.rb b/lib/arel.rb new file mode 100644 index 0000000000..54a31b6ed0 --- /dev/null +++ b/lib/arel.rb @@ -0,0 +1,13 @@ +$LOAD_PATH.unshift(File.dirname(__FILE__)) + +require 'rubygems' +require 'activesupport' +require 'active_support/dependencies' +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/delegation' +require 'activerecord' +require 'active_record/connection_adapters/abstract/quoting' + +require 'arel/algebra' +require 'arel/engines' +require 'arel/session' diff --git a/lib/arel/algebra.rb b/lib/arel/algebra.rb new file mode 100644 index 0000000000..c206fea0b0 --- /dev/null +++ b/lib/arel/algebra.rb @@ -0,0 +1,4 @@ +require 'arel/algebra/extensions' +require 'arel/algebra/predicates' +require 'arel/algebra/relations' +require 'arel/algebra/primitives' diff --git a/lib/arel/algebra/extensions.rb b/lib/arel/algebra/extensions.rb new file mode 100644 index 0000000000..bc8edd3274 --- /dev/null +++ b/lib/arel/algebra/extensions.rb @@ -0,0 +1,4 @@ +require 'arel/algebra/extensions/object' +require 'arel/algebra/extensions/class' +require 'arel/algebra/extensions/symbol' +require 'arel/algebra/extensions/hash' diff --git a/lib/arel/algebra/extensions/class.rb b/lib/arel/algebra/extensions/class.rb new file mode 100644 index 0000000000..520111b90f --- /dev/null +++ b/lib/arel/algebra/extensions/class.rb @@ -0,0 +1,32 @@ +module Arel + module ClassExtensions + def attributes(*attrs) + @attributes = attrs + attr_reader *attrs + end + + def deriving(*methods) + methods.each { |m| derive m } + end + + def derive(method_name) + methods = { + :initialize => " + def #{method_name}(#{@attributes.join(',')}) + #{@attributes.collect { |a| "@#{a} = #{a}" }.join("\n")} + end + ", + :== => " + def ==(other) + #{name} === other && + #{@attributes.collect { |a| "@#{a} == other.#{a}" }.join(" &&\n")} + end + " + } + class_eval methods[method_name], __FILE__, __LINE__ + end + + Class.send(:include, self) + end +end + diff --git a/lib/arel/algebra/extensions/hash.rb b/lib/arel/algebra/extensions/hash.rb new file mode 100644 index 0000000000..05c15e7ebe --- /dev/null +++ b/lib/arel/algebra/extensions/hash.rb @@ -0,0 +1,11 @@ +module Arel + module HashExtensions + def bind(relation) + inject({}) do |bound, (key, value)| + bound.merge(key.bind(relation) => value.bind(relation)) + end + end + + Hash.send(:include, self) + end +end diff --git a/lib/arel/algebra/extensions/object.rb b/lib/arel/algebra/extensions/object.rb new file mode 100644 index 0000000000..d8c60b5dd5 --- /dev/null +++ b/lib/arel/algebra/extensions/object.rb @@ -0,0 +1,17 @@ +module Arel + module ObjectExtensions + def bind(relation) + Arel::Value.new(self, relation) + end + + def find_correlate_in(relation) + bind(relation) + end + + def let + yield(self) + end + + Object.send(:include, self) + end +end diff --git a/lib/arel/algebra/extensions/symbol.rb b/lib/arel/algebra/extensions/symbol.rb new file mode 100644 index 0000000000..9bb47ef7ab --- /dev/null +++ b/lib/arel/algebra/extensions/symbol.rb @@ -0,0 +1,9 @@ +module Arel + module SymbolExtensions + def to_attribute(relation) + Arel::Attribute.new(relation, self) + end + + Symbol.send(:include, self) + end +end diff --git a/lib/arel/algebra/predicates.rb b/lib/arel/algebra/predicates.rb new file mode 100644 index 0000000000..72167c2b27 --- /dev/null +++ b/lib/arel/algebra/predicates.rb @@ -0,0 +1,41 @@ +module Arel + class Predicate + def or(other_predicate) + Or.new(self, other_predicate) + end + + def and(other_predicate) + And.new(self, other_predicate) + end + end + + class Binary < Predicate + attributes :operand1, :operand2 + deriving :initialize + + def ==(other) + self.class === other and + @operand1 == other.operand1 and + @operand2 == other.operand2 + end + + def bind(relation) + self.class.new(operand1.find_correlate_in(relation), operand2.find_correlate_in(relation)) + end + end + + class Equality < Binary + def ==(other) + Equality === other and + ((operand1 == other.operand1 and operand2 == other.operand2) or + (operand1 == other.operand2 and operand2 == other.operand1)) + end + end + + class GreaterThanOrEqualTo < Binary; end + class GreaterThan < Binary; end + class LessThanOrEqualTo < Binary; end + class LessThan < Binary; end + class Match < Binary; end + class In < Binary; end +end diff --git a/lib/arel/algebra/primitives.rb b/lib/arel/algebra/primitives.rb new file mode 100644 index 0000000000..df8d16a5d5 --- /dev/null +++ b/lib/arel/algebra/primitives.rb @@ -0,0 +1,5 @@ +require 'arel/algebra/primitives/attribute' +require 'arel/algebra/primitives/ordering' +require 'arel/algebra/primitives/value' +require 'arel/algebra/primitives/expression' + diff --git a/lib/arel/algebra/primitives/attribute.rb b/lib/arel/algebra/primitives/attribute.rb new file mode 100644 index 0000000000..44a2f41733 --- /dev/null +++ b/lib/arel/algebra/primitives/attribute.rb @@ -0,0 +1,150 @@ +require 'set' + +module Arel + class Attribute + attributes :relation, :name, :alias, :ancestor + deriving :== + delegate :engine, :christener, :to => :relation + + def initialize(relation, name, options = {}) + @relation, @name, @alias, @ancestor = relation, name, options[:alias], options[:ancestor] + end + + def named?(hypothetical_name) + (@alias || name).to_s == hypothetical_name.to_s + end + + def aggregation? + false + end + + def inspect + "<Attribute #{name}>" + end + + module Transformations + def self.included(klass) + klass.send :alias_method, :eql?, :== + end + + def hash + @hash ||= history.size + name.hash + relation.hash + end + + def as(aliaz = nil) + Attribute.new(relation, name, :alias => aliaz, :ancestor => self) + end + + def bind(new_relation) + relation == new_relation ? self : Attribute.new(new_relation, name, :alias => @alias, :ancestor => self) + end + + def to_attribute(relation) + bind(relation) + end + end + include Transformations + + module Congruence + def history + @history ||= [self] + (ancestor ? ancestor.history : []) + end + + def join? + relation.join? + end + + def root + history.last + end + + def original_relation + @original_relation ||= original_attribute.relation + end + + def original_attribute + @original_attribute ||= history.detect { |a| !a.join? } + end + + def find_correlate_in(relation) + relation[self] || self + end + + def descends_from?(other) + history.include?(other) + end + + def /(other) + other ? (history & other.history).size : 0 + end + end + include Congruence + + module Predications + def eq(other) + Equality.new(self, other) + end + + def lt(other) + LessThan.new(self, other) + end + + def lteq(other) + LessThanOrEqualTo.new(self, other) + end + + def gt(other) + GreaterThan.new(self, other) + end + + def gteq(other) + GreaterThanOrEqualTo.new(self, other) + end + + def matches(regexp) + Match.new(self, regexp) + end + + def in(array) + In.new(self, array) + end + end + include Predications + + module Expressions + def count(distinct = false) + distinct ? Distinct.new(self).count : Count.new(self) + end + + def sum + Sum.new(self) + end + + def maximum + Maximum.new(self) + end + + def minimum + Minimum.new(self) + end + + def average + Average.new(self) + end + end + include Expressions + + module Orderings + def asc + Ascending.new(self) + end + + def desc + Descending.new(self) + end + + alias_method :to_ordering, :asc + end + include Orderings + end +end diff --git a/lib/arel/algebra/primitives/expression.rb b/lib/arel/algebra/primitives/expression.rb new file mode 100644 index 0000000000..875498c282 --- /dev/null +++ b/lib/arel/algebra/primitives/expression.rb @@ -0,0 +1,43 @@ +module Arel + class Expression < Attribute + attributes :attribute, :alias, :ancestor + deriving :== + delegate :relation, :to => :attribute + alias_method :name, :alias + + def initialize(attribute, aliaz = nil, ancestor = nil) + @attribute, @alias, @ancestor = attribute, aliaz, ancestor + end + + def aggregation? + true + end + + def inspect + "<#{self.class.name} #{attribute.inspect}>" + end + + module Transformations + def as(aliaz) + self.class.new(attribute, aliaz, self) + end + + def bind(new_relation) + new_relation == relation ? self : self.class.new(attribute.bind(new_relation), @alias, self) + end + + def to_attribute(relation) + Attribute.new(relation, @alias, :ancestor => self) + end + end + include Transformations + end + + class Count < Expression; end + class Distinct < Expression; end + class Sum < Expression; end + class Maximum < Expression; end + class Minimum < Expression; end + class Average < Expression; end +end + diff --git a/lib/arel/algebra/primitives/ordering.rb b/lib/arel/algebra/primitives/ordering.rb new file mode 100644 index 0000000000..3efb4c6280 --- /dev/null +++ b/lib/arel/algebra/primitives/ordering.rb @@ -0,0 +1,23 @@ +module Arel + class Ordering + delegate :relation, :to => :attribute + + def bind(relation) + self.class.new(attribute.bind(relation)) + end + + def to_ordering + self + end + end + + class Ascending < Ordering + attributes :attribute + deriving :initialize, :== + end + + class Descending < Ordering + attributes :attribute + deriving :initialize, :== + end +end diff --git a/lib/arel/algebra/primitives/value.rb b/lib/arel/algebra/primitives/value.rb new file mode 100644 index 0000000000..e363805140 --- /dev/null +++ b/lib/arel/algebra/primitives/value.rb @@ -0,0 +1,14 @@ +module Arel + class Value + attributes :value, :relation + deriving :initialize, :== + + def bind(relation) + Value.new(value, relation) + end + + def to_ordering + self + end + end +end diff --git a/lib/arel/algebra/relations.rb b/lib/arel/algebra/relations.rb new file mode 100644 index 0000000000..f9fa24ba25 --- /dev/null +++ b/lib/arel/algebra/relations.rb @@ -0,0 +1,14 @@ +require 'arel/algebra/relations/relation' +require 'arel/algebra/relations/utilities/compound' +require 'arel/algebra/relations/utilities/nil' +require 'arel/algebra/relations/utilities/externalization' +require 'arel/algebra/relations/row' +require 'arel/algebra/relations/writes' +require 'arel/algebra/relations/operations/alias' +require 'arel/algebra/relations/operations/group' +require 'arel/algebra/relations/operations/join' +require 'arel/algebra/relations/operations/order' +require 'arel/algebra/relations/operations/project' +require 'arel/algebra/relations/operations/where' +require 'arel/algebra/relations/operations/skip' +require 'arel/algebra/relations/operations/take' diff --git a/lib/arel/algebra/relations/operations/alias.rb b/lib/arel/algebra/relations/operations/alias.rb new file mode 100644 index 0000000000..0331d98b85 --- /dev/null +++ b/lib/arel/algebra/relations/operations/alias.rb @@ -0,0 +1,7 @@ +module Arel + class Alias < Compound + attributes :relation + deriving :initialize + alias_method :==, :equal? + end +end diff --git a/lib/arel/algebra/relations/operations/group.rb b/lib/arel/algebra/relations/operations/group.rb new file mode 100644 index 0000000000..2bfc42214b --- /dev/null +++ b/lib/arel/algebra/relations/operations/group.rb @@ -0,0 +1,12 @@ +module Arel + class Group < Compound + attributes :relation, :groupings + deriving :== + + def initialize(relation, *groupings, &block) + @relation = relation + @groupings = (groupings + arguments_from_block(relation, &block)) \ + .collect { |g| g.bind(relation) } + end + end +end diff --git a/lib/arel/algebra/relations/operations/join.rb b/lib/arel/algebra/relations/operations/join.rb new file mode 100644 index 0000000000..e9320f28e1 --- /dev/null +++ b/lib/arel/algebra/relations/operations/join.rb @@ -0,0 +1,64 @@ +module Arel + class Join < Relation + attributes :relation1, :relation2, :predicates + deriving :== + delegate :name, :to => :relation1 + + def initialize(relation1, relation2 = Nil.instance, *predicates) + @relation1, @relation2, @predicates = relation1, relation2, predicates + end + + def hash + @hash ||= :relation1.hash + end + + def eql?(other) + self == other + end + + def attributes + @attributes ||= (relation1.externalize.attributes + + relation2.externalize.attributes).collect { |a| a.bind(self) } + end + + def wheres + # TESTME bind to self? + relation1.externalize.wheres + end + + def ons + @ons ||= @predicates.collect { |p| p.bind(self) } + end + + # TESTME + def externalizable? + relation1.externalizable? or relation2.externalizable? + end + + def join? + true + end + + def engine + relation1.engine != relation2.engine ? Memory::Engine.new : relation1.engine + end + end + + class InnerJoin < Join; end + class OuterJoin < Join; end + class StringJoin < Join + def attributes + relation1.externalize.attributes + end + + def engine + relation1.engine + end + end + + class Relation + def join? + false + end + end +end diff --git a/lib/arel/algebra/relations/operations/order.rb b/lib/arel/algebra/relations/operations/order.rb new file mode 100644 index 0000000000..a589b56997 --- /dev/null +++ b/lib/arel/algebra/relations/operations/order.rb @@ -0,0 +1,18 @@ +module Arel + class Order < Compound + attributes :relation, :orderings + deriving :== + + def initialize(relation, *orderings, &block) + @relation = relation + @orderings = (orderings + arguments_from_block(relation, &block)) \ + .collect { |o| o.bind(relation) } + end + + # TESTME + def orders + # QUESTION - do we still need relation.orders ? + (orderings + relation.orders).collect { |o| o.bind(self) }.collect { |o| o.to_ordering } + end + end +end diff --git a/lib/arel/algebra/relations/operations/project.rb b/lib/arel/algebra/relations/operations/project.rb new file mode 100644 index 0000000000..223d320e22 --- /dev/null +++ b/lib/arel/algebra/relations/operations/project.rb @@ -0,0 +1,20 @@ +module Arel + class Project < Compound + attributes :relation, :projections + deriving :== + + def initialize(relation, *projections, &block) + @relation = relation + @projections = (projections + arguments_from_block(relation, &block)) \ + .collect { |p| p.bind(relation) } + end + + def attributes + @attributes ||= projections.collect { |p| p.bind(self) } + end + + def externalizable? + attributes.any?(&:aggregation?) or relation.externalizable? + end + end +end diff --git a/lib/arel/algebra/relations/operations/skip.rb b/lib/arel/algebra/relations/operations/skip.rb new file mode 100644 index 0000000000..689e06e1c3 --- /dev/null +++ b/lib/arel/algebra/relations/operations/skip.rb @@ -0,0 +1,6 @@ +module Arel + class Skip < Compound + attributes :relation, :skipped + deriving :initialize, :== + end +end diff --git a/lib/arel/algebra/relations/operations/take.rb b/lib/arel/algebra/relations/operations/take.rb new file mode 100644 index 0000000000..eb32ec492e --- /dev/null +++ b/lib/arel/algebra/relations/operations/take.rb @@ -0,0 +1,10 @@ +module Arel + class Take < Compound + attributes :relation, :taken + deriving :initialize, :== + + def externalizable? + true + end + end +end diff --git a/lib/arel/algebra/relations/operations/where.rb b/lib/arel/algebra/relations/operations/where.rb new file mode 100644 index 0000000000..608aaeb4b7 --- /dev/null +++ b/lib/arel/algebra/relations/operations/where.rb @@ -0,0 +1,16 @@ +module Arel + class Where < Compound + attributes :relation, :predicate + deriving :== + + def initialize(relation, *predicates, &block) + predicate = block_given?? yield(relation) : predicates.shift + @relation = predicates.empty?? relation : Where.new(relation, *predicates) + @predicate = predicate.bind(@relation) + end + + def wheres + @wheres ||= (relation.wheres + [predicate]).collect { |p| p.bind(self) } + end + end +end diff --git a/lib/arel/algebra/relations/relation.rb b/lib/arel/algebra/relations/relation.rb new file mode 100644 index 0000000000..9fdac26528 --- /dev/null +++ b/lib/arel/algebra/relations/relation.rb @@ -0,0 +1,136 @@ +module Arel + class Relation + attr_reader :count + + def session + Session.new + end + + def call + engine.read(self) + end + + def bind(relation) + self + end + + module Enumerable + include ::Enumerable + + def each(&block) + session.read(self).each(&block) + end + + def first + session.read(self).first + end + end + include Enumerable + + module Operable + def join(other_relation = nil, join_class = InnerJoin) + case other_relation + when String + StringJoin.new(self, other_relation) + when Relation + JoinOperation.new(join_class, self, other_relation) + else + self + end + end + + def outer_join(other_relation = nil) + join(other_relation, OuterJoin) + end + + [:where, :project, :order, :take, :skip, :group].each do |operation_name| + class_eval <<-OPERATION, __FILE__, __LINE__ + def #{operation_name}(*arguments, &block) + arguments.all?(&:blank?) && !block_given?? self : #{operation_name.to_s.classify}.new(self, *arguments, &block) + end + OPERATION + end + + def alias + Alias.new(self) + end + + module Writable + def insert(record) + session.create Insert.new(self, record) + end + + def update(assignments) + session.update Update.new(self, assignments) + end + + def delete + session.delete Deletion.new(self) + end + end + include Writable + + JoinOperation = Struct.new(:join_class, :relation1, :relation2) do + def on(*predicates) + join_class.new(relation1, relation2, *predicates) + end + end + end + include Operable + + module AttributeAccessable + def [](index) + case index + when Symbol, String + find_attribute_matching_name(index) + when Attribute, Expression + find_attribute_matching_attribute(index) + when ::Array + # TESTME + index.collect { |i| self[i] } + end + end + + def find_attribute_matching_name(name) + attributes.detect { |a| a.named?(name) } + end + + def find_attribute_matching_attribute(attribute) + matching_attributes(attribute).max do |a1, a2| + (a1.original_attribute / attribute) <=> (a2.original_attribute / attribute) + end + end + + def position_of(attribute) + (@position_of ||= Hash.new do |h, attribute| + h[attribute] = attributes.index(self[attribute]) + end)[attribute] + end + + private + def matching_attributes(attribute) + (@matching_attributes ||= attributes.inject({}) do |hash, a| + (hash[a.root] ||= []) << a + hash + end)[attribute.root] || [] + end + + def has_attribute?(attribute) + !matching_attributes(attribute).empty? + end + end + include AttributeAccessable + + module DefaultOperations + def attributes; [] end + def wheres; [] end + def orders; [] end + def inserts; [] end + def groupings; [] end + def joins(formatter = nil); nil end # FIXME + def taken; nil end + def skipped; nil end + end + include DefaultOperations + end +end diff --git a/lib/arel/algebra/relations/row.rb b/lib/arel/algebra/relations/row.rb new file mode 100644 index 0000000000..3158557448 --- /dev/null +++ b/lib/arel/algebra/relations/row.rb @@ -0,0 +1,26 @@ +module Arel + class Row + attributes :relation, :tuple + deriving :==, :initialize + + def [](attribute) + attribute.type_cast(tuple[relation.position_of(attribute)]) + end + + def slice(*attributes) + Row.new(relation, attributes.inject([]) do |cheese, attribute| + # FIXME TESTME method chaining + cheese << tuple[relation.relation.position_of(attribute)] + cheese + end) + end + + def bind(relation) + Row.new(relation, tuple) + end + + def combine(other, relation) + Row.new(relation, tuple + other.tuple) + end + end +end diff --git a/lib/arel/algebra/relations/utilities/compound.rb b/lib/arel/algebra/relations/utilities/compound.rb new file mode 100644 index 0000000000..5e775618f1 --- /dev/null +++ b/lib/arel/algebra/relations/utilities/compound.rb @@ -0,0 +1,30 @@ +module Arel + class Compound < Relation + attr_reader :relation + delegate :joins, :join?, :inserts, :taken, :skipped, :name, :externalizable?, + :column_for, :engine, + :to => :relation + + [:attributes, :wheres, :groupings, :orders].each do |operation_name| + class_eval <<-OPERATION, __FILE__, __LINE__ + def #{operation_name} + @#{operation_name} ||= relation.#{operation_name}.collect { |o| o.bind(self) } + end + OPERATION + end + + def hash + @hash ||= :relation.hash + end + + def eql?(other) + self == other + end + + private + + def arguments_from_block(relation, &block) + block_given?? [yield(relation)] : [] + end + end +end diff --git a/lib/arel/algebra/relations/utilities/externalization.rb b/lib/arel/algebra/relations/utilities/externalization.rb new file mode 100644 index 0000000000..13758ccec9 --- /dev/null +++ b/lib/arel/algebra/relations/utilities/externalization.rb @@ -0,0 +1,24 @@ +module Arel + class Externalization < Compound + attributes :relation + deriving :initialize, :== + + def wheres + [] + end + + def attributes + @attributes ||= relation.attributes.collect { |a| a.to_attribute(self) } + end + end + + class Relation + def externalize + @externalized ||= externalizable?? Externalization.new(self) : self + end + + def externalizable? + false + end + end +end diff --git a/lib/arel/algebra/relations/utilities/nil.rb b/lib/arel/algebra/relations/utilities/nil.rb new file mode 100644 index 0000000000..6a9d678c45 --- /dev/null +++ b/lib/arel/algebra/relations/utilities/nil.rb @@ -0,0 +1,7 @@ +require 'singleton' + +module Arel + class Nil < Relation + include Singleton + end +end diff --git a/lib/arel/algebra/relations/writes.rb b/lib/arel/algebra/relations/writes.rb new file mode 100644 index 0000000000..d344987915 --- /dev/null +++ b/lib/arel/algebra/relations/writes.rb @@ -0,0 +1,36 @@ +module Arel + class Deletion < Compound + attributes :relation + deriving :initialize, :== + + def call + engine.delete(self) + end + end + + class Insert < Compound + attributes :relation, :record + deriving :== + + def initialize(relation, record) + @relation, @record = relation, record.bind(relation) + end + + def call + engine.create(self) + end + end + + class Update < Compound + attributes :relation, :assignments + deriving :== + + def initialize(relation, assignments) + @relation, @assignments = relation, assignments.bind(relation) + end + + def call + engine.update(self) + end + end +end diff --git a/lib/arel/engines.rb b/lib/arel/engines.rb new file mode 100644 index 0000000000..cd848d83e2 --- /dev/null +++ b/lib/arel/engines.rb @@ -0,0 +1,2 @@ +require 'arel/engines/sql' +require 'arel/engines/memory' 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 diff --git a/lib/arel/session.rb b/lib/arel/session.rb new file mode 100644 index 0000000000..921ad0a7ac --- /dev/null +++ b/lib/arel/session.rb @@ -0,0 +1,48 @@ +module Arel + class Session + class << self + attr_accessor :instance + alias_method :manufacture, :new + + def start + if @started + yield + else + begin + @started = true + @instance = manufacture + metaclass.send :alias_method, :new, :instance + yield + ensure + metaclass.send :alias_method, :new, :manufacture + @started = false + end + end + end + end + + module CRUD + def create(insert) + insert.call + insert + end + + def read(select) + (@read ||= Hash.new do |hash, select| + hash[select] = select.call + end)[select] + end + + def update(update) + update.call + update + end + + def delete(delete) + delete.call + delete + end + end + include CRUD + end +end |