diff options
147 files changed, 4342 insertions, 1320 deletions
diff --git a/.gitignore b/.gitignore index 2bab52ab21..c6dd721751 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ coverage/* config/database.yml +spec/fixtures/*database* *.DS_Store +debug.log diff --git a/README b/README.markdown index e979dbc2a3..4d95234423 100644 --- a/README +++ b/README.markdown @@ -18,7 +18,7 @@ Generating a query with ARel is simple. For example, in order to produce you construct a table relation and convert it to sql: - users = Arel(:users) + users = Table(:users) users.to_sql In fact, you will probably never call `#to_sql`. Rather, you'll work with data from the table directly. You can iterate through all rows in the `users` table like this: @@ -81,7 +81,7 @@ The `AND` operator will behave similarly. Finally, most operations take a block form. For example: - Arel(:users) \ + Table(:users) \ .where { |u| u[:id].eq(1) } \ .project { |u| u[:id] } @@ -95,7 +95,7 @@ The examples above are fairly simple and other libraries match or come close to Where Arel really shines in its ability to handle complex joins and aggregations. As a first example, let's consider an "adjacency list", a tree represented in a table. Suppose we have a table `comments`, representing a threaded discussion: - comments = Arel(:comments) + comments = Table(:comments) And this table has the following attributes: @@ -1,15 +1,40 @@ require 'rubygems' require 'spec/rake/spectask' -Spec::Rake::SpecTask.new do |t| - t.spec_files = FileList['spec/**/*_spec.rb'] -end +spec_file_list = FileList['spec/**/*_spec.rb'] +desc "Run specs using RCov (uses mysql database adapter)" Spec::Rake::SpecTask.new(:coverage) do |t| - t.spec_files = FileList['spec/**/*_spec.rb'] + t.spec_files = + ["spec/connections/mysql_connection.rb"] + + spec_file_list + t.rcov = true - t.rcov_opts = ['-x', 'spec,gems'] + t.rcov_opts << '--exclude' << "spec,gems" + t.rcov_opts << '--text-summary' + t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse' + t.rcov_opts << '--only-uncovered' +end + +namespace :spec do + for adapter in %w[mysql sqlite3 postgresql] + desc "Run specs with the #{adapter} database adapter" + Spec::Rake::SpecTask.new(adapter) do |t| + t.spec_files = + ["spec/connections/#{adapter}_connection.rb"] + + ["spec/schemas/#{adapter}_schema.rb"] + + spec_file_list + end + end end +desc "Run specs with mysql and sqlite3 database adapters (default)" +task :spec => ["spec:sqlite3", "spec:mysql", "spec:postgresql"] + desc "Default task is to run specs" -task :default => :spec
\ No newline at end of file +task :default => :spec + +desc 'Removes trailing whitespace' +task :whitespace do + sh %{find . -name '*.rb' -exec sed -i '' 's/ *$//g' {} \\;} +end @@ -1,29 +1,26 @@ todo: -- joining with LIMIT is like aggregations!! +- fix AR again +- blocks for joins +- fix sql insertions +- implement mnesia adapter -users.delete().where( - addresses.c.user_id== - select([users.c.id]). - where(users.c.name=='jack') - ) - - SELECT id, name, - (select count(*) FROM addresses WHERE - user_id=users.id) - FROM users - - SELECT users.*, (SELECT count(id) FROM addresses WHERE - addresses.user_id=users.id) FROM users -- blocks for all operations -- result sets to attr correlation too +- CLEANUP!!!!! +- rename externalize to derived. +- deal with table tests in algebra +- fix grouping +- audit unit coverage of algebra +- data objects +- remove all explicit aliasing +- scoped writes +- refactor adapter pattern +- break out adapters into sep modules +- projection is by definition distincting? +- union/intersection - cache expiry on write - - rewrite of arecord querycache test in light of this - transactions -- scoped writes done: - and/or w/ predicates -- mock out database . Relation <=> Relation -> InnerJoinOperation . Relation << Relation -> LeftOuterJoinOperation . InnerJoinOperation.on(*Predicate) -> InnerJoinRelation @@ -74,6 +71,7 @@ done: - test Value, in particular bind. - test blank checks in relation.rb - rename active_relation to arel +- mock out database - fix complex joining cases: - active record query adapter - anonymous table names @@ -84,6 +82,20 @@ done: - test: find_attribute_given_attribute and all @attribute ||= everywhere and memoization of table class. - rename select to where - rename all ion classes +- joining with LIMIT is like aggregations!! +- blocks for non-joins +- expressions should be class-based, and joins too, anything _sql should be renamed +- implement in memory adapter +- clean up block_given stuff +- reorganize sql tests +- recursive memory operations +- reorganize memory tests +- result sets to attr correlation too +- implement joins in memory +- result sets should be array relations +- fix AR +- insertions for in memory +- cross-engine joins icebox: - #bind in Attribute and Expression should be doing a descend? @@ -98,3 +110,9 @@ icebox: @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } - lock - SELECT suchandsuch FOR UPDATE +- joins become subselects in writes: +users.delete().where( + addresses.c.user_id== + select([users.c.id]). + where(users.c.name=='jack') + )
\ No newline at end of file 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 diff --git a/spec/arel/algebra/unit/predicates/binary_spec.rb b/spec/arel/algebra/unit/predicates/binary_spec.rb new file mode 100644 index 0000000000..14fd7ab21b --- /dev/null +++ b/spec/arel/algebra/unit/predicates/binary_spec.rb @@ -0,0 +1,33 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Binary do + before do + @relation = Table.new(:users) + @attribute1 = @relation[:id] + @attribute2 = @relation[:name] + class ConcreteBinary < Binary + end + end + + describe '#bind' do + before do + @another_relation = @relation.alias + end + + describe 'when both operands are attributes' do + it "manufactures an expression with the attributes bound to the relation" do + ConcreteBinary.new(@attribute1, @attribute2).bind(@another_relation). \ + should == ConcreteBinary.new(@another_relation[@attribute1], @another_relation[@attribute2]) + end + end + + describe 'when an operand is a value' do + it "manufactures an expression with unmodified values" do + ConcreteBinary.new(@attribute1, "asdf").bind(@another_relation). \ + should == ConcreteBinary.new(@attribute1.find_correlate_in(@another_relation), "asdf".find_correlate_in(@another_relation)) + end + end + end + end +end diff --git a/spec/arel/unit/predicates/equality_spec.rb b/spec/arel/algebra/unit/predicates/equality_spec.rb index 8a58ba3096..af91f8b51b 100644 --- a/spec/arel/unit/predicates/equality_spec.rb +++ b/spec/arel/algebra/unit/predicates/equality_spec.rb @@ -1,4 +1,4 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') module Arel describe Equality do @@ -8,42 +8,20 @@ module Arel @attribute1 = @relation1[:id] @attribute2 = @relation2[:user_id] end - - describe '==' do + + describe '==' do it "obtains if attribute1 and attribute2 are identical" do Equality.new(@attribute1, @attribute2).should == Equality.new(@attribute1, @attribute2) Equality.new(@attribute1, @attribute2).should_not == Equality.new(@attribute1, @attribute1) end - + it "obtains if the concrete type of the predicates are identical" do Equality.new(@attribute1, @attribute2).should_not == Binary.new(@attribute1, @attribute2) end - + it "is commutative on the attributes" do Equality.new(@attribute1, @attribute2).should == Equality.new(@attribute2, @attribute1) end end - - describe '#to_sql' do - describe 'when relating to a non-nil value' do - it "manufactures an equality predicate" do - Equality.new(@attribute1, @attribute2).to_sql.should be_like(" - `users`.`id` = `photos`.`user_id` - ") - end - end - - describe 'when relation to a nil value' do - before do - @nil = nil - end - - it "manufactures an is null predicate" do - Equality.new(@attribute1, @nil).to_sql.should be_like(" - `users`.`id` IS NULL - ") - end - end - end end -end
\ No newline at end of file +end diff --git a/spec/arel/algebra/unit/predicates/in_spec.rb b/spec/arel/algebra/unit/predicates/in_spec.rb new file mode 100644 index 0000000000..a8a15ce4e3 --- /dev/null +++ b/spec/arel/algebra/unit/predicates/in_spec.rb @@ -0,0 +1,10 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe In do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + end +end diff --git a/spec/arel/unit/primitives/attribute_spec.rb b/spec/arel/algebra/unit/primitives/attribute_spec.rb index b341c6f88e..2ca63ba48e 100644 --- a/spec/arel/unit/primitives/attribute_spec.rb +++ b/spec/arel/algebra/unit/primitives/attribute_spec.rb @@ -1,4 +1,4 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') module Arel describe Attribute do @@ -6,57 +6,71 @@ module Arel @relation = Table.new(:users) @attribute = @relation[:id] end - + + describe "#inspect" do + it "returns a simple, short inspect string" do + @attribute.inspect.should == "<Attribute id>" + end + end + describe Attribute::Transformations do describe '#as' do it "manufactures an aliased attributed" do @attribute.as(:alias).should == Attribute.new(@relation, @attribute.name, :alias => :alias, :ancestor => @attribute) end end - + describe '#bind' do it "manufactures an attribute with the relation bound and self as an ancestor" do derived_relation = @relation.where(@relation[:id].eq(1)) @attribute.bind(derived_relation).should == Attribute.new(derived_relation, @attribute.name, :ancestor => @attribute) end - + it "returns self if the substituting to the same relation" do @attribute.bind(@relation).should == @attribute end end - + describe '#to_attribute' do - it "returns self" do - @attribute.to_attribute.should == @attribute + describe 'when the given relation is the same as the attributes relation' do + it "returns self" do + @attribute.to_attribute(@relation).should == @attribute + end + end + + describe 'when the given relation differs from the attributes relation' do + it 'binds to the new relation' do + @attribute.to_attribute(new_relation = @relation.alias).should == @attribute.bind(new_relation) + end end end end - + describe '#column' do it "returns the corresponding column in the relation" do @attribute.column.should == @relation.column_for(@attribute) end end - + describe '#engine' do it "delegates to its relation" do Attribute.new(@relation, :id).engine.should == @relation.engine end end - + describe Attribute::Congruence do describe '/' do before do @aliased_relation = @relation.alias @doubly_aliased_relation = @aliased_relation.alias end - + describe 'when dividing two unrelated attributes' do it "returns 0.0" do (@relation[:id] / @relation[:name]).should == 0.0 end end - + describe 'when dividing two matching attributes' do it 'returns a the highest score for the most similar attributes' do (@aliased_relation[:id] / @relation[:id]) \ @@ -67,97 +81,103 @@ module Arel end end end - - describe '#to_sql' do - describe 'for a simple attribute' do - it "manufactures sql with an alias" do - @attribute.to_sql.should be_like("`users`.`id`") - end - end - end - + describe Attribute::Predications do before do @attribute = Attribute.new(@relation, :name) end - + describe '#eq' do it "manufactures an equality predicate" do @attribute.eq('name').should == Equality.new(@attribute, 'name') end end - + describe '#lt' do it "manufactures a less-than predicate" do @attribute.lt(10).should == LessThan.new(@attribute, 10) end end - + describe '#lteq' do it "manufactures a less-than or equal-to predicate" do @attribute.lteq(10).should == LessThanOrEqualTo.new(@attribute, 10) end end - + describe '#gt' do it "manufactures a greater-than predicate" do @attribute.gt(10).should == GreaterThan.new(@attribute, 10) end end - + describe '#gteq' do it "manufactures a greater-than or equal-to predicate" do @attribute.gteq(10).should == GreaterThanOrEqualTo.new(@attribute, 10) end end - + describe '#matches' do it "manufactures a match predicate" do @attribute.matches(/.*/).should == Match.new(@attribute, /.*/) end end - + describe '#in' do it "manufactures an in predicate" do @attribute.in(1..30).should == In.new(@attribute, (1..30)) end end end - + describe Attribute::Expressions do before do - @attribute = Attribute.new(@relation, :name) + @attribute = Attribute.new(@relation, :name) end - + describe '#count' do it "manufactures a count Expression" do - @attribute.count.should == Expression.new(@attribute, "COUNT") + @attribute.count.should == Count.new(@attribute) end end - + describe '#sum' do it "manufactures a sum Expression" do - @attribute.sum.should == Expression.new(@attribute, "SUM") + @attribute.sum.should == Sum.new(@attribute) end end - + describe '#maximum' do it "manufactures a maximum Expression" do - @attribute.maximum.should == Expression.new(@attribute, "MAX") + @attribute.maximum.should == Maximum.new(@attribute) end end - + describe '#minimum' do it "manufactures a minimum Expression" do - @attribute.minimum.should == Expression.new(@attribute, "MIN") + @attribute.minimum.should == Minimum.new(@attribute) end end - + describe '#average' do it "manufactures an average Expression" do - @attribute.average.should == Expression.new(@attribute, "AVG") + @attribute.average.should == Average.new(@attribute) + end + end + end + + describe Attribute::Orderings do + describe '#asc' do + it 'manufactures an ascending ordering' do + @attribute.asc.should == Ascending.new(@attribute) end - end + end + + describe '#desc' do + it 'manufactures a descending ordering' do + @attribute.desc.should == Descending.new(@attribute) + end + end end end -end
\ No newline at end of file +end diff --git a/spec/arel/unit/primitives/expression_spec.rb b/spec/arel/algebra/unit/primitives/expression_spec.rb index 4943f4ef33..768bb492a7 100644 --- a/spec/arel/unit/primitives/expression_spec.rb +++ b/spec/arel/algebra/unit/primitives/expression_spec.rb @@ -1,4 +1,4 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') module Arel describe Expression do @@ -7,15 +7,21 @@ module Arel @attribute = @relation[:id] end + describe "#inspect" do + it "returns a simple, short inspect string" do + @attribute.count.inspect.should == "<Arel::Count <Attribute id>>" + end + end + describe Expression::Transformations do before do - @expression = Expression.new(@attribute, "COUNT") + @expression = Count.new(@attribute) end describe '#bind' do it "manufactures an attribute with a rebound relation and self as the ancestor" do derived_relation = @relation.where(@relation[:id].eq(1)) - @expression.bind(derived_relation).should == Expression.new(@attribute.bind(derived_relation), "COUNT", nil, @expression) + @expression.bind(derived_relation).should == Count.new(@attribute.bind(derived_relation), nil, @expression) end it "returns self if the substituting to the same relation" do @@ -25,21 +31,15 @@ module Arel describe '#as' do it "manufactures an aliased expression" do - @expression.as(:alias).should == Expression.new(@attribute, "COUNT", :alias, @expression) + @expression.as(:alias).should == Expression.new(@attribute, :alias, @expression) end end describe '#to_attribute' do it "manufactures an attribute with the expression as an ancestor" do - @expression.to_attribute.should == Attribute.new(@expression.relation, @expression.alias, :ancestor => @expression) + @expression.to_attribute(@relation).should == Attribute.new(@relation, @expression.alias, :ancestor => @expression) end end end - - describe '#to_sql' do - it "manufactures sql with the expression and alias" do - Expression.new(@attribute, "COUNT", :alias).to_sql.should == "COUNT(`users`.`id`) AS `alias`" - end - end end end diff --git a/spec/arel/algebra/unit/primitives/value_spec.rb b/spec/arel/algebra/unit/primitives/value_spec.rb new file mode 100644 index 0000000000..45208e6c5d --- /dev/null +++ b/spec/arel/algebra/unit/primitives/value_spec.rb @@ -0,0 +1,15 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Value do + before do + @relation = Table.new(:users) + end + + describe '#bind' do + it "manufactures a new value whose relation is the provided relation" do + Value.new(1, @relation).bind(another_relation = Table.new(:photos)).should == Value.new(1, another_relation) + end + end + end +end diff --git a/spec/arel/algebra/unit/relations/alias_spec.rb b/spec/arel/algebra/unit/relations/alias_spec.rb new file mode 100644 index 0000000000..a5d716a638 --- /dev/null +++ b/spec/arel/algebra/unit/relations/alias_spec.rb @@ -0,0 +1,16 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Alias do + before do + @relation = Table.new(:users) + end + + describe '==' do + it "obtains if the objects are the same" do + Alias.new(@relation).should_not == Alias.new(@relation) + (aliaz = Alias.new(@relation)).should == aliaz + end + end + end +end diff --git a/spec/arel/algebra/unit/relations/delete_spec.rb b/spec/arel/algebra/unit/relations/delete_spec.rb new file mode 100644 index 0000000000..7578e12a3e --- /dev/null +++ b/spec/arel/algebra/unit/relations/delete_spec.rb @@ -0,0 +1,9 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Deletion do + before do + @relation = Table.new(:users) + end + end +end diff --git a/spec/arel/algebra/unit/relations/group_spec.rb b/spec/arel/algebra/unit/relations/group_spec.rb new file mode 100644 index 0000000000..58f9252356 --- /dev/null +++ b/spec/arel/algebra/unit/relations/group_spec.rb @@ -0,0 +1,10 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Group do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + end +end diff --git a/spec/arel/algebra/unit/relations/insert_spec.rb b/spec/arel/algebra/unit/relations/insert_spec.rb new file mode 100644 index 0000000000..feb1a5eae4 --- /dev/null +++ b/spec/arel/algebra/unit/relations/insert_spec.rb @@ -0,0 +1,9 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Insert do + before do + @relation = Table.new(:users) + end + end +end diff --git a/spec/arel/algebra/unit/relations/join_spec.rb b/spec/arel/algebra/unit/relations/join_spec.rb new file mode 100644 index 0000000000..f5a8bd32aa --- /dev/null +++ b/spec/arel/algebra/unit/relations/join_spec.rb @@ -0,0 +1,26 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Join do + before do + @relation1 = Table.new(:users) + @relation2 = Table.new(:photos) + @predicate = @relation1[:id].eq(@relation2[:user_id]) + end + + describe 'hashing' do + it 'implements hash equality' do + InnerJoin.new(@relation1, @relation2, @predicate) \ + .should hash_the_same_as(InnerJoin.new(@relation1, @relation2, @predicate)) + end + end + + describe '#attributes' do + it 'combines the attributes of the two relations' do + join = InnerJoin.new(@relation1, @relation2, @predicate) + join.attributes.should == + (@relation1.attributes + @relation2.attributes).collect { |a| a.bind(join) } + end + end + end +end diff --git a/spec/arel/algebra/unit/relations/order_spec.rb b/spec/arel/algebra/unit/relations/order_spec.rb new file mode 100644 index 0000000000..8b3c932fb9 --- /dev/null +++ b/spec/arel/algebra/unit/relations/order_spec.rb @@ -0,0 +1,21 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Order do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + + describe "#==" do + it "returns true when the Orders are for the same attribute and direction" do + Ascending.new(@attribute).should == Ascending.new(@attribute) + end + + it "returns false when the Orders are for a diferent direction" do + Ascending.new(@attribute).should_not == Descending.new(@attribute) + end + end + end +end + diff --git a/spec/arel/algebra/unit/relations/project_spec.rb b/spec/arel/algebra/unit/relations/project_spec.rb new file mode 100644 index 0000000000..9f4358ea54 --- /dev/null +++ b/spec/arel/algebra/unit/relations/project_spec.rb @@ -0,0 +1,34 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Project do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + + describe '#attributes' do + before do + @projection = Project.new(@relation, @attribute) + end + + it "manufactures attributes associated with the projection relation" do + @projection.attributes.should == [@attribute].collect { |a| a.bind(@projection) } + end + end + + describe '#externalizable?' do + describe 'when the projections are attributes' do + it 'returns false' do + Project.new(@relation, @attribute).should_not be_externalizable + end + end + + describe 'when the projections include an aggregation' do + it "obtains" do + Project.new(@relation, @attribute.sum).should be_externalizable + end + end + end + end +end diff --git a/spec/arel/unit/relations/relation_spec.rb b/spec/arel/algebra/unit/relations/relation_spec.rb index a3bfa67353..adf82847ac 100644 --- a/spec/arel/unit/relations/relation_spec.rb +++ b/spec/arel/algebra/unit/relations/relation_spec.rb @@ -1,4 +1,4 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') module Arel describe Relation do @@ -34,13 +34,13 @@ module Arel describe 'when given a relation' do it "manufactures an inner join operation between those two relations" do @relation.join(@relation).on(@predicate). \ - should == Join.new("INNER JOIN", @relation, @relation, @predicate) + should == InnerJoin.new(@relation, @relation, @predicate) end end describe "when given a string" do it "manufactures a join operation with the string passed through" do - @relation.join(arbitrary_string = "ASDF").should == Join.new(arbitrary_string, @relation) + @relation.join(arbitrary_string = "ASDF").should == StringJoin.new(@relation, arbitrary_string) end end @@ -54,7 +54,7 @@ module Arel describe '#outer_join' do it "manufactures a left outer join operation between those two relations" do @relation.outer_join(@relation).on(@predicate). \ - should == Join.new("LEFT OUTER JOIN", @relation, @relation, @predicate) + should == OuterJoin.new(@relation, @relation, @predicate) end end end @@ -147,12 +147,31 @@ module Arel end describe Relation::Operable::Writable do + describe '#delete' do + it 'manufactures a deletion relation' do + Session.start do + mock(Session.new).delete(Deletion.new(@relation)) + @relation.delete + end + end + end + describe '#insert' do it 'manufactures an insertion relation' do Session.start do - record = {@relation[:name] => 'carl'} + record = { @relation[:name] => 'carl' } mock(Session.new).create(Insert.new(@relation, record)) - @relation.insert(record).should == @relation + @relation.insert(record) + end + end + end + + describe '#update' do + it 'manufactures an update relation' do + Session.start do + assignments = { @relation[:name] => Value.new('bob', @relation) } + mock(Session.new).update(Update.new(@relation, assignments)) + @relation.update(assignments) end end end @@ -161,15 +180,8 @@ module Arel describe Relation::Enumerable do it "implements enumerable" do - @relation.collect.should == @relation.session.read(@relation) - @relation.first.should == @relation.session.read(@relation).first - end - end - - describe '#call' do - it 'executes a select_all on the connection' do - mock(connection = Object.new).execute(@relation.to_sql) { [] } - @relation.call(connection) + @relation.collect.should == @relation.session.read(@relation).collect + @relation.first.should == @relation.session.read(@relation).first end end end diff --git a/spec/arel/algebra/unit/relations/skip_spec.rb b/spec/arel/algebra/unit/relations/skip_spec.rb new file mode 100644 index 0000000000..a41913436e --- /dev/null +++ b/spec/arel/algebra/unit/relations/skip_spec.rb @@ -0,0 +1,10 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Skip do + before do + @relation = Table.new(:users) + @skipped = 4 + end + end +end diff --git a/spec/arel/algebra/unit/relations/table_spec.rb b/spec/arel/algebra/unit/relations/table_spec.rb new file mode 100644 index 0000000000..dfe457043c --- /dev/null +++ b/spec/arel/algebra/unit/relations/table_spec.rb @@ -0,0 +1,39 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Table do + before do + @relation = Table.new(:users) + end + + describe '[]' do + describe 'when given a', Symbol do + it "manufactures an attribute if the symbol names an attribute within the relation" do + @relation[:id].should == Attribute.new(@relation, :id) + @relation[:does_not_exist].should be_nil + end + end + + describe 'when given an', Attribute do + it "returns the attribute if the attribute is within the relation" do + @relation[@relation[:id]].should == @relation[:id] + end + + it "returns nil if the attribtue is not within the relation" do + another_relation = Table.new(:photos) + @relation[another_relation[:id]].should be_nil + end + end + + describe 'when given an', Expression do + before do + @expression = @relation[:id].count + end + + it "returns the Expression if the Expression is within the relation" do + @relation[@expression].should be_nil + end + end + end + end +end diff --git a/spec/arel/algebra/unit/relations/take_spec.rb b/spec/arel/algebra/unit/relations/take_spec.rb new file mode 100644 index 0000000000..2bc17db5a1 --- /dev/null +++ b/spec/arel/algebra/unit/relations/take_spec.rb @@ -0,0 +1,10 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Take do + before do + @relation = Table.new(:users) + @taken = 4 + end + end +end diff --git a/spec/arel/algebra/unit/relations/update_spec.rb b/spec/arel/algebra/unit/relations/update_spec.rb new file mode 100644 index 0000000000..e9642ffc99 --- /dev/null +++ b/spec/arel/algebra/unit/relations/update_spec.rb @@ -0,0 +1,9 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Update do + before do + @relation = Table.new(:users) + end + end +end diff --git a/spec/arel/algebra/unit/relations/where_spec.rb b/spec/arel/algebra/unit/relations/where_spec.rb new file mode 100644 index 0000000000..6c3074a3a5 --- /dev/null +++ b/spec/arel/algebra/unit/relations/where_spec.rb @@ -0,0 +1,18 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Where do + before do + @relation = Table.new(:users) + @predicate = @relation[:id].eq(1) + end + + describe '#initialize' do + it "manufactures nested where relations if multiple predicates are provided" do + another_predicate = @relation[:name].lt(2) + Where.new(@relation, @predicate, another_predicate). \ + should == Where.new(Where.new(@relation, another_predicate), @predicate) + end + end + end +end diff --git a/spec/arel/unit/session/session_spec.rb b/spec/arel/algebra/unit/session/session_spec.rb index 6e73d74f2d..ca0a43f278 100644 --- a/spec/arel/unit/session/session_spec.rb +++ b/spec/arel/algebra/unit/session/session_spec.rb @@ -1,4 +1,4 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') module Arel describe Session do @@ -40,19 +40,19 @@ module Arel describe '#create' do it "executes an insertion on the connection" do - mock(@insert).call(@insert.engine) + mock(@insert).call @session.create(@insert) end end describe '#read' do it "executes an selection on the connection" do - mock(@read).call(@read.engine) + mock(@read).call @session.read(@read) end it "is memoized" do - mock(@read).call(@read.engine).once + mock(@read).call.once @session.read(@read) @session.read(@read) end @@ -60,14 +60,14 @@ module Arel describe '#update' do it "executes an update on the connection" do - mock(@update).call(@update.engine) + mock(@update).call @session.update(@update) end end describe '#delete' do it "executes a delete on the connection" do - mock(@delete).call(@delete.engine) + mock(@delete).call @session.delete(@delete) end end diff --git a/spec/arel/engines/memory/integration/joins/cross_engine_spec.rb b/spec/arel/engines/memory/integration/joins/cross_engine_spec.rb new file mode 100644 index 0000000000..bffecc9182 --- /dev/null +++ b/spec/arel/engines/memory/integration/joins/cross_engine_spec.rb @@ -0,0 +1,48 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Join do + before do + @users = Array.new([ + [1, 'bryan' ], + [2, 'emilio' ], + [3, 'nick'] + ], [:id, :name]) + @photos = Table.new(:photos) + @photos.delete + @photos \ + .insert(@photos[:id] => 1, @photos[:user_id] => 1, @photos[:camera_id] => 6) \ + .insert(@photos[:id] => 2, @photos[:user_id] => 2, @photos[:camera_id] => 42) + end + + describe 'when the in memory relation is on the left' do + it 'joins across engines' do + @users \ + .join(@photos) \ + .on(@users[:id].eq(@photos[:user_id])) \ + .project(@users[:name], @photos[:camera_id]) \ + .let do |relation| + relation.call.should == [ + Row.new(relation, ['bryan', '6']), + Row.new(relation, ['emilio', '42']) + ] + end + end + end + + describe 'when the in memory relation is on the right' do + it 'joins across engines' do + @photos \ + .join(@users) \ + .on(@users[:id].eq(@photos[:user_id])) \ + .project(@users[:name], @photos[:camera_id]) \ + .let do |relation| + relation.call.should == [ + Row.new(relation, ['bryan', '6']), + Row.new(relation, ['emilio', '42']) + ] + end + end + end + end +end diff --git a/spec/arel/engines/memory/unit/relations/array_spec.rb b/spec/arel/engines/memory/unit/relations/array_spec.rb new file mode 100644 index 0000000000..dd9da41569 --- /dev/null +++ b/spec/arel/engines/memory/unit/relations/array_spec.rb @@ -0,0 +1,32 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Array do + before do + @relation = Array.new([ + [1, 'duck' ], + [2, 'duck' ], + [3, 'goose'] + ], [:id, :name]) + end + + describe '#attributes' do + it 'manufactures attributes corresponding to the names given on construction' do + @relation.attributes.should == [ + Attribute.new(@relation, :id), + Attribute.new(@relation, :name) + ] + end + end + + describe '#call' do + it "manufactures an array of hashes of attributes to values" do + @relation.call.should == [ + Row.new(@relation, [1, 'duck']), + Row.new(@relation, [2, 'duck']), + Row.new(@relation, [3, 'goose']) + ] + end + end + end +end diff --git a/spec/arel/engines/memory/unit/relations/insert_spec.rb b/spec/arel/engines/memory/unit/relations/insert_spec.rb new file mode 100644 index 0000000000..59e43328a3 --- /dev/null +++ b/spec/arel/engines/memory/unit/relations/insert_spec.rb @@ -0,0 +1,28 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Insert do + before do + @relation = Array.new([ + [1, 'duck' ], + [2, 'duck' ], + [3, 'goose'] + ], [:id, :name]) + end + + describe '#call' do + it "manufactures an array of hashes of attributes to values" do + @relation \ + .insert(@relation[:id] => 4, @relation[:name] => 'guinea fowl') \ + .let do |relation| + relation.call.should == [ + Row.new(relation, [1, 'duck']), + Row.new(relation, [2, 'duck']), + Row.new(relation, [3, 'goose']), + Row.new(relation, [4, 'guinea fowl']) + ] + end + end + end + end +end diff --git a/spec/arel/engines/memory/unit/relations/join_spec.rb b/spec/arel/engines/memory/unit/relations/join_spec.rb new file mode 100644 index 0000000000..110fdb03b7 --- /dev/null +++ b/spec/arel/engines/memory/unit/relations/join_spec.rb @@ -0,0 +1,31 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Join do + before do + @relation1 = Array.new([ + [1, 'duck' ], + [2, 'duck' ], + [3, 'goose'] + ], [:id, :name]) + @relation2 = @relation1.alias + end + + describe InnerJoin do + describe '#call' do + it 'combines the two tables where the predicate obtains' do + @relation1 \ + .join(@relation2) \ + .on(@relation1[:id].eq(@relation2[:id])) \ + .let do |relation| + relation.call.should == [ + Row.new(relation, [1, 'duck', 1, 'duck' ]), + Row.new(relation, [2, 'duck', 2, 'duck' ]), + Row.new(relation, [3, 'goose', 3, 'goose']) + ] + end + end + end + end + end +end diff --git a/spec/arel/engines/memory/unit/relations/order_spec.rb b/spec/arel/engines/memory/unit/relations/order_spec.rb new file mode 100644 index 0000000000..1e9690bbbf --- /dev/null +++ b/spec/arel/engines/memory/unit/relations/order_spec.rb @@ -0,0 +1,27 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Order do + before do + @relation = Array.new([ + [1, 'duck' ], + [2, 'duck' ], + [3, 'goose'] + ], [:id, :name]) + end + + describe '#call' do + it 'sorts the relation with the provided ordering' do + @relation \ + .order(@relation[:id].desc) \ + .let do |relation| + relation.call.should == [ + Row.new(relation, [3, 'goose']), + Row.new(relation, [2, 'duck' ]), + Row.new(relation, [1, 'duck' ]) + ] + end + end + end + end +end diff --git a/spec/arel/engines/memory/unit/relations/project_spec.rb b/spec/arel/engines/memory/unit/relations/project_spec.rb new file mode 100644 index 0000000000..1690910026 --- /dev/null +++ b/spec/arel/engines/memory/unit/relations/project_spec.rb @@ -0,0 +1,27 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Project do + before do + @relation = Array.new([ + [1, 'duck' ], + [2, 'duck' ], + [3, 'goose'] + ], [:id, :name]) + end + + describe '#call' do + it 'retains only the attributes that are provided' do + @relation \ + .project(@relation[:id]) \ + .let do |relation| + relation.call.should == [ + Row.new(relation, [1]), + Row.new(relation, [2]), + Row.new(relation, [3]) + ] + end + end + end + end +end diff --git a/spec/arel/engines/memory/unit/relations/skip_spec.rb b/spec/arel/engines/memory/unit/relations/skip_spec.rb new file mode 100644 index 0000000000..3411c5493b --- /dev/null +++ b/spec/arel/engines/memory/unit/relations/skip_spec.rb @@ -0,0 +1,26 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Skip do + before do + @relation = Array.new([ + [1, 'duck' ], + [2, 'duck' ], + [3, 'goose'] + ], [:id, :name]) + end + + describe '#call' do + it 'removes the first n rows' do + @relation \ + .skip(1) \ + .let do |relation| + relation.call.should == [ + Row.new(relation, [2, 'duck']), + Row.new(relation, [3, 'goose']), + ] + end + end + end + end +end diff --git a/spec/arel/engines/memory/unit/relations/take_spec.rb b/spec/arel/engines/memory/unit/relations/take_spec.rb new file mode 100644 index 0000000000..5e7c4fb462 --- /dev/null +++ b/spec/arel/engines/memory/unit/relations/take_spec.rb @@ -0,0 +1,26 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Take do + before do + @relation = Array.new([ + [1, 'duck' ], + [2, 'duck' ], + [3, 'goose'] + ], [:id, :name]) + end + + describe '#call' do + it 'removes the rows after the first n' do + @relation \ + .take(2) \ + .let do |relation| + relation.call.should == [ + Row.new(relation, [1, 'duck']), + Row.new(relation, [2, 'duck']), + ] + end + end + end + end +end diff --git a/spec/arel/engines/memory/unit/relations/where_spec.rb b/spec/arel/engines/memory/unit/relations/where_spec.rb new file mode 100644 index 0000000000..1d2c2eb39c --- /dev/null +++ b/spec/arel/engines/memory/unit/relations/where_spec.rb @@ -0,0 +1,39 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Where do + before do + @relation = Array.new([ + [1, 'duck' ], + [2, 'duck' ], + [3, 'goose'] + ], [:id, :name]) + end + + describe '#call' do + it 'filters the relation with the provided predicate' do + @relation \ + .where(@relation[:id].lt(3)) \ + .let do |relation| + relation.call.should == [ + Row.new(relation, [1, 'duck']), + Row.new(relation, [2, 'duck']), + ] + end + end + + describe 'when filtering a where relation' do + it 'further filters the already-filtered relation with the provided predicate' do + @relation \ + .where(@relation[:id].gt(1)) \ + .where(@relation[:id].lt(3)) \ + .let do |relation| + relation.call.should == [ + Row.new(relation, [2, 'duck']) + ] + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/integration/joins/with_adjacency_spec.rb b/spec/arel/engines/sql/integration/joins/with_adjacency_spec.rb new file mode 100644 index 0000000000..50b0908441 --- /dev/null +++ b/spec/arel/engines/sql/integration/joins/with_adjacency_spec.rb @@ -0,0 +1,209 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Join do + before do + @relation1 = Table(:users) + @relation2 = @relation1.alias + @predicate = @relation1[:id].eq(@relation2[:id]) + end + + describe 'when joining a relation to itself' do + describe '#to_sql' do + it 'manufactures sql aliasing the table and attributes properly in the join predicate and the where clause' do + sql = @relation1.join(@relation2).on(@predicate).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name` + FROM `users` + INNER JOIN `users` AS `users_2` + ON `users`.`id` = `users_2`.`id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "users_2"."id", "users_2"."name" + FROM "users" + INNER JOIN "users" AS "users_2" + ON "users"."id" = "users_2"."id" + }) + end + end + + describe 'when joining with a where on the same relation' do + it 'manufactures sql aliasing the tables properly' do + sql = @relation1 \ + .join(@relation2.where(@relation2[:id].eq(1))) \ + .on(@predicate) \ + .to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name` + FROM `users` + INNER JOIN `users` AS `users_2` + ON `users`.`id` = `users_2`.`id` AND `users_2`.`id` = 1 + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "users_2"."id", "users_2"."name" + FROM "users" + INNER JOIN "users" AS "users_2" + ON "users"."id" = "users_2"."id" AND "users_2"."id" = 1 + }) + end + end + + describe 'when the where occurs before the alias' do + it 'manufactures sql aliasing the predicates properly' do + relation2 = @relation1.where(@relation1[:id].eq(1)).alias + + sql = @relation1 \ + .join(relation2) \ + .on(relation2[:id].eq(@relation1[:id])) \ + .to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name` + FROM `users` + INNER JOIN `users` AS `users_2` + ON `users_2`.`id` = `users`.`id` AND `users_2`.`id` = 1 + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "users_2"."id", "users_2"."name" + FROM "users" + INNER JOIN "users" AS "users_2" + ON "users_2"."id" = "users"."id" AND "users_2"."id" = 1 + }) + end + end + end + end + + describe 'when joining the relation to itself multiple times' do + before do + @relation3 = @relation1.alias + end + + describe 'when joining left-associatively' do + it 'manufactures sql aliasing the tables properly' do + sql = @relation1 \ + .join(@relation2 \ + .join(@relation3) \ + .on(@relation2[:id].eq(@relation3[:id]))) \ + .on(@relation1[:id].eq(@relation2[:id])) \ + .to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name`, `users_3`.`id`, `users_3`.`name` + FROM `users` + INNER JOIN `users` AS `users_2` + ON `users`.`id` = `users_2`.`id` + INNER JOIN `users` AS `users_3` + ON `users_2`.`id` = `users_3`.`id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "users_2"."id", "users_2"."name", "users_3"."id", "users_3"."name" + FROM "users" + INNER JOIN "users" AS "users_2" + ON "users"."id" = "users_2"."id" + INNER JOIN "users" AS "users_3" + ON "users_2"."id" = "users_3"."id" + }) + end + end + end + + describe 'when joining right-associatively' do + it 'manufactures sql aliasing the tables properly' do + sql = @relation1 \ + .join(@relation2).on(@relation1[:id].eq(@relation2[:id])) \ + .join(@relation3).on(@relation2[:id].eq(@relation3[:id])) \ + .to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name`, `users_3`.`id`, `users_3`.`name` + FROM `users` + INNER JOIN `users` AS `users_2` + ON `users`.`id` = `users_2`.`id` + INNER JOIN `users` AS `users_3` + ON `users_2`.`id` = `users_3`.`id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "users_2"."id", "users_2"."name", "users_3"."id", "users_3"."name" + FROM "users" + INNER JOIN "users" AS "users_2" + ON "users"."id" = "users_2"."id" + INNER JOIN "users" AS "users_3" + ON "users_2"."id" = "users_3"."id" + }) + end + end + end + end + end + + describe '[]' do + describe 'when given an attribute belonging to both sub-relations' do + it 'disambiguates the relation that serves as the ancestor to the attribute' do + @relation1 \ + .join(@relation2) \ + .on(@predicate) \ + .should disambiguate_attributes(@relation1[:id], @relation2[:id]) + end + + describe 'when both relations are compound and only one is an alias' do + it 'disambiguates the relation that serves as the ancestor to the attribute' do + compound1 = @relation1.where(@predicate) + compound2 = compound1.alias + compound1 \ + .join(compound2) \ + .on(@predicate) \ + .should disambiguate_attributes(compound1[:id], compound2[:id]) + end + end + + describe 'when the left relation is extremely compound' do + it 'disambiguates the relation that serves as the ancestor to the attribute' do + @relation1 \ + .where(@predicate) \ + .where(@predicate) \ + .join(@relation2) \ + .on(@predicate) \ + .should disambiguate_attributes(@relation1[:id], @relation2[:id]) + end + end + + describe 'when the right relation is extremely compound' do + it 'disambiguates the relation that serves as the ancestor to the attribute' do + @relation1 \ + .join( \ + @relation2 \ + .where(@predicate) \ + .where(@predicate) \ + .where(@predicate)) \ + .on(@predicate) \ + .should disambiguate_attributes(@relation1[:id], @relation2[:id]) + end + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/integration/joins/with_aggregations_spec.rb b/spec/arel/engines/sql/integration/joins/with_aggregations_spec.rb new file mode 100644 index 0000000000..709ae9f8d1 --- /dev/null +++ b/spec/arel/engines/sql/integration/joins/with_aggregations_spec.rb @@ -0,0 +1,167 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Join do + before do + @relation1 = Table(:users) + @relation2 = Table(:photos) + @predicate = @relation1[:id].eq(@relation2[:user_id]) + end + + describe 'when joining aggregated relations' do + before do + @aggregation = @relation2 \ + .group(@relation2[:user_id]) \ + .project(@relation2[:user_id], @relation2[:id].count.as(:cnt)) \ + end + + describe '#to_sql' do + # CLEANUP + it '' do + sql = @relation1.join(@relation2.take(3)).on(@predicate).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `photos_external`.`id`, `photos_external`.`user_id`, `photos_external`.`camera_id` + FROM `users` + INNER JOIN (SELECT `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` FROM `photos` LIMIT 3) AS `photos_external` + ON `users`.`id` = `photos_external`.`user_id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "photos_external"."id", "photos_external"."user_id", "photos_external"."camera_id" + FROM "users" + INNER JOIN (SELECT "photos"."id", "photos"."user_id", "photos"."camera_id" FROM "photos" LIMIT 3) AS "photos_external" + ON "users"."id" = "photos_external"."user_id" + }) + end + end + + describe 'with the aggregation on the right' do + it 'manufactures sql joining the left table to a derived table' do + sql = @relation1.join(@aggregation).on(@predicate).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `photos_external`.`user_id`, `photos_external`.`cnt` + FROM `users` + INNER JOIN (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` GROUP BY `photos`.`user_id`) AS `photos_external` + ON `users`.`id` = `photos_external`.`user_id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "photos_external"."user_id", "photos_external"."cnt" + FROM "users" + INNER JOIN (SELECT "photos"."user_id", COUNT("photos"."id") AS "cnt" FROM "photos" GROUP BY "photos"."user_id") AS "photos_external" + ON "users"."id" = "photos_external"."user_id" + }) + end + end + end + + describe 'with the aggregation on the left' do + it 'manufactures sql joining the right table to a derived table' do + sql = @aggregation.join(@relation1).on(@predicate).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `photos_external`.`user_id`, `photos_external`.`cnt`, `users`.`id`, `users`.`name` + FROM (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` GROUP BY `photos`.`user_id`) AS `photos_external` + INNER JOIN `users` + ON `users`.`id` = `photos_external`.`user_id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "photos_external"."user_id", "photos_external"."cnt", "users"."id", "users"."name" + FROM (SELECT "photos"."user_id", COUNT("photos"."id") AS "cnt" FROM "photos" GROUP BY "photos"."user_id") AS "photos_external" + INNER JOIN "users" + ON "users"."id" = "photos_external"."user_id" + }) + end + end + end + + describe 'with the aggregation on both sides' do + it 'it properly aliases the aggregations' do + aggregation2 = @aggregation.alias + sql = @aggregation.join(aggregation2).on(aggregation2[:user_id].eq(@aggregation[:user_id])).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `photos_external`.`user_id`, `photos_external`.`cnt`, `photos_external_2`.`user_id`, `photos_external_2`.`cnt` + FROM (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` GROUP BY `photos`.`user_id`) AS `photos_external` + INNER JOIN (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` GROUP BY `photos`.`user_id`) AS `photos_external_2` + ON `photos_external_2`.`user_id` = `photos_external`.`user_id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "photos_external"."user_id", "photos_external"."cnt", "photos_external_2"."user_id", "photos_external_2"."cnt" + FROM (SELECT "photos"."user_id", COUNT("photos"."id") AS "cnt" FROM "photos" GROUP BY "photos"."user_id") AS "photos_external" + INNER JOIN (SELECT "photos"."user_id", COUNT("photos"."id") AS "cnt" FROM "photos" GROUP BY "photos"."user_id") AS "photos_external_2" + ON "photos_external_2"."user_id" = "photos_external"."user_id" + }) + end + end + end + + describe 'when the aggration has a where' do + describe 'with the aggregation on the left' do + it "manufactures sql keeping wheres on the aggregation within the derived table" do + sql = @relation1.join(@aggregation.where(@aggregation[:user_id].eq(1))).on(@predicate).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `photos_external`.`user_id`, `photos_external`.`cnt` + FROM `users` + INNER JOIN (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` WHERE `photos`.`user_id` = 1 GROUP BY `photos`.`user_id`) AS `photos_external` + ON `users`.`id` = `photos_external`.`user_id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "photos_external"."user_id", "photos_external"."cnt" + FROM "users" + INNER JOIN (SELECT "photos"."user_id", COUNT("photos"."id") AS "cnt" FROM "photos" WHERE "photos"."user_id" = 1 GROUP BY "photos"."user_id") AS "photos_external" + ON "users"."id" = "photos_external"."user_id" + }) + end + end + end + + describe 'with the aggregation on the right' do + it "manufactures sql keeping wheres on the aggregation within the derived table" do + sql = @aggregation.where(@aggregation[:user_id].eq(1)).join(@relation1).on(@predicate).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `photos_external`.`user_id`, `photos_external`.`cnt`, `users`.`id`, `users`.`name` + FROM (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` WHERE `photos`.`user_id` = 1 GROUP BY `photos`.`user_id`) AS `photos_external` + INNER JOIN `users` + ON `users`.`id` = `photos_external`.`user_id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "photos_external"."user_id", "photos_external"."cnt", "users"."id", "users"."name" + FROM (SELECT "photos"."user_id", COUNT("photos"."id") AS "cnt" FROM "photos" WHERE "photos"."user_id" = 1 GROUP BY "photos"."user_id") AS "photos_external" + INNER JOIN "users" + ON "users"."id" = "photos_external"."user_id" + }) + end + end + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/integration/joins/with_compounds_spec.rb b/spec/arel/engines/sql/integration/joins/with_compounds_spec.rb new file mode 100644 index 0000000000..4bceef4975 --- /dev/null +++ b/spec/arel/engines/sql/integration/joins/with_compounds_spec.rb @@ -0,0 +1,107 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Join do + before do + @relation1 = Table(:users) + @relation2 = Table(:photos) + @predicate = @relation1[:id].eq(@relation2[:user_id]) + end + + describe '#to_sql' do + describe 'when the join contains a where' do + describe 'and the where is given a string' do + it 'does not escape the string' do + sql = @relation1 \ + .join(@relation2.where("asdf")) \ + .on(@predicate) \ + .to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` + FROM `users` + INNER JOIN `photos` + ON `users`.`id` = `photos`.`user_id` AND asdf + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "photos"."id", "photos"."user_id", "photos"."camera_id" + FROM "users" + INNER JOIN "photos" + ON "users"."id" = "photos"."user_id" AND asdf + }) + end + end + end + end + + describe 'when a compound contains a join' do + describe 'and the compound is a where' do + it 'manufactures sql disambiguating the tables' do + sql = @relation1 \ + .where(@relation1[:id].eq(1)) \ + .join(@relation2) \ + .on(@predicate) \ + .where(@relation1[:id].eq(1)) \ + .to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` + FROM `users` + INNER JOIN `photos` + ON `users`.`id` = `photos`.`user_id` + WHERE `users`.`id` = 1 + AND `users`.`id` = 1 + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "photos"."id", "photos"."user_id", "photos"."camera_id" + FROM "users" + INNER JOIN "photos" + ON "users"."id" = "photos"."user_id" + WHERE "users"."id" = 1 + AND "users"."id" = 1 + }) + end + end + end + + describe 'and the compound is a group' do + it 'manufactures sql disambiguating the tables' do + sql = @relation1 \ + .join(@relation2) \ + .on(@predicate) \ + .group(@relation1[:id]) \ + .to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` + FROM `users` + INNER JOIN `photos` + ON `users`.`id` = `photos`.`user_id` + GROUP BY `users`.`id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "photos"."id", "photos"."user_id", "photos"."camera_id" + FROM "users" + INNER JOIN "photos" + ON "users"."id" = "photos"."user_id" + GROUP BY "users"."id" + }) + end + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/engine_spec.rb b/spec/arel/engines/sql/unit/engine_spec.rb new file mode 100644 index 0000000000..c607abcfa1 --- /dev/null +++ b/spec/arel/engines/sql/unit/engine_spec.rb @@ -0,0 +1,45 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Sql::Engine do + before do + @users = Table.new(:users) + @users.delete + end + + describe "CRUD" do + describe "#create" do + it "inserts into the relation" do + @users.insert @users[:name] => "Bryan" + @users.first[@users[:name]].should == "Bryan" + end + end + + describe "#read" do + it "reads from the relation" do + @users.insert @users[:name] => "Bryan" + + @users.each do |row| + row[@users[:name]].should == "Bryan" + end + end + end + + describe "#update" do + it "updates the relation" do + @users.insert @users[:name] => "Nick" + @users.update @users[:name] => "Bryan" + @users.first[@users[:name]].should == "Bryan" + end + end + + describe "#delete" do + it "deletes from the relation" do + @users.insert @users[:name] => "Bryan" + @users.delete + @users.first.should == nil + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/predicates/binary_spec.rb b/spec/arel/engines/sql/unit/predicates/binary_spec.rb new file mode 100644 index 0000000000..b1400e2588 --- /dev/null +++ b/spec/arel/engines/sql/unit/predicates/binary_spec.rb @@ -0,0 +1,117 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Binary do + before do + @relation = Table.new(:users) + @attribute1 = @relation[:id] + @attribute2 = @relation[:name] + class ConcreteBinary < Binary + def predicate_sql + "<=>" + end + end + end + + describe "with compound predicates" do + before do + @operand1 = ConcreteBinary.new(@attribute1, 1) + @operand2 = ConcreteBinary.new(@attribute2, "name") + end + + describe Or do + describe "#to_sql" do + it "manufactures sql with an OR operation" do + sql = Or.new(@operand1, @operand2).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{(`users`.`id` <=> 1 OR `users`.`name` <=> 'name')}) + end + + adapter_is :postgresql do + sql.should be_like(%Q{("users"."id" <=> 1 OR "users"."name" <=> E'name')}) + end + + adapter_is :sqlite3 do + sql.should be_like(%Q{("users"."id" <=> 1 OR "users"."name" <=> 'name')}) + end + end + end + end + + describe And do + describe "#to_sql" do + it "manufactures sql with an AND operation" do + sql = And.new(@operand1, @operand2).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{(`users`.`id` <=> 1 AND `users`.`name` <=> 'name')}) + end + + adapter_is :sqlite3 do + sql.should be_like(%Q{("users"."id" <=> 1 AND "users"."name" <=> 'name')}) + end + + adapter_is :postgresql do + sql.should be_like(%Q{("users"."id" <=> 1 AND "users"."name" <=> E'name')}) + end + end + end + end + end + + describe '#to_sql' do + describe 'when relating two attributes' do + it 'manufactures sql with a binary operation' do + sql = ConcreteBinary.new(@attribute1, @attribute2).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{`users`.`id` <=> `users`.`name`}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{"users"."id" <=> "users"."name"}) + end + end + end + + describe 'when relating an attribute and a value' do + before do + @value = "1-asdf" + end + + describe 'when relating to an integer attribute' do + it 'formats values as integers' do + sql = ConcreteBinary.new(@attribute1, @value).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{`users`.`id` <=> 1}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{"users"."id" <=> 1}) + end + end + end + + describe 'when relating to a string attribute' do + it 'formats values as strings' do + sql = ConcreteBinary.new(@attribute2, @value).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{`users`.`name` <=> '1-asdf'}) + end + + adapter_is :sqlite3 do + sql.should be_like(%Q{"users"."name" <=> '1-asdf'}) + end + + adapter_is :postgresql do + sql.should be_like(%Q{"users"."name" <=> E'1-asdf'}) + end + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/predicates/equality_spec.rb b/spec/arel/engines/sql/unit/predicates/equality_spec.rb new file mode 100644 index 0000000000..688a6a20be --- /dev/null +++ b/spec/arel/engines/sql/unit/predicates/equality_spec.rb @@ -0,0 +1,46 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Equality do + before do + @relation1 = Table.new(:users) + @relation2 = Table.new(:photos) + @attribute1 = @relation1[:id] + @attribute2 = @relation2[:user_id] + end + + describe '#to_sql' do + describe 'when relating to a non-nil value' do + it "manufactures an equality predicate" do + sql = Equality.new(@attribute1, @attribute2).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{`users`.`id` = `photos`.`user_id`}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{"users"."id" = "photos"."user_id"}) + end + end + end + + describe 'when relation to a nil value' do + before do + @nil = nil + end + + it "manufactures an is null predicate" do + sql = Equality.new(@attribute1, @nil).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{`users`.`id` IS NULL}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{"users"."id" IS NULL}) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/predicates/in_spec.rb b/spec/arel/engines/sql/unit/predicates/in_spec.rb new file mode 100644 index 0000000000..d3e75cfb84 --- /dev/null +++ b/spec/arel/engines/sql/unit/predicates/in_spec.rb @@ -0,0 +1,86 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe In do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + + describe '#to_sql' do + describe 'when relating to an array' do + describe 'when the array\'s elements are the same type as the attribute' do + before do + @array = [1, 2, 3] + end + + it 'manufactures sql with a comma separated list' do + sql = In.new(@attribute, @array).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{`users`.`id` IN (1, 2, 3)}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{"users"."id" IN (1, 2, 3)}) + end + end + end + + describe 'when the array\'s elements are not same type as the attribute' do + before do + @array = ['1-asdf', 2, 3] + end + + it 'formats values in the array as the type of the attribute' do + sql = In.new(@attribute, @array).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{`users`.`id` IN (1, 2, 3)}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{"users"."id" IN (1, 2, 3)}) + end + end + end + end + + describe 'when relating to a range' do + before do + @range = 1..2 + end + + it 'manufactures sql with a between' do + sql = In.new(@attribute, @range).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{`users`.`id` BETWEEN 1 AND 2}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{"users"."id" BETWEEN 1 AND 2}) + end + end + end + + describe 'when relating to a relation' do + it 'manufactures sql with a subselect' do + sql = In.new(@attribute, @relation).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + `users`.`id` IN (SELECT `users`.`id`, `users`.`name` FROM `users`) + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + "users"."id" IN (SELECT "users"."id", "users"."name" FROM "users") + }) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/predicates/predicates_spec.rb b/spec/arel/engines/sql/unit/predicates/predicates_spec.rb new file mode 100644 index 0000000000..d55e178e43 --- /dev/null +++ b/spec/arel/engines/sql/unit/predicates/predicates_spec.rb @@ -0,0 +1,65 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Predicate do + before do + @relation = Table.new(:users) + @attribute1 = @relation[:id] + @attribute2 = @relation[:name] + @operand1 = Equality.new(@attribute1, 1) + @operand2 = Equality.new(@attribute2, "name") + end + + describe "when being combined with another predicate with AND logic" do + describe "#to_sql" do + it "manufactures sql with an AND operation" do + sql = @operand1.and(@operand2).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + (`users`.`id` = 1 AND `users`.`name` = 'name') + }) + end + + adapter_is :sqlite3 do + sql.should be_like(%Q{ + ("users"."id" = 1 AND "users"."name" = 'name') + }) + end + + adapter_is :postgresql do + sql.should be_like(%Q{ + ("users"."id" = 1 AND "users"."name" = E'name') + }) + end + end + end + end + + describe "when being combined with another predicate with OR logic" do + describe "#to_sql" do + it "manufactures sql with an OR operation" do + sql = @operand1.or(@operand2).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + (`users`.`id` = 1 OR `users`.`name` = 'name') + }) + end + + adapter_is :sqlite3 do + sql.should be_like(%Q{ + ("users"."id" = 1 OR "users"."name" = 'name') + }) + end + + adapter_is :postgresql do + sql.should be_like(%Q{ + ("users"."id" = 1 OR "users"."name" = E'name') + }) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/primitives/attribute_spec.rb b/spec/arel/engines/sql/unit/primitives/attribute_spec.rb new file mode 100644 index 0000000000..6cb72f3c19 --- /dev/null +++ b/spec/arel/engines/sql/unit/primitives/attribute_spec.rb @@ -0,0 +1,32 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Attribute do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + + describe '#column' do + it "returns the corresponding column in the relation" do + @attribute.column.should == @relation.column_for(@attribute) + end + end + + describe '#to_sql' do + describe 'for a simple attribute' do + it "manufactures sql with an alias" do + sql = @attribute.to_sql + + adapter_is :mysql do + sql.should be_like(%Q{`users`.`id`}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{"users"."id"}) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/primitives/expression_spec.rb b/spec/arel/engines/sql/unit/primitives/expression_spec.rb new file mode 100644 index 0000000000..ee7f2c1461 --- /dev/null +++ b/spec/arel/engines/sql/unit/primitives/expression_spec.rb @@ -0,0 +1,24 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Expression do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + + describe '#to_sql' do + it "manufactures sql with the expression and alias" do + sql = Count.new(@attribute, :alias).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{COUNT(`users`.`id`) AS `alias`}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{COUNT("users"."id") AS "alias"}) + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/primitives/literal_spec.rb b/spec/arel/engines/sql/unit/primitives/literal_spec.rb new file mode 100644 index 0000000000..c7ff1cf879 --- /dev/null +++ b/spec/arel/engines/sql/unit/primitives/literal_spec.rb @@ -0,0 +1,23 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe SqlLiteral do + before do + @relation = Table.new(:users) + end + + describe '#to_sql' do + it "manufactures sql with a literal SQL fragment" do + sql = @relation.project(Count.new(SqlLiteral.new("*"))).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{SELECT COUNT(*) AS count_id FROM `users`}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{SELECT COUNT(*) AS count_id FROM "users"}) + end + end + end + end +end diff --git a/spec/arel/unit/primitives/value_spec.rb b/spec/arel/engines/sql/unit/primitives/value_spec.rb index ba9a80bb49..ff3533f6ef 100644 --- a/spec/arel/unit/primitives/value_spec.rb +++ b/spec/arel/engines/sql/unit/primitives/value_spec.rb @@ -1,4 +1,4 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') module Arel describe Value do @@ -9,7 +9,14 @@ module Arel describe '#to_sql' do it "appropriately quotes the value" do Value.new(1, @relation).to_sql.should be_like('1') - Value.new('asdf', @relation).to_sql.should be_like("'asdf'") + + adapter_is_not :postgresql do + Value.new('asdf', @relation).to_sql.should be_like("'asdf'") + end + + adapter_is :postgresql do + Value.new('asdf', @relation).to_sql.should be_like("E'asdf'") + end end end @@ -18,11 +25,5 @@ module Arel Value.new(1, @relation).format(@relation[:id]).should == @relation[:id].to_sql end end - - describe '#bind' do - it "manufactures a new value whose relation is the provided relation" do - Value.new(1, @relation).bind(another_relation = Table.new(:photos)).should == Value.new(1, another_relation) - end - end end end diff --git a/spec/arel/engines/sql/unit/relations/alias_spec.rb b/spec/arel/engines/sql/unit/relations/alias_spec.rb new file mode 100644 index 0000000000..b67a0bbc89 --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/alias_spec.rb @@ -0,0 +1,43 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Alias do + before do + @relation = Table.new(:users) + end + + describe '#to_sql' do + describe 'when there is no ambiguity' do + it 'does not alias table names anywhere a table name can appear' do + sql = @relation \ + .where(@relation[:id].eq(1)) \ + .order(@relation[:id]) \ + .project(@relation[:id]) \ + .group(@relation[:id]) \ + .alias \ + .to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id` + FROM `users` + WHERE `users`.`id` = 1 + GROUP BY `users`.`id` + ORDER BY `users`.`id` ASC + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id" + FROM "users" + WHERE "users"."id" = 1 + GROUP BY "users"."id" + ORDER BY "users"."id" ASC + }) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/delete_spec.rb b/spec/arel/engines/sql/unit/relations/delete_spec.rb new file mode 100644 index 0000000000..7a5e2b0088 --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/delete_spec.rb @@ -0,0 +1,63 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Deletion do + before do + @relation = Table.new(:users) + end + + describe '#to_sql' do + it 'manufactures sql deleting a table relation' do + sql = Deletion.new(@relation).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{DELETE FROM `users`}) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{DELETE FROM "users"}) + end + end + + it 'manufactures sql deleting a where relation' do + sql = Deletion.new(@relation.where(@relation[:id].eq(1))).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + DELETE + FROM `users` + WHERE `users`.`id` = 1 + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + DELETE + FROM "users" + WHERE "users"."id" = 1 + }) + end + end + + it "manufactures sql deleting a ranged relation" do + sql = Deletion.new(@relation.take(1)).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + DELETE + FROM `users` + LIMIT 1 + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + DELETE + FROM "users" + LIMIT 1 + }) + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/group_spec.rb b/spec/arel/engines/sql/unit/relations/group_spec.rb new file mode 100644 index 0000000000..5e0c675c8b --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/group_spec.rb @@ -0,0 +1,56 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Group do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + + describe '#to_sql' do + describe 'when given a predicate' do + it "manufactures sql with where clause conditions" do + sql = Group.new(@relation, @attribute).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + GROUP BY `users`.`id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + GROUP BY "users"."id" + }) + end + end + end + + describe 'when given a string' do + it "passes the string through to the where clause" do + sql = Group.new(@relation, 'asdf').to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + GROUP BY asdf + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + GROUP BY asdf + }) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/insert_spec.rb b/spec/arel/engines/sql/unit/relations/insert_spec.rb new file mode 100644 index 0000000000..29a5e0bf42 --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/insert_spec.rb @@ -0,0 +1,107 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Insert do + before do + @relation = Table.new(:users) + end + + describe '#to_sql' do + it 'manufactures sql inserting data when given multiple rows' do + pending 'it should insert multiple rows' do + @insertion = Insert.new(@relation, [@relation[:name] => "nick", @relation[:name] => "bryan"]) + + @insertion.to_sql.should be_like(" + INSERT + INTO `users` + (`name`) VALUES ('nick'), ('bryan') + ") + end + end + + it 'manufactures sql inserting data when given multiple values' do + @insertion = Insert.new(@relation, @relation[:id] => "1", @relation[:name] => "nick") + + adapter_is :mysql do + @insertion.to_sql.should be_like(%Q{ + INSERT + INTO `users` + (`id`, `name`) VALUES (1, 'nick') + }) + end + + adapter_is :sqlite3 do + @insertion.to_sql.should be_like(%Q{ + INSERT + INTO "users" + ("id", "name") VALUES (1, 'nick') + }) + end + + adapter_is :postgresql do + @insertion.to_sql.should be_like(%Q{ + INSERT + INTO "users" + ("id", "name") VALUES (1, E'nick') + }) + end + end + + describe 'when given values whose types correspond to the types of the attributes' do + before do + @insertion = Insert.new(@relation, @relation[:name] => "nick") + end + + it 'manufactures sql inserting data' do + adapter_is :mysql do + @insertion.to_sql.should be_like(%Q{ + INSERT + INTO `users` + (`name`) VALUES ('nick') + }) + end + + adapter_is :sqlite3 do + @insertion.to_sql.should be_like(%Q{ + INSERT + INTO "users" + ("name") VALUES ('nick') + }) + end + + adapter_is :postgresql do + @insertion.to_sql.should be_like(%Q{ + INSERT + INTO "users" + ("name") VALUES (E'nick') + }) + end + end + end + + describe 'when given values whose types differ from from the types of the attributes' do + before do + @insertion = Insert.new(@relation, @relation[:id] => '1-asdf') + end + + it 'manufactures sql inserting data' do + adapter_is :mysql do + @insertion.to_sql.should be_like(%Q{ + INSERT + INTO `users` + (`id`) VALUES (1) + }) + end + + adapter_is_not :mysql do + @insertion.to_sql.should be_like(%Q{ + INSERT + INTO "users" + ("id") VALUES (1) + }) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/join_spec.rb b/spec/arel/engines/sql/unit/relations/join_spec.rb new file mode 100644 index 0000000000..f904b61870 --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/join_spec.rb @@ -0,0 +1,57 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Join do + before do + @relation1 = Table.new(:users) + @relation2 = Table.new(:photos) + @predicate = @relation1[:id].eq(@relation2[:user_id]) + end + + describe '#to_sql' do + describe 'when joining with another relation' do + it 'manufactures sql joining the two tables on the predicate' do + sql = InnerJoin.new(@relation1, @relation2, @predicate).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` + FROM `users` + INNER JOIN `photos` ON `users`.`id` = `photos`.`user_id` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name", "photos"."id", "photos"."user_id", "photos"."camera_id" + FROM "users" + INNER JOIN "photos" ON "users"."id" = "photos"."user_id" + }) + end + end + end + + describe 'when joining with a string' do + it "passes the string through to the where clause" do + sql = StringJoin.new(@relation1, "INNER JOIN asdf ON fdsa").to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + INNER JOIN asdf ON fdsa + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + INNER JOIN asdf ON fdsa + }) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/order_spec.rb b/spec/arel/engines/sql/unit/relations/order_spec.rb new file mode 100644 index 0000000000..ce97a4dd5e --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/order_spec.rb @@ -0,0 +1,113 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Order do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + + describe '#to_sql' do + describe "when given an attribute" do + it "manufactures sql with an order clause populated by the attribute" do + sql = Order.new(@relation, @attribute).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + ORDER BY `users`.`id` ASC + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + ORDER BY "users"."id" ASC + }) + end + end + end + + describe "when given multiple attributes" do + before do + @another_attribute = @relation[:name] + end + + it "manufactures sql with an order clause populated by comma-separated attributes" do + sql = Order.new(@relation, @attribute, @another_attribute).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + ORDER BY `users`.`id` ASC, `users`.`name` ASC + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + ORDER BY "users"."id" ASC, "users"."name" ASC + }) + end + end + end + + describe "when given a string" do + before do + @string = "asdf" + end + + it "passes the string through to the order clause" do + sql = Order.new(@relation, @string).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + ORDER BY asdf + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + ORDER BY asdf + }) + end + end + end + + describe "when ordering an ordered relation" do + before do + @ordered_relation = Order.new(@relation, @attribute) + @another_attribute = @relation[:name] + end + + it "manufactures sql with the order clause of the last ordering preceding the first ordering" do + sql = Order.new(@ordered_relation, @another_attribute).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + ORDER BY `users`.`name` ASC, `users`.`id` ASC + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + ORDER BY "users"."name" ASC, "users"."id" ASC + }) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/project_spec.rb b/spec/arel/engines/sql/unit/relations/project_spec.rb new file mode 100644 index 0000000000..5e29124cfa --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/project_spec.rb @@ -0,0 +1,110 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Project do + before do + @relation = Table.new(:users) + @attribute = @relation[:id] + end + + describe '#to_sql' do + describe 'when given an attribute' do + it "manufactures sql with a limited select clause" do + sql = Project.new(@relation, @attribute).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id` + FROM `users` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id" + FROM "users" + }) + end + end + end + + describe 'when given a relation' do + before do + @scalar_relation = Project.new(@relation, @relation[:name]) + end + + it "manufactures sql with scalar selects" do + sql = Project.new(@relation, @scalar_relation).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT (SELECT `users`.`name` FROM `users`) AS `users` FROM `users` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT (SELECT "users"."name" FROM "users") AS "users" FROM "users" + }) + end + end + end + + describe 'when given a string' do + it "passes the string through to the select clause" do + sql = Project.new(@relation, 'asdf').to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT asdf FROM `users` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT asdf FROM "users" + }) + end + end + end + + describe 'when given an expression' do + it 'manufactures sql with expressions' do + sql = @relation.project(@attribute.count).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT COUNT(`users`.`id`) AS count_id + FROM `users` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT COUNT("users"."id") AS count_id + FROM "users" + }) + end + end + + it 'manufactures sql with distinct expressions' do + sql = @relation.project(@attribute.count(true)).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT COUNT(DISTINCT `users`.`id`) AS count_id + FROM `users` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT COUNT(DISTINCT "users"."id") AS count_id + FROM "users" + }) + end + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/skip_spec.rb b/spec/arel/engines/sql/unit/relations/skip_spec.rb new file mode 100644 index 0000000000..c14bd1ce95 --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/skip_spec.rb @@ -0,0 +1,32 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Skip do + before do + @relation = Table.new(:users) + @skipped = 4 + end + + describe '#to_sql' do + it "manufactures sql with limit and offset" do + sql = Skip.new(@relation, @skipped).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + OFFSET 4 + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + OFFSET 4 + }) + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/table_spec.rb b/spec/arel/engines/sql/unit/relations/table_spec.rb new file mode 100644 index 0000000000..9797b38822 --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/table_spec.rb @@ -0,0 +1,69 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Table do + before do + @relation = Table.new(:users) + end + + describe '#to_sql' do + it "manufactures a simple select query" do + sql = @relation.to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + }) + end + end + end + + describe '#column_for' do + it "returns the column corresponding to the attribute" do + @relation.column_for(@relation[:id]).should == @relation.columns.detect { |c| c.name == 'id' } + end + end + + describe '#attributes' do + it 'manufactures attributes corresponding to columns in the table' do + @relation.attributes.should == [ + Attribute.new(@relation, :id), + Attribute.new(@relation, :name) + ] + end + + describe '#reset' do + it "reloads columns from the database" do + lambda { stub(@relation.engine).columns { [] } }.should_not change { @relation.attributes } + lambda { @relation.reset }.should change { @relation.attributes } + end + end + end + + describe 'hashing' do + it "implements hash equality" do + Table.new(:users).should hash_the_same_as(Table.new(:users)) + Table.new(:users).should_not hash_the_same_as(Table.new(:photos)) + end + end + + describe '#engine' do + it "defaults to global engine" do + Table.engine = engine = Sql::Engine.new + Table.new(:users).engine.should == engine + end + + it "can be specified" do + Table.new(:users, engine = Sql::Engine.new).engine.should == engine + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/take_spec.rb b/spec/arel/engines/sql/unit/relations/take_spec.rb new file mode 100644 index 0000000000..8f1240fc17 --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/take_spec.rb @@ -0,0 +1,32 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Take do + before do + @relation = Table.new(:users) + @taken = 4 + end + + describe '#to_sql' do + it "manufactures sql with limit and offset" do + sql = Take.new(@relation, @taken).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + LIMIT 4 + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + LIMIT 4 + }) + end + end + end + end +end diff --git a/spec/arel/engines/sql/unit/relations/update_spec.rb b/spec/arel/engines/sql/unit/relations/update_spec.rb new file mode 100644 index 0000000000..4d728eb241 --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/update_spec.rb @@ -0,0 +1,151 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Update do + before do + @relation = Table.new(:users) + end + + describe '#to_sql' do + it "manufactures sql updating attributes when given multiple attributes" do + sql = Update.new(@relation, @relation[:id] => 1, @relation[:name] => "nick").to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + UPDATE `users` + SET `id` = 1, `name` = 'nick' + }) + end + + adapter_is :sqlite3 do + sql.should be_like(%Q{ + UPDATE "users" + SET "id" = 1, "name" = 'nick' + }) + end + + adapter_is :postgresql do + sql.should be_like(%Q{ + UPDATE "users" + SET "id" = 1, "name" = E'nick' + }) + end + end + + it "manufactures sql updating attributes when given a ranged relation" do + sql = Update.new(@relation.take(1), @relation[:name] => "nick").to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + UPDATE `users` + SET `name` = 'nick' + LIMIT 1 + }) + end + + adapter_is :sqlite3 do + sql.should be_like(%Q{ + UPDATE "users" + SET "name" = 'nick' + LIMIT 1 + }) + end + + adapter_is :postgresql do + sql.should be_like(%Q{ + UPDATE "users" + SET "name" = E'nick' + LIMIT 1 + }) + end + end + + describe 'when given values whose types correspond to the types of the attributes' do + before do + @update = Update.new(@relation, @relation[:name] => "nick") + end + + it 'manufactures sql updating attributes' do + adapter_is :mysql do + @update.to_sql.should be_like(%Q{ + UPDATE `users` + SET `name` = 'nick' + }) + end + + adapter_is :sqlite3 do + @update.to_sql.should be_like(%Q{ + UPDATE "users" + SET "name" = 'nick' + }) + end + + adapter_is :postgresql do + @update.to_sql.should be_like(%Q{ + UPDATE "users" + SET "name" = E'nick' + }) + end + end + end + + describe 'when given values whose types differ from from the types of the attributes' do + before do + @update = Update.new(@relation, @relation[:id] => '1-asdf') + end + + it 'manufactures sql updating attributes' do + adapter_is :mysql do + @update.to_sql.should be_like(%Q{ + UPDATE `users` + SET `id` = 1 + }) + end + + adapter_is_not :mysql do + @update.to_sql.should be_like(%Q{ + UPDATE "users" + SET "id" = 1 + }) + end + end + end + + describe 'when the relation is a where' do + before do + @update = Update.new( + @relation.where(@relation[:id].eq(1)), + @relation[:name] => "nick" + ) + end + + it 'manufactures sql updating a where relation' do + adapter_is :mysql do + @update.to_sql.should be_like(%Q{ + UPDATE `users` + SET `name` = 'nick' + WHERE `users`.`id` = 1 + }) + end + + adapter_is :sqlite3 do + @update.to_sql.should be_like(%Q{ + UPDATE "users" + SET "name" = 'nick' + WHERE "users"."id" = 1 + }) + end + + adapter_is :postgresql do + @update.to_sql.should be_like(%Q{ + UPDATE "users" + SET "name" = E'nick' + WHERE "users"."id" = 1 + }) + end + end + end + end + + end +end diff --git a/spec/arel/engines/sql/unit/relations/where_spec.rb b/spec/arel/engines/sql/unit/relations/where_spec.rb new file mode 100644 index 0000000000..4f0cce1e01 --- /dev/null +++ b/spec/arel/engines/sql/unit/relations/where_spec.rb @@ -0,0 +1,56 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'spec_helper') + +module Arel + describe Where do + before do + @relation = Table.new(:users) + @predicate = @relation[:id].eq(1) + end + + describe '#to_sql' do + describe 'when given a predicate' do + it "manufactures sql with where clause conditions" do + sql = Where.new(@relation, @predicate).to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + WHERE `users`.`id` = 1 + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + WHERE "users"."id" = 1 + }) + end + end + end + + describe 'when given a string' do + it "passes the string through to the where clause" do + sql = Where.new(@relation, 'asdf').to_sql + + adapter_is :mysql do + sql.should be_like(%Q{ + SELECT `users`.`id`, `users`.`name` + FROM `users` + WHERE asdf + }) + end + + adapter_is_not :mysql do + sql.should be_like(%Q{ + SELECT "users"."id", "users"."name" + FROM "users" + WHERE asdf + }) + end + end + end + end + end +end diff --git a/spec/arel/integration/joins/with_adjacency_spec.rb b/spec/arel/integration/joins/with_adjacency_spec.rb deleted file mode 100644 index 559b5bbe1a..0000000000 --- a/spec/arel/integration/joins/with_adjacency_spec.rb +++ /dev/null @@ -1,139 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Join do - before do - @relation1 = Arel(:users) - @relation2 = @relation1.alias - @predicate = @relation1[:id].eq(@relation2[:id]) - end - - describe 'when joining a relation to itself' do - describe '#to_sql' do - it 'manufactures sql aliasing the table and attributes properly in the join predicate and the where clause' do - @relation1.join(@relation2).on(@predicate).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name` - FROM `users` - INNER JOIN `users` AS `users_2` - ON `users`.`id` = `users_2`.`id` - ") - end - - describe 'when joining with a where on the same relation' do - it 'manufactures sql aliasing the tables properly' do - @relation1 \ - .join(@relation2.where(@relation2[:id].eq(1))) \ - .on(@predicate) \ - .to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name` - FROM `users` - INNER JOIN `users` AS `users_2` - ON `users`.`id` = `users_2`.`id` AND `users_2`.`id` = 1 - ") - end - - describe 'when the where occurs before the alias' do - it 'manufactures sql aliasing the predicates properly' do - relation2 = @relation1.where(@relation1[:id].eq(1)).alias - @relation1 \ - .join(relation2) \ - .on(relation2[:id].eq(@relation1[:id])) \ - .to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name` - FROM `users` - INNER JOIN `users` AS `users_2` - ON `users_2`.`id` = `users`.`id` AND `users_2`.`id` = 1 - ") - end - end - end - - describe 'when joining the relation to itself multiple times' do - before do - @relation3 = @relation1.alias - end - - describe 'when joining left-associatively' do - it 'manufactures sql aliasing the tables properly' do - @relation1 \ - .join(@relation2 \ - .join(@relation3) \ - .on(@relation2[:id].eq(@relation3[:id]))) \ - .on(@relation1[:id].eq(@relation2[:id])) \ - .to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name`, `users_3`.`id`, `users_3`.`name` - FROM `users` - INNER JOIN `users` AS `users_2` - ON `users`.`id` = `users_2`.`id` - INNER JOIN `users` AS `users_3` - ON `users_2`.`id` = `users_3`.`id` - ") - end - end - - describe 'when joining right-associatively' do - it 'manufactures sql aliasing the tables properly' do - @relation1 \ - .join(@relation2).on(@relation1[:id].eq(@relation2[:id])) \ - .join(@relation3).on(@relation2[:id].eq(@relation3[:id])) \ - .to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `users_2`.`id`, `users_2`.`name`, `users_3`.`id`, `users_3`.`name` - FROM `users` - INNER JOIN `users` AS `users_2` - ON `users`.`id` = `users_2`.`id` - INNER JOIN `users` AS `users_3` - ON `users_2`.`id` = `users_3`.`id` - ") - end - end - end - end - - describe '[]' do - describe 'when given an attribute belonging to both sub-relations' do - it 'disambiguates the relation that serves as the ancestor to the attribute' do - @relation1 \ - .join(@relation2) \ - .on(@predicate) \ - .should disambiguate_attributes(@relation1[:id], @relation2[:id]) - end - - describe 'when both relations are compound and only one is an alias' do - it 'disambiguates the relation that serves as the ancestor to the attribute' do - compound1 = @relation1.where(@predicate) - compound2 = compound1.alias - compound1 \ - .join(compound2) \ - .on(@predicate) \ - .should disambiguate_attributes(compound1[:id], compound2[:id]) - end - end - - describe 'when the left relation is extremely compound' do - it 'disambiguates the relation that serves as the ancestor to the attribute' do - @relation1 \ - .where(@predicate) \ - .where(@predicate) \ - .join(@relation2) \ - .on(@predicate) \ - .should disambiguate_attributes(@relation1[:id], @relation2[:id]) - end - end - - describe 'when the right relation is extremely compound' do - it 'disambiguates the relation that serves as the ancestor to the attribute' do - @relation1 \ - .join( \ - @relation2 \ - .where(@predicate) \ - .where(@predicate) \ - .where(@predicate)) \ - .on(@predicate) \ - .should disambiguate_attributes(@relation1[:id], @relation2[:id]) - end - end - end - end - end - end -end diff --git a/spec/arel/integration/joins/with_aggregations_spec.rb b/spec/arel/integration/joins/with_aggregations_spec.rb deleted file mode 100644 index 2b21dcaa1e..0000000000 --- a/spec/arel/integration/joins/with_aggregations_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Join do - before do - @relation1 = Arel(:users) - @relation2 = Arel(:photos) - @predicate = @relation1[:id].eq(@relation2[:user_id]) - end - - describe 'when joining aggregated relations' do - before do - @aggregation = @relation2 \ - .group(@relation2[:user_id]) \ - .project(@relation2[:user_id], @relation2[:id].count.as(:cnt)) \ - end - - describe '#to_sql' do - # CLEANUP - it '' do - @relation1.join(@relation2.take(3)).on(@predicate).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `photos_external`.`id`, `photos_external`.`user_id`, `photos_external`.`camera_id` - FROM `users` - INNER JOIN (SELECT `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` FROM `photos` LIMIT 3) AS `photos_external` - ON `users`.`id` = `photos_external`.`user_id` - ") - end - - describe 'with the aggregation on the right' do - it 'manufactures sql joining the left table to a derived table' do - @relation1.join(@aggregation).on(@predicate).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `photos_external`.`user_id`, `photos_external`.`cnt` - FROM `users` - INNER JOIN (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` GROUP BY `photos`.`user_id`) AS `photos_external` - ON `users`.`id` = `photos_external`.`user_id` - ") - end - end - - describe 'with the aggregation on the left' do - it 'manufactures sql joining the right table to a derived table' do - @aggregation.join(@relation1).on(@predicate).to_sql.should be_like(" - SELECT `photos_external`.`user_id`, `photos_external`.`cnt`, `users`.`id`, `users`.`name` - FROM (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` GROUP BY `photos`.`user_id`) AS `photos_external` - INNER JOIN `users` - ON `users`.`id` = `photos_external`.`user_id` - ") - end - end - - describe 'with the aggregation on both sides' do - it 'it properly aliases the aggregations' do - aggregation2 = @aggregation.alias - @aggregation.join(aggregation2).on(aggregation2[:user_id].eq(@aggregation[:user_id])).to_sql.should be_like(" - SELECT `photos_external`.`user_id`, `photos_external`.`cnt`, `photos_external_2`.`user_id`, `photos_external_2`.`cnt` - FROM (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` GROUP BY `photos`.`user_id`) AS `photos_external` - INNER JOIN (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` GROUP BY `photos`.`user_id`) AS `photos_external_2` - ON `photos_external_2`.`user_id` = `photos_external`.`user_id` - ") - end - end - - describe 'when the aggration has a where' do - describe 'with the aggregation on the left' do - it "manufactures sql keeping wheres on the aggregation within the derived table" do - @relation1.join(@aggregation.where(@aggregation[:user_id].eq(1))).on(@predicate).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `photos_external`.`user_id`, `photos_external`.`cnt` - FROM `users` - INNER JOIN (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` WHERE `photos`.`user_id` = 1 GROUP BY `photos`.`user_id`) AS `photos_external` - ON `users`.`id` = `photos_external`.`user_id` - ") - end - end - - describe 'with the aggregation on the right' do - it "manufactures sql keeping wheres on the aggregation within the derived table" do - @aggregation.where(@aggregation[:user_id].eq(1)).join(@relation1).on(@predicate).to_sql.should be_like(" - SELECT `photos_external`.`user_id`, `photos_external`.`cnt`, `users`.`id`, `users`.`name` - FROM (SELECT `photos`.`user_id`, COUNT(`photos`.`id`) AS `cnt` FROM `photos` WHERE `photos`.`user_id` = 1 GROUP BY `photos`.`user_id`) AS `photos_external` - INNER JOIN `users` - ON `users`.`id` = `photos_external`.`user_id` - ") - end - end - end - end - end - end -end diff --git a/spec/arel/integration/joins/with_compounds_spec.rb b/spec/arel/integration/joins/with_compounds_spec.rb deleted file mode 100644 index 95fadc23d6..0000000000 --- a/spec/arel/integration/joins/with_compounds_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Join do - before do - @relation1 = Arel(:users) - @relation2 = Arel(:photos) - @predicate = @relation1[:id].eq(@relation2[:user_id]) - end - - describe '#to_sql' do - describe 'when the join contains a where' do - describe 'and the where is given a string' do - it 'does not escape the string' do - @relation1 \ - .join(@relation2.where("asdf")) \ - .on(@predicate) \ - .to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` - FROM `users` - INNER JOIN `photos` - ON `users`.`id` = `photos`.`user_id` AND asdf - ") - end - end - end - - describe 'when a compound contains a join' do - describe 'and the compound is a where' do - it 'manufactures sql disambiguating the tables' do - @relation1 \ - .where(@relation1[:id].eq(1)) \ - .join(@relation2) \ - .on(@predicate) \ - .where(@relation1[:id].eq(1)) \ - .to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` - FROM `users` - INNER JOIN `photos` - ON `users`.`id` = `photos`.`user_id` - WHERE `users`.`id` = 1 - AND `users`.`id` = 1 - ") - end - end - - describe 'and the compound is a group' do - it 'manufactures sql disambiguating the tables' do - @relation1 \ - .join(@relation2) \ - .on(@predicate) \ - .group(@relation1[:id]) \ - .to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` - FROM `users` - INNER JOIN `photos` - ON `users`.`id` = `photos`.`user_id` - GROUP BY `users`.`id` - ") - end - end - end - end - end -end diff --git a/spec/arel/unit/predicates/binary_spec.rb b/spec/arel/unit/predicates/binary_spec.rb deleted file mode 100644 index 56fcf2d8ad..0000000000 --- a/spec/arel/unit/predicates/binary_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Binary do - before do - @relation = Table.new(:users) - @attribute1 = @relation[:id] - @attribute2 = @relation[:name] - class ConcreteBinary < Binary - def predicate_sql - "<=>" - end - end - end - - describe "with compound predicates" do - before do - @operand1 = ConcreteBinary.new(@attribute1, 1) - @operand2 = ConcreteBinary.new(@attribute2, "name") - end - - describe Or do - describe "#to_sql" do - it "manufactures sql with an OR operation" do - Or.new(@operand1, @operand2).to_sql.should be_like(" - (`users`.`id` <=> 1 OR `users`.`name` <=> 'name') - ") - end - end - end - - describe And do - describe "#to_sql" do - it "manufactures sql with an AND operation" do - And.new(@operand1, @operand2).to_sql.should be_like(" - (`users`.`id` <=> 1 AND `users`.`name` <=> 'name') - ") - end - end - end - end - - describe '#to_sql' do - describe 'when relating two attributes' do - it 'manufactures sql with a binary operation' do - ConcreteBinary.new(@attribute1, @attribute2).to_sql.should be_like(" - `users`.`id` <=> `users`.`name` - ") - end - end - - describe 'when relating an attribute and a value' do - before do - @value = "1-asdf" - end - - describe 'when relating to an integer attribute' do - it 'formats values as integers' do - ConcreteBinary.new(@attribute1, @value).to_sql.should be_like(" - `users`.`id` <=> 1 - ") - end - end - - describe 'when relating to a string attribute' do - it 'formats values as strings' do - ConcreteBinary.new(@attribute2, @value).to_sql.should be_like(" - `users`.`name` <=> '1-asdf' - ") - end - end - end - end - - describe '#bind' do - before do - @another_relation = @relation.alias - end - - describe 'when both operands are attributes' do - it "manufactures an expression with the attributes bound to the relation" do - ConcreteBinary.new(@attribute1, @attribute2).bind(@another_relation). \ - should == ConcreteBinary.new(@another_relation[@attribute1], @another_relation[@attribute2]) - end - end - - describe 'when an operand is a value' do - it "manufactures an expression with unmodified values" do - ConcreteBinary.new(@attribute1, "asdf").bind(@another_relation). \ - should == ConcreteBinary.new(@attribute1.find_correlate_in(@another_relation), "asdf".find_correlate_in(@another_relation)) - end - end - end - end -end
\ No newline at end of file diff --git a/spec/arel/unit/predicates/in_spec.rb b/spec/arel/unit/predicates/in_spec.rb deleted file mode 100644 index 797798a77f..0000000000 --- a/spec/arel/unit/predicates/in_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe In do - before do - @relation = Table.new(:users) - @attribute = @relation[:id] - end - - describe '#to_sql' do - describe 'when relating to an array' do - describe 'when the array\'s elements are the same type as the attribute' do - before do - @array = [1, 2, 3] - end - - it 'manufactures sql with a comma separated list' do - In.new(@attribute, @array).to_sql.should be_like(" - `users`.`id` IN (1, 2, 3) - ") - end - end - - describe 'when the array\'s elements are not same type as the attribute' do - before do - @array = ['1-asdf', 2, 3] - end - - it 'formats values in the array as the type of the attribute' do - In.new(@attribute, @array).to_sql.should be_like(" - `users`.`id` IN (1, 2, 3) - ") - end - end - end - - describe 'when relating to a range' do - before do - @range = 1..2 - end - - it 'manufactures sql with a between' do - In.new(@attribute, @range).to_sql.should be_like(" - `users`.`id` BETWEEN 1 AND 2 - ") - end - end - - describe 'when relating to a relation' do - it 'manufactures sql with a subselect' do - In.new(@attribute, @relation).to_sql.should be_like(" - `users`.`id` IN (SELECT `users`.`id`, `users`.`name` FROM `users`) - ") - end - end - end - end -end
\ No newline at end of file diff --git a/spec/arel/unit/predicates/predicates_spec.rb b/spec/arel/unit/predicates/predicates_spec.rb deleted file mode 100644 index d11637cabe..0000000000 --- a/spec/arel/unit/predicates/predicates_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Predicate do - before do - @relation = Table.new(:users) - @attribute1 = @relation[:id] - @attribute2 = @relation[:name] - @operand1 = Equality.new(@attribute1, 1) - @operand2 = Equality.new(@attribute2, "name") - end - - describe "when being combined with another predicate with AND logic" do - describe "#to_sql" do - it "manufactures sql with an AND operation" do - @operand1.and(@operand2).to_sql.should be_like(" - (`users`.`id` = 1 AND `users`.`name` = 'name') - ") - end - end - end - - describe "when being combined with another predicate with OR logic" do - describe "#to_sql" do - it "manufactures sql with an OR operation" do - @operand1.or(@operand2).to_sql.should be_like(" - (`users`.`id` = 1 OR `users`.`name` = 'name') - ") - end - end - end - end -end
\ No newline at end of file diff --git a/spec/arel/unit/relations/alias_spec.rb b/spec/arel/unit/relations/alias_spec.rb deleted file mode 100644 index 460a0ed0df..0000000000 --- a/spec/arel/unit/relations/alias_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Alias do - before do - @relation = Table.new(:users) - end - - describe '==' do - it "obtains if the objects are the same" do - Alias.new(@relation).should_not == Alias.new(@relation) - (aliaz = Alias.new(@relation)).should == aliaz - end - end - - describe '#to_sql' do - describe 'when there is no ambiguity' do - it 'does not alias table names anywhere a table name can appear' do - @relation \ - .where(@relation[:id].eq(1)) \ - .order(@relation[:id]) \ - .project(@relation[:id]) \ - .group(@relation[:id]) \ - .alias \ - .to_sql.should be_like(" - SELECT `users`.`id` - FROM `users` - WHERE `users`.`id` = 1 - GROUP BY `users`.`id` - ORDER BY `users`.`id` - ") - end - end - end - end -end diff --git a/spec/arel/unit/relations/delete_spec.rb b/spec/arel/unit/relations/delete_spec.rb deleted file mode 100644 index fa887d20fd..0000000000 --- a/spec/arel/unit/relations/delete_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Deletion do - before do - @relation = Table.new(:users) - end - - describe '#to_sql' do - it 'manufactures sql deleting a table relation' do - Deletion.new(@relation).to_sql.should be_like(" - DELETE - FROM `users` - ") - end - - it 'manufactures sql deleting a where relation' do - Deletion.new(@relation.where(@relation[:id].eq(1))).to_sql.should be_like(" - DELETE - FROM `users` - WHERE `users`.`id` = 1 - ") - end - - it "manufactures sql deleting a ranged relation" do - Deletion.new(@relation.take(1)).to_sql.should be_like(" - DELETE - FROM `users` - LIMIT 1 - ") - end - end - - describe '#call' do - it 'executes a delete on the connection' do - deletion = Deletion.new(@relation) - mock(connection = Object.new).delete(deletion.to_sql) - deletion.call(connection) - end - end - end -end diff --git a/spec/arel/unit/relations/group_spec.rb b/spec/arel/unit/relations/group_spec.rb deleted file mode 100644 index a0147b9416..0000000000 --- a/spec/arel/unit/relations/group_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Group do - before do - @relation = Table.new(:users) - @attribute = @relation[:id] - end - - describe '#to_sql' do - describe 'when given a predicate' do - it "manufactures sql with where clause conditions" do - Group.new(@relation, @attribute).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - GROUP BY `users`.`id` - ") - end - end - - describe 'when given a string' do - it "passes the string through to the where clause" do - Group.new(@relation, 'asdf').to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - GROUP BY asdf - ") - end - end - end - end -end
\ No newline at end of file diff --git a/spec/arel/unit/relations/insert_spec.rb b/spec/arel/unit/relations/insert_spec.rb deleted file mode 100644 index 441c97b290..0000000000 --- a/spec/arel/unit/relations/insert_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Insert do - before do - @relation = Table.new(:users) - end - - describe '#to_sql' do - it 'manufactures sql inserting data when given multiple rows' do - pending 'it should insert multiple rows' - @insertion = Insert.new(@relation, [@relation[:name] => "nick", @relation[:name] => "bryan"]) - - @insertion.to_sql.should be_like(" - INSERT - INTO `users` - (`name`) VALUES ('nick'), ('bryan') - ") - end - - it 'manufactures sql inserting data when given multiple values' do - @insertion = Insert.new(@relation, @relation[:id] => "1", @relation[:name] => "nick") - - @insertion.to_sql.should be_like(" - INSERT - INTO `users` - (`id`, `name`) VALUES (1, 'nick') - ") - end - - describe 'when given values whose types correspond to the types of the attributes' do - before do - @insertion = Insert.new(@relation, @relation[:name] => "nick") - end - - it 'manufactures sql inserting data' do - @insertion.to_sql.should be_like(" - INSERT - INTO `users` - (`name`) VALUES ('nick') - ") - end - end - - describe 'when given values whose types differ from from the types of the attributes' do - before do - @insertion = Insert.new(@relation, @relation[:id] => '1-asdf') - end - - it 'manufactures sql inserting data' do - @insertion.to_sql.should be_like(" - INSERT - INTO `users` - (`id`) VALUES (1) - ") - end - end - end - - describe '#call' do - before do - @insertion = Insert.new(@relation, @relation[:name] => "nick") - end - - it 'executes an insert on the connection' do - mock(connection = Object.new).insert(@insertion.to_sql) - @insertion.call(connection) - end - end - end -end diff --git a/spec/arel/unit/relations/join_spec.rb b/spec/arel/unit/relations/join_spec.rb deleted file mode 100644 index 1698bf9647..0000000000 --- a/spec/arel/unit/relations/join_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Join do - before do - @relation1 = Table.new(:users) - @relation2 = Table.new(:photos) - @predicate = @relation1[:id].eq(@relation2[:user_id]) - end - - describe 'hashing' do - it 'implements hash equality' do - Join.new("INNER JOIN", @relation1, @relation2, @predicate) \ - .should hash_the_same_as(Join.new("INNER JOIN", @relation1, @relation2, @predicate)) - end - end - - describe '#engine' do - it "delegates to a relation's engine" do - Join.new("INNER JOIN", @relation1, @relation2, @predicate).engine.should == @relation1.engine - end - end - - describe '#attributes' do - it 'combines the attributes of the two relations' do - join = Join.new("INNER JOIN", @relation1, @relation2, @predicate) - join.attributes.should == - (@relation1.attributes + @relation2.attributes).collect { |a| a.bind(join) } - end - end - - describe '#to_sql' do - describe 'when joining with another relation' do - it 'manufactures sql joining the two tables on the predicate' do - Join.new("INNER JOIN", @relation1, @relation2, @predicate).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id` - FROM `users` - INNER JOIN `photos` ON `users`.`id` = `photos`.`user_id` - ") - end - end - - describe 'when joining with a string' do - it "passes the string through to the where clause" do - Join.new("INNER JOIN asdf ON fdsa", @relation1).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - INNER JOIN asdf ON fdsa - ") - end - end - end - end -end
\ No newline at end of file diff --git a/spec/arel/unit/relations/order_spec.rb b/spec/arel/unit/relations/order_spec.rb deleted file mode 100644 index d373a8ba12..0000000000 --- a/spec/arel/unit/relations/order_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Order do - before do - @relation = Table.new(:users) - @attribute = @relation[:id] - end - - describe '#to_sql' do - describe "when given an attribute" do - it "manufactures sql with an order clause populated by the attribute" do - Order.new(@relation, @attribute).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - ORDER BY `users`.`id` - ") - end - end - - describe "when given multiple attributes" do - before do - @another_attribute = @relation[:name] - end - - it "manufactures sql with an order clause populated by comma-separated attributes" do - Order.new(@relation, @attribute, @another_attribute).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - ORDER BY `users`.`id`, `users`.`name` - ") - end - end - - describe "when given a string" do - before do - @string = "asdf" - end - - it "passes the string through to the order clause" do - Order.new(@relation, @string).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - ORDER BY asdf - ") - end - end - - describe "when ordering an ordered relation" do - before do - @ordered_relation = Order.new(@relation, @attribute) - @another_attribute = @relation[:name] - end - - it "manufactures sql with the order clause of the last ordering preceding the first ordering" do - Order.new(@ordered_relation, @another_attribute).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - ORDER BY `users`.`name`, `users`.`id` - ") - end - end - end - end -end -
\ No newline at end of file diff --git a/spec/arel/unit/relations/project_spec.rb b/spec/arel/unit/relations/project_spec.rb deleted file mode 100644 index f389b18c54..0000000000 --- a/spec/arel/unit/relations/project_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Project do - before do - @relation = Table.new(:users) - @attribute = @relation[:id] - end - - describe '#attributes' do - before do - @projection = Project.new(@relation, @attribute) - end - - it "manufactures attributes associated with the projection relation" do - @projection.attributes.should == [@attribute].collect { |a| a.bind(@projection) } - end - end - - describe '#to_sql' do - describe 'when given an attribute' do - it "manufactures sql with a limited select clause" do - Project.new(@relation, @attribute).to_sql.should be_like(" - SELECT `users`.`id` - FROM `users` - ") - end - end - - describe 'when given a relation' do - before do - @scalar_relation = Project.new(@relation, @relation[:name]) - end - - it "manufactures sql with scalar selects" do - Project.new(@relation, @scalar_relation).to_sql.should be_like(" - SELECT (SELECT `users`.`name` FROM `users`) AS `users` FROM `users` - ") - end - end - - describe 'when given a string' do - it "passes the string through to the select clause" do - Project.new(@relation, 'asdf').to_sql.should be_like(" - SELECT asdf FROM `users` - ") - end - end - - describe 'when given an expression' do - it 'manufactures sql with expressions' do - @relation.project(@attribute.count).to_sql.should be_like(" - SELECT COUNT(`users`.`id`) AS count_id - FROM `users` - ") - end - - it 'manufactures sql with distinct expressions' do - @relation.project(@attribute.count(true)).to_sql.should be_like(" - SELECT COUNT(DISTINCT `users`.`id`) AS count_id - FROM `users` - ") - end - end - end - - describe '#externalizable?' do - describe 'when the projections are attributes' do - it 'returns false' do - Project.new(@relation, @attribute).should_not be_externalizable - end - end - - describe 'when the projections include an aggregation' do - it "obtains" do - Project.new(@relation, @attribute.sum).should be_externalizable - end - end - end - end -end diff --git a/spec/arel/unit/relations/skip_spec.rb b/spec/arel/unit/relations/skip_spec.rb deleted file mode 100644 index d83c969aa8..0000000000 --- a/spec/arel/unit/relations/skip_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Skip do - before do - @relation = Table.new(:users) - @skipped = 4 - end - - describe '#to_sql' do - it "manufactures sql with limit and offset" do - Skip.new(@relation, @skipped).to_s.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - OFFSET #{@skipped} - ") - end - end - end -end
\ No newline at end of file diff --git a/spec/arel/unit/relations/table_spec.rb b/spec/arel/unit/relations/table_spec.rb deleted file mode 100644 index 54520bf3b6..0000000000 --- a/spec/arel/unit/relations/table_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Table do - before do - @relation = Table.new(:users) - end - - describe '[]' do - describe 'when given a', Symbol do - it "manufactures an attribute if the symbol names an attribute within the relation" do - @relation[:id].should == Attribute.new(@relation, :id) - @relation[:does_not_exist].should be_nil - end - end - - describe 'when given an', Attribute do - it "returns the attribute if the attribute is within the relation" do - @relation[@relation[:id]].should == @relation[:id] - end - - it "returns nil if the attribtue is not within the relation" do - another_relation = Table.new(:photos) - @relation[another_relation[:id]].should be_nil - end - end - - describe 'when given an', Expression do - before do - @expression = @relation[:id].count - end - - it "returns the Expression if the Expression is within the relation" do - @relation[@expression].should be_nil - end - end - end - - describe '#to_sql' do - it "manufactures a simple select query" do - @relation.to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - ") - end - end - - describe '#column_for' do - it "returns the column corresponding to the attribute" do - @relation.column_for(@relation[:id]).should == @relation.columns.detect { |c| c.name == 'id' } - end - end - - describe '#attributes' do - it 'manufactures attributes corresponding to columns in the table' do - @relation.attributes.should == [ - Attribute.new(@relation, :id), - Attribute.new(@relation, :name) - ] - end - - describe '#reset' do - it "reloads columns from the database" do - lambda { stub(@relation.engine).columns { [] } }.should_not change { @relation.attributes } - lambda { @relation.reset }.should change { @relation.attributes } - end - end - end - - describe 'hashing' do - it "implements hash equality" do - Table.new(:users).should hash_the_same_as(Table.new(:users)) - Table.new(:users).should_not hash_the_same_as(Table.new(:photos)) - end - end - - describe '#engine' do - it "defaults to global engine" do - Table.engine = engine = Engine.new - Table.new(:users).engine.should == engine - end - - it "can be specified" do - Table.new(:users, engine = Engine.new).engine.should == engine - end - end - end -end
\ No newline at end of file diff --git a/spec/arel/unit/relations/take_spec.rb b/spec/arel/unit/relations/take_spec.rb deleted file mode 100644 index dca7806057..0000000000 --- a/spec/arel/unit/relations/take_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Take do - before do - @relation = Table.new(:users) - @taken = 4 - end - - describe '#to_sql' do - it "manufactures sql with limit and offset" do - Take.new(@relation, @taken).to_s.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - LIMIT #{@taken} - ") - end - end - end -end
\ No newline at end of file diff --git a/spec/arel/unit/relations/update_spec.rb b/spec/arel/unit/relations/update_spec.rb deleted file mode 100644 index b67369251f..0000000000 --- a/spec/arel/unit/relations/update_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Update do - before do - @relation = Table.new(:users) - end - - describe '#to_sql' do - it "manufactures sql updating attributes when given multiple attributes" do - Update.new(@relation, @relation[:id] => 1, @relation[:name] => "nick").to_sql.should be_like(" - UPDATE `users` - SET `id` = 1, `name` = 'nick' - ") - end - - it "manufactures sql updating attributes when given a ranged relation" do - Update.new(@relation.take(1), @relation[:name] => "nick").to_sql.should be_like(" - UPDATE `users` - SET `name` = 'nick' - LIMIT 1 - ") - end - - describe 'when given values whose types correspond to the types of the attributes' do - before do - @update = Update.new(@relation, @relation[:name] => "nick") - end - - it 'manufactures sql updating attributes' do - @update.to_sql.should be_like(" - UPDATE `users` - SET `name` = 'nick' - ") - end - end - - describe 'when given values whose types differ from from the types of the attributes' do - before do - @update = Update.new(@relation, @relation[:id] => '1-asdf') - end - - it 'manufactures sql updating attributes' do - @update.to_sql.should be_like(" - UPDATE `users` - SET `id` = 1 - ") - end - end - - describe 'when the relation is a where' do - before do - @update = Update.new( - @relation.where(@relation[:id].eq(1)), - @relation[:name] => "nick" - ) - end - - it 'manufactures sql updating a where relation' do - @update.to_sql.should be_like(" - UPDATE `users` - SET `name` = 'nick' - WHERE `users`.`id` = 1 - ") - end - end - end - - describe '#call' do - before do - @update = Update.new(@relation, @relation[:name] => "nick") - end - - it 'executes an update on the connection' do - mock(connection = Object.new).update(@update.to_sql) - @update.call(connection) - end - end - - end -end diff --git a/spec/arel/unit/relations/where_spec.rb b/spec/arel/unit/relations/where_spec.rb deleted file mode 100644 index 8ef4d54b63..0000000000 --- a/spec/arel/unit/relations/where_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module Arel - describe Where do - before do - @relation = Table.new(:users) - @predicate = @relation[:id].eq(1) - end - - describe '#initialize' do - it "manufactures nested where relations if multiple predicates are provided" do - another_predicate = @relation[:name].lt(2) - Where.new(@relation, @predicate, another_predicate). \ - should == Where.new(Where.new(@relation, another_predicate), @predicate) - end - end - - describe '#to_sql' do - describe 'when given a predicate' do - it "manufactures sql with where clause conditions" do - Where.new(@relation, @predicate).to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - WHERE `users`.`id` = 1 - ") - end - end - - describe 'when given a string' do - it "passes the string through to the where clause" do - Where.new(@relation, 'asdf').to_sql.should be_like(" - SELECT `users`.`id`, `users`.`name` - FROM `users` - WHERE asdf - ") - end - end - end - end -end diff --git a/spec/connections/mysql_connection.rb b/spec/connections/mysql_connection.rb new file mode 100644 index 0000000000..a58ddc35ef --- /dev/null +++ b/spec/connections/mysql_connection.rb @@ -0,0 +1,15 @@ +require "activerecord" +puts "Using native MySQL" + +ActiveRecord::Base.logger = Logger.new("debug.log") + +ActiveRecord::Base.configurations = { + 'unit' => { + :adapter => 'mysql', + :username => 'rails', + :encoding => 'utf8', + :database => 'arel_unit', + } +} + +ActiveRecord::Base.establish_connection 'unit' diff --git a/spec/connections/postgresql_connection.rb b/spec/connections/postgresql_connection.rb new file mode 100644 index 0000000000..e376d33ec6 --- /dev/null +++ b/spec/connections/postgresql_connection.rb @@ -0,0 +1,14 @@ +require "activerecord" +puts "Using native PostgreSQL" + +ActiveRecord::Base.logger = Logger.new("debug.log") + +ActiveRecord::Base.configurations = { + 'unit' => { + :adapter => 'postgresql', + :encoding => 'utf8', + :database => 'arel_unit', + } +} + +ActiveRecord::Base.establish_connection 'unit' diff --git a/spec/connections/sqlite3_connection.rb b/spec/connections/sqlite3_connection.rb new file mode 100644 index 0000000000..9e9503e0ca --- /dev/null +++ b/spec/connections/sqlite3_connection.rb @@ -0,0 +1,25 @@ +require "rubygems" +require "activerecord" +puts "Using native SQLite3" + +ActiveRecord::Base.logger = Logger.new("debug.log") + +db_file = "spec/fixtures/fixture_database.sqlite3" + +ActiveRecord::Base.configurations = { + "unit" => { + :adapter => 'sqlite3', + :database => db_file, + :timeout => 5000 + } +} + +unless File.exist?(db_file) + puts "SQLite3 database not found at #{db_file}. Rebuilding it." + FileUtils.mkdir_p(File.dirname(db_file)) + sqlite_command = %Q{sqlite3 "#{db_file}" "create table a (a integer); drop table a;"} + puts "Executing '#{sqlite_command}'" + raise "Seems that there is no sqlite3 executable available" unless system(sqlite_command) +end + +ActiveRecord::Base.establish_connection("unit") diff --git a/spec/doubles/database.rb b/spec/doubles/database.rb deleted file mode 100644 index f8a4b38e17..0000000000 --- a/spec/doubles/database.rb +++ /dev/null @@ -1,51 +0,0 @@ -module Fake - class Engine - def connection - @conn ||= Connection.new - end - end - - class Connection - include ActiveRecord::ConnectionAdapters::Quoting - - def columns(table_name, comment) - { "users" => - [ - Column.new("id", :integer), - Column.new("name", :string) - ], - "photos" => - [ - Column.new("id", :integer), - Column.new("user_id", :integer), - Column.new("camera_id", :integer) - ] - }[table_name] - end - - def execute(*args) - [] - end - - def quote_column_name(column_name) - "`#{column_name}`" - end - - def quote_table_name(table_name) - "`#{table_name}`" - end - - def supports_count_distinct? - true - end - end - - class Column - attr_reader :name, :type - - def initialize(name, type) - @name = name - @type = type - end - end -end diff --git a/spec/doubles/hash.rb b/spec/doubles/hash.rb index 97d25742cb..32c5b98058 100644 --- a/spec/doubles/hash.rb +++ b/spec/doubles/hash.rb @@ -2,19 +2,19 @@ class Hash def ordered_array to_a.sort { |(key1, value1), (key2, value2)| key1.hash <=> key2.hash } end - + def keys ordered_array.collect(&:first) end - + def values ordered_array.collect { |_, v| v } end - + def each(&block) ordered_array.each(&block) end - + def shift returning to_a.first do |k, v| delete(k) diff --git a/spec/matchers/be_like.rb b/spec/matchers/be_like.rb index 4ff5bc532f..c9d4d4b979 100644 --- a/spec/matchers/be_like.rb +++ b/spec/matchers/be_like.rb @@ -3,22 +3,22 @@ module BeLikeMatcher def initialize(expected) @expected = expected end - + def matches?(actual) @actual = actual @expected.gsub(/\s+/, ' ').strip == @actual.gsub(/\s+/, ' ').strip end - + def failure_message "expected\n#{@actual}\nto be like\n#{@expected}" end - + def negative_failure_message "expected\n#{@actual}\nto be unlike\n#{@expected}" end end - + def be_like(expected) BeLike.new(expected) end -end
\ No newline at end of file +end diff --git a/spec/matchers/disambiguate_attributes.rb b/spec/matchers/disambiguate_attributes.rb index bee7d22b0c..bc4a5215d4 100644 --- a/spec/matchers/disambiguate_attributes.rb +++ b/spec/matchers/disambiguate_attributes.rb @@ -3,7 +3,7 @@ module DisambiguateAttributesMatcher def initialize(attributes) @attributes = attributes end - + def matches?(actual) @actual = actual attribute1, attribute2 = @attributes @@ -11,18 +11,18 @@ module DisambiguateAttributesMatcher !@actual[attribute1].descends_from?(attribute2) && @actual[attribute2].descends_from?(attribute2) end - + def failure_message "" # "expected #{@actual} to disambiguate its attributes" end - + def negative_failure_message "expected #{@actual} to not disambiguate its attributes" end end - + def disambiguate_attributes(*attributes) DisambiguateAttributes.new(attributes) end -end
\ No newline at end of file +end diff --git a/spec/matchers/hash_the_same_as.rb b/spec/matchers/hash_the_same_as.rb index c1903b62b4..03e955a0cb 100644 --- a/spec/matchers/hash_the_same_as.rb +++ b/spec/matchers/hash_the_same_as.rb @@ -3,24 +3,24 @@ module HashTheSameAsMatcher def initialize(expected) @expected = expected end - + def matches?(actual) @actual = actual hash = {} hash[@expected] = :some_arbitrary_value hash[@actual] == :some_arbitrary_value end - + def failure_message "expected #{@actual} to hash the same as #{@expected}; they must be `eql?` and have the same `#hash` value" end - + def negative_failure_message "expected #{@actual} to hash differently than #{@expected}; they must not be `eql?` or have a differing `#hash` values" end end - + def hash_the_same_as(expected) HashTheSameAs.new(expected) end -end
\ No newline at end of file +end diff --git a/spec/schemas/mysql_schema.rb b/spec/schemas/mysql_schema.rb new file mode 100644 index 0000000000..dc2558fd6a --- /dev/null +++ b/spec/schemas/mysql_schema.rb @@ -0,0 +1,18 @@ +sql = <<-SQL + DROP TABLE IF EXISTS users; + CREATE TABLE users ( + id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL + ); + + DROP TABLE IF EXISTS photos; + CREATE TABLE photos ( + id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, + user_id INTEGER NOT NULL, + camera_id INTEGER NOT NULL + ); +SQL + +sql.split(/;/).select(&:present?).each do |sql_statement| + ActiveRecord::Base.connection.execute sql_statement +end diff --git a/spec/schemas/postgresql_schema.rb b/spec/schemas/postgresql_schema.rb new file mode 100644 index 0000000000..30fa665902 --- /dev/null +++ b/spec/schemas/postgresql_schema.rb @@ -0,0 +1,18 @@ +sql = <<-SQL + DROP TABLE IF EXISTS users; + CREATE TABLE users ( + id SERIAL PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL + ); + + DROP TABLE IF EXISTS photos; + CREATE TABLE photos ( + id SERIAL PRIMARY KEY NOT NULL, + user_id INTEGER NOT NULL, + camera_id INTEGER NOT NULL + ); +SQL + +sql.split(/;/).select(&:present?).each do |sql_statement| + ActiveRecord::Base.connection.execute sql_statement +end diff --git a/spec/schemas/sqlite3_schema.rb b/spec/schemas/sqlite3_schema.rb new file mode 100644 index 0000000000..94d224520e --- /dev/null +++ b/spec/schemas/sqlite3_schema.rb @@ -0,0 +1,18 @@ +sql = <<-SQL + DROP TABLE IF EXISTS users; + CREATE TABLE users ( + id INTEGER NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL + ); + + DROP TABLE IF EXISTS photos; + CREATE TABLE photos ( + id INTEGER NOT NULL PRIMARY KEY, + user_id INTEGER NOT NULL, + camera_id INTEGER NOT NULL + ); +SQL + +sql.split(/;/).select(&:present?).each do |sql_statement| + ActiveRecord::Base.connection.execute sql_statement +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ce539b6ffa..beb634fbd3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,10 +11,37 @@ require 'arel' Dir["#{dir}/#{helper}/*"].each { |m| require "#{dir}/#{helper}/#{File.basename(m)}" } end +module AdapterGuards + def adapter_is(name) + verify_adapter_name(name) + yield if name.to_s == adapter_name + end + + def adapter_is_not(name) + verify_adapter_name(name) + yield if name.to_s != adapter_name + end + + def adapter_name + name = ActiveRecord::Base.configurations["unit"][:adapter] + verify_adapter_name(name) + name + end + + def verify_adapter_name(name) + raise "Invalid adapter name: #{name}" unless valid_adapters.include?(name.to_s) + end + + def valid_adapters + %w[mysql postgresql sqlite3] + end +end + Spec::Runner.configure do |config| - config.include(BeLikeMatcher, HashTheSameAsMatcher, DisambiguateAttributesMatcher) + config.include BeLikeMatcher, HashTheSameAsMatcher, DisambiguateAttributesMatcher + config.include AdapterGuards config.mock_with :rr config.before do - Arel::Table.engine = Arel::Engine.new(Fake::Engine.new) + Arel::Table.engine = Arel::Sql::Engine.new(ActiveRecord::Base) end end |