diff options
41 files changed, 374 insertions, 152 deletions
diff --git a/README.markdown b/README.markdown index 4324b0b128..89122a6dd3 100644 --- a/README.markdown +++ b/README.markdown @@ -3,7 +3,7 @@ ActiveRelation ## Abstract ## -ActiveRelation is a Relational Algebra for Ruby. It simplifies the generation of both the simplest and the most complex of SQL queries and it transparently adapts to various RDBMS systems. It is intended to be a framework framework; that is, you can build your own ORM with it, focusing on innovative object and collection modeling as opposed to database compatibility and query generation. +ActiveRelation is a Relational Algebra for Ruby. It 1) simplifies the generation of both the simplest and the most complex of SQL queries and it 2) transparently adapts to various RDBMS systems. It is intended to be a framework framework; that is, you can build your own ORM with it, focusing on innovative object and collection modeling as opposed to database compatibility and query generation. ## A Gentle Introduction ## @@ -0,0 +1,49 @@ +todo: +- clean up integration tests in join - i find the aggregate stuff too integrationy; unit testing would be better +- test relation, table reset +- cache expiry on write + - rewrite of querycache test in light of this +- relation inclusion when given an array (1,2,3,4) should quote the elements using the appropriate quoting strategy taken from the attribute + - descend on array, along with bind written in terms of it +- standardize quoting + - use strings everywhere, not symbols ? +- rename sql strategies +- test sql strategies + - the parallel names are confusing since it's not obvious what's what. plus, we lack unit tests; the biggest complaint is a lack of clarity concerning what purpose these serve in the system. Unit tests wont illustrate much unless we use concrete examples. +- rename the tion (Selection) classes so that words that don't end in tion don't seem inconsistent +- re-evaluate bind + +done: +. Relation <=> Relation -> InnerJoinOperation +. Relation << Relation -> LeftOuterJoinOperation +. InnerJoinOperation.on(*Predicate) -> InnerJoinRelation +. LeftOuterJoinOperation.on(*Predicate) -> LeftOuterJoinRelation +. Relation[Symbol] -> Attribute +. Relation[Range] -> Relation +. Attribute == Attribute -> EqualityPredicate +. Attribute >= Attribute -> GreaterThanOrEqualToPredicate +. Relation.include?(Column) -> Predicate +. Relation.project(*Column) -> ProjectionRelation +. Relation.select(*Predicate) -> SelectionRelation +. Relation.order(*Column) -> OrderRelation +. #to_sql +. Remove Builder +. Namespace +. Audit SqlAlchemy for missing features +- Generalized denormalizations on any aggregation (count, yes, but also max, min, average) +- Remove operator overloading of << and <=> for joins. Make it just foo.join(bar) and foo.outer_join(bar). +- Remove operator overloading of == for predicates. make it a.equals(b) (note lack of question mark). +- hookup more predicates (=, <=, =>) +- get some basic aggregations working: users.project(user[:points].max) +- Alias Table Names +- When joining with any sort of aggregation, it needs to be a nested select +- get a value select working: users.project(users[:name], addresses.select(addresses[:user_id] == users[:id]).project(addresses[:id].count)) +- Session +- sublimate values to deal with the fact that they must be quoted per engine +- clean-up singleton monstrosity +- extract hashing module +- hash custom matcher +- make session engine stuff follow laws of demeter - currently doing some odd method chaining? rethink who is responsible for what + - session just calls execute, passing in a connection; by default it gets a connection from the relation. +- #strategy is now on value, attribute and relation; you must admit it's name is confusing given that e.g., relation already has a strategy (Sql::Relation) ... should it be called predicate strategy? operand1.to_sql(operand2.predicate) maybe prefer operand1.cast(operand2) or project or in light of + - renamed to #format: operand1.format(operand2) diff --git a/lib/active_relation.rb b/lib/active_relation.rb index f04825ad4c..5b5c91706f 100644 --- a/lib/active_relation.rb +++ b/lib/active_relation.rb @@ -4,8 +4,8 @@ require 'rubygems' require 'activesupport' require 'activerecord' -require 'active_relation/sql' require 'active_relation/extensions' +require 'active_relation/sql' require 'active_relation/predicates' require 'active_relation/relations' require 'active_relation/engines' diff --git a/lib/active_relation/.DS_Store b/lib/active_relation/.DS_Store Binary files differindex 2a449ff62e..9918127870 100644 --- a/lib/active_relation/.DS_Store +++ b/lib/active_relation/.DS_Store diff --git a/lib/active_relation/extensions.rb b/lib/active_relation/extensions.rb index 8a024947ed..7268a5549b 100644 --- a/lib/active_relation/extensions.rb +++ b/lib/active_relation/extensions.rb @@ -1,3 +1,4 @@ require 'active_relation/extensions/object' +require 'active_relation/extensions/class' require 'active_relation/extensions/array' require 'active_relation/extensions/hash' diff --git a/lib/active_relation/extensions/array.rb b/lib/active_relation/extensions/array.rb index aa4354a78a..4bd20d8121 100644 --- a/lib/active_relation/extensions/array.rb +++ b/lib/active_relation/extensions/array.rb @@ -2,7 +2,7 @@ class Array def to_hash Hash[*flatten] end - + def to_sql(strategy = nil) "(#{collect(&:to_sql).join(', ')})" end diff --git a/lib/active_relation/extensions/class.rb b/lib/active_relation/extensions/class.rb new file mode 100644 index 0000000000..0e5b728c26 --- /dev/null +++ b/lib/active_relation/extensions/class.rb @@ -0,0 +1,17 @@ +class Class + def abstract(*methods) + methods.each do |method| + define_method method do + raise NotImplementedError + end + end + end + + def hash_on(delegatee) + define_method :eql? do |other| + self == other + end + + delegate :hash, :to => delegatee + end +end
\ No newline at end of file diff --git a/lib/active_relation/extensions/object.rb b/lib/active_relation/extensions/object.rb index d13cf9aabb..90eb73f0b0 100644 --- a/lib/active_relation/extensions/object.rb +++ b/lib/active_relation/extensions/object.rb @@ -1,6 +1,6 @@ class Object def bind(relation) - ActiveRelation::Scalar.new(self, relation) + ActiveRelation::Value.new(self, relation) end def metaclass diff --git a/lib/active_relation/predicates.rb b/lib/active_relation/predicates.rb index ba926a86e5..12ddd1b48d 100644 --- a/lib/active_relation/predicates.rb +++ b/lib/active_relation/predicates.rb @@ -25,7 +25,7 @@ module ActiveRelation end def to_sql(strategy = nil) - "#{operand1.to_sql(operand2.strategy)} #{predicate_sql} #{operand2.to_sql(operand1.strategy)}" + "#{operand2.format(operand1)} #{predicate_sql} #{operand1.format(operand2)}" end def descend diff --git a/lib/active_relation/primitives.rb b/lib/active_relation/primitives.rb index 7629256034..9909734d24 100644 --- a/lib/active_relation/primitives.rb +++ b/lib/active_relation/primitives.rb @@ -1,4 +1,4 @@ require 'active_relation/primitives/attribute' -require 'active_relation/primitives/scalar' +require 'active_relation/primitives/value' require 'active_relation/primitives/expression' diff --git a/lib/active_relation/primitives/attribute.rb b/lib/active_relation/primitives/attribute.rb index baaae1973c..0fed676727 100644 --- a/lib/active_relation/primitives/attribute.rb +++ b/lib/active_relation/primitives/attribute.rb @@ -48,10 +48,7 @@ module ActiveRelation module Congruence def self.included(klass) - klass.class_eval do - alias_method :eql?, :== - delegate :hash, :to => :name - end + klass.hash_on :name end def history @@ -114,15 +111,19 @@ module ActiveRelation end include Expressions - def to_sql(strategy = self.strategy) + def to_sql(strategy = Sql::WhereCondition.new(engine)) strategy.attribute prefix, name, self.alias end + def format(object) + object.to_sql(strategy) + end + + private def strategy Sql::Attribute.new(self) end - private def prefix relation.prefix_for(self) end diff --git a/lib/active_relation/primitives/scalar.rb b/lib/active_relation/primitives/value.rb index fa88404ee3..096c876ecd 100644 --- a/lib/active_relation/primitives/scalar.rb +++ b/lib/active_relation/primitives/value.rb @@ -1,17 +1,17 @@ module ActiveRelation - class Scalar + class Value attr_reader :value, :relation def initialize(value, relation) @value, @relation = value, relation end - def to_sql(strategy = self.strategy) - strategy.scalar value + def to_sql(strategy = Sql::WhereCondition.new(relation.engine)) + strategy.value value end - def strategy - ActiveRelation::Sql::Scalar.new(relation.engine) + def format(object) + object.to_sql(Sql::Value.new(relation.engine)) end def ==(other) diff --git a/lib/active_relation/relations.rb b/lib/active_relation/relations.rb index f992fca8be..7776fd3d18 100644 --- a/lib/active_relation/relations.rb +++ b/lib/active_relation/relations.rb @@ -1,5 +1,6 @@ require 'active_relation/relations/relation' require 'active_relation/relations/compound' +require 'active_relation/relations/writing' require 'active_relation/relations/table' require 'active_relation/relations/join' require 'active_relation/relations/aggregation' diff --git a/lib/active_relation/relations/compound.rb b/lib/active_relation/relations/compound.rb index a10b7588e7..fac0939c6f 100644 --- a/lib/active_relation/relations/compound.rb +++ b/lib/active_relation/relations/compound.rb @@ -1,5 +1,7 @@ module ActiveRelation class Compound < Relation + abstract :==, :descend + attr_reader :relation delegate :joins, :selects, :orders, :groupings, :table_sql, :inserts, :limit, :offset, :name, :alias, :aggregation?, :alias?, :prefix_for, :column_for, diff --git a/lib/active_relation/relations/deletion.rb b/lib/active_relation/relations/deletion.rb index f3d81baf27..0fcf523efe 100644 --- a/lib/active_relation/relations/deletion.rb +++ b/lib/active_relation/relations/deletion.rb @@ -1,5 +1,5 @@ module ActiveRelation - class Deletion < Compound + class Deletion < Writing def initialize(relation) @relation = relation end @@ -12,6 +12,10 @@ module ActiveRelation ].compact.join("\n") end + def call(connection = engine.connection) + connection.delete(to_sql) + end + def ==(other) self.class == other.class and relation == other.relation diff --git a/lib/active_relation/relations/insertion.rb b/lib/active_relation/relations/insertion.rb index 16fe3d5f46..ce065b64c1 100644 --- a/lib/active_relation/relations/insertion.rb +++ b/lib/active_relation/relations/insertion.rb @@ -1,5 +1,5 @@ module ActiveRelation - class Insertion < Compound + class Insertion < Writing attr_reader :record def initialize(relation, record) @@ -15,6 +15,10 @@ module ActiveRelation ].join("\n") end + def call(connection = engine.connection) + connection.insert(to_sql) + end + def ==(other) self.class == other.class and relation == other.relation and diff --git a/lib/active_relation/relations/join.rb b/lib/active_relation/relations/join.rb index 043b237de7..4a57795280 100644 --- a/lib/active_relation/relations/join.rb +++ b/lib/active_relation/relations/join.rb @@ -61,7 +61,7 @@ module ActiveRelation delegate :engine, :to => :relation def table_sql - relation.aggregation?? relation.to_sql(Sql::Aggregation.new(engine)) : relation.send(:table_sql) + relation.aggregation?? relation.to_sql(Sql::TableReference.new(engine)) : relation.send(:table_sql) end def selects diff --git a/lib/active_relation/relations/relation.rb b/lib/active_relation/relations/relation.rb index 1c97cc7035..15782cde06 100644 --- a/lib/active_relation/relations/relation.rb +++ b/lib/active_relation/relations/relation.rb @@ -1,5 +1,7 @@ module ActiveRelation class Relation + abstract :attributes, :selects, :orders, :inserts, :groupings, :joins, :limit, :offset, :alias + def session Session.new end @@ -102,38 +104,41 @@ module ActiveRelation false end - def eql?(other) - self == other - end - - def to_sql(strategy = Sql::Relation.new(engine)) + def to_sql(strategy = Sql::SelectStatement.new(engine)) strategy.select [ - "SELECT #{attributes.collect{ |a| a.to_sql(Sql::Projection.new(engine)) }.join(', ')}", + "SELECT #{attributes.collect{ |a| a.to_sql(Sql::SelectExpression.new(engine)) }.join(', ')}", "FROM #{table_sql}", - (joins unless joins.blank? ), - ("WHERE #{selects.collect{|s| s.to_sql(Sql::Selection.new(engine))}.join("\n\tAND ")}" unless selects.blank? ), - ("ORDER BY #{orders.collect(&:to_sql)}" unless orders.blank? ), - ("GROUP BY #{groupings.collect(&:to_sql)}" unless groupings.blank? ), - ("LIMIT #{limit}" unless limit.blank? ), - ("OFFSET #{offset}" unless offset.blank? ) + (joins unless joins.blank? ), + ("WHERE #{selects.collect{|s| s.to_sql(Sql::WhereClause.new(engine))}.join("\n\tAND ")}" unless selects.blank? ), + ("ORDER BY #{orders.collect(&:to_sql)}" unless orders.blank? ), + ("GROUP BY #{groupings.collect(&:to_sql)}" unless groupings.blank? ), + ("LIMIT #{limit}" unless limit.blank? ), + ("OFFSET #{offset}" unless offset.blank? ) ].compact.join("\n"), self.alias end alias_method :to_s, :to_sql - - def attribute_for_name(name) - attributes.detect { |a| a.alias_or_name.to_s == name.to_s } + + def call(connection = engine.connection) + connection.select_all(to_sql) end + + module AttributeAccessors + def attribute_for_name(name) + attributes.detect { |a| a.alias_or_name.to_s == name.to_s } + end - def attribute_for_attribute(attribute) - attributes.detect { |a| a =~ attribute } + def attribute_for_attribute(attribute) + attributes.detect { |a| a =~ attribute } + end end + include AttributeAccessors def bind(relation) self end - def strategy - Sql::Predicate.new(engine) + def format(object) + object.to_sql(Sql::WhereCondition.new(engine)) end def attributes; [] end diff --git a/lib/active_relation/relations/table.rb b/lib/active_relation/relations/table.rb index 84eb1213ee..4682298608 100644 --- a/lib/active_relation/relations/table.rb +++ b/lib/active_relation/relations/table.rb @@ -3,7 +3,7 @@ module ActiveRelation cattr_accessor :engine attr_reader :name, :engine - delegate :hash, :to => :name + hash_on :name def initialize(name, engine = Table.engine) @name, @engine = name.to_s, engine diff --git a/lib/active_relation/relations/update.rb b/lib/active_relation/relations/update.rb index c50919af3e..a51f248866 100644 --- a/lib/active_relation/relations/update.rb +++ b/lib/active_relation/relations/update.rb @@ -1,5 +1,5 @@ module ActiveRelation - class Update < Compound + class Update < Writing attr_reader :assignments def initialize(relation, assignments) @@ -16,6 +16,10 @@ module ActiveRelation ].join("\n") end + def call(connection = engine.connection) + connection.update(to_sql) + end + def ==(other) self.class == other.class and relation == other.relation and diff --git a/lib/active_relation/relations/writing.rb b/lib/active_relation/relations/writing.rb new file mode 100644 index 0000000000..3054e9b72f --- /dev/null +++ b/lib/active_relation/relations/writing.rb @@ -0,0 +1,5 @@ +module ActiveRelation + class Writing < Compound + abstract :call, :to_sql + end +end
\ No newline at end of file diff --git a/lib/active_relation/sessions/session.rb b/lib/active_relation/sessions/session.rb index 4bdbe69978..fe917a0e4d 100644 --- a/lib/active_relation/sessions/session.rb +++ b/lib/active_relation/sessions/session.rb @@ -25,22 +25,22 @@ module ActiveRelation module CRUD def create(insert) - insert.engine.insert(insert.to_sql) + insert.call(insert.engine.connection) end def read(select) @read ||= Hash.new do |hash, select| - hash[select] = select.engine.select_all(select.to_sql) + hash[select] = select.call(select.engine.connection) end @read[select] end def update(update) - update.engine.update(update.to_sql) + update.call(update.engine.connection) end def delete(delete) - delete.engine.delete(delete.to_sql) + delete.call(delete.engine.connection) end end include CRUD diff --git a/lib/active_relation/sql.rb b/lib/active_relation/sql.rb index 99cfc66383..0d233b307e 100644 --- a/lib/active_relation/sql.rb +++ b/lib/active_relation/sql.rb @@ -4,9 +4,11 @@ module ActiveRelation delegate :quote_table_name, :quote_column_name, :quote, :to => :engine end - # module Formatting Context / Strategy # unit test me!!! - class Strategy + class Formatter + abstract :attribute, :select, :value + attr_reader :engine + include Quoting def initialize(engine) @@ -14,7 +16,7 @@ module ActiveRelation end end - class Projection < Strategy + class SelectExpression < Formatter def attribute(relation_name, attribute_name, aliaz) "#{quote_table_name(relation_name)}.#{quote_column_name(attribute_name)}" + (aliaz ? " AS #{quote(aliaz.to_s)}" : "") end @@ -24,13 +26,19 @@ module ActiveRelation end end - class Predicate < Strategy + class WhereClause < Formatter + def value(value) + value + end + end + + class WhereCondition < Formatter def attribute(relation_name, attribute_name, aliaz) "#{quote_table_name(relation_name)}.#{quote_column_name(attribute_name)}" end - def scalar(scalar, column = nil) - quote(scalar, column) + def value(value, column = nil) + quote(value, column) end def select(select_sql, aliaz) @@ -38,35 +46,29 @@ module ActiveRelation end end - class Selection < Strategy - def scalar(scalar) - scalar - end - end - - class Relation < Strategy + class SelectStatement < Formatter def select(select_sql, aliaz) select_sql end end - class Aggregation < Strategy + class TableReference < Formatter def select(select_sql, aliaz) - "(#{select_sql}) AS #{engine.quote_table_name(aliaz)}" + "(#{select_sql}) AS #{quote_table_name(aliaz)}" end end - class Attribute < Predicate + class Attribute < WhereCondition def initialize(attribute) @attribute, @engine = attribute, attribute.engine end - def scalar(scalar) - quote(scalar, @attribute.column) + def value(value) + quote(value, @attribute.column) end end - class Scalar < Predicate + class Value < WhereCondition end end end
\ No newline at end of file diff --git a/spec/active_relation/unit/predicates/binary_spec.rb b/spec/active_relation/unit/predicates/binary_spec.rb index 677d8f3ab4..13b3f10a8c 100644 --- a/spec/active_relation/unit/predicates/binary_spec.rb +++ b/spec/active_relation/unit/predicates/binary_spec.rb @@ -3,17 +3,83 @@ require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') module ActiveRelation describe Binary do before do - @relation1 = Table.new(:users) - @relation2 = Table.new(:photos) - @attribute1 = @relation1[:id] - @attribute2 = @relation2[:id] + @relation = Table.new(:users) + @attribute1 = @relation[:id] + @attribute2 = @relation[:name] + @value = "1-asdf".bind(@relation) class ConcreteBinary < Binary def predicate_sql "<=>" 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 + 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 + + describe 'when relating two values' do + before do + @another_value = 2.bind(@relation) + end + + it 'quotes values appropriate to their type' do + ConcreteBinary.new(string = @value, integer = @another_value).to_sql.should be_like(" + '1-asdf' <=> 2 + ") + end + end + + describe 'when relating to an array' do + it 'manufactures sql with a list' do + pending + array = [1, 2, 3] + ConcreteBinary.new(@attribute1, array).to_sql.should be_like(" + `users`.`id` <=> (1,2,3) + ") + end + + it 'formats values in the array in the type of the attribute' do + pending + array = ['1-asdf', 2, 3] + ConcreteBinary.new(@attribute1, array).to_sql.should be_like(" + `users`.`id` <=> (1,2,3) + ") + end + end + + describe 'when relating to a relation' do + it 'manufactures sql with a subselect' do + ConcreteBinary.new(@attribute1, @relation).to_sql.should be_like(" + `users`.`id` <=> (SELECT `users`.`id`, `users`.`name` FROM `users`) + ") + end + end + end + describe '==' do it "obtains if attribute1 and attribute2 are identical" do Binary.new(@attribute1, @attribute2).should == Binary.new(@attribute1, @attribute2) @@ -34,30 +100,20 @@ module ActiveRelation end describe '#descend' do - it "distributes over the predicates and attributes" do + it "distributes a block over the predicates and attributes" do ConcreteBinary.new(@attribute1, @attribute2).descend(&:qualify). \ should == ConcreteBinary.new(@attribute1.qualify, @attribute2.qualify) end end describe '#bind' do - it "distributes over the predicates and attributes" do - ConcreteBinary.new(@attribute1, @attribute2).bind(@relation2). \ - should == ConcreteBinary.new(@attribute1.bind(@relation2), @attribute2.bind(@relation2)) - end - end - - describe '#to_sql' do - it 'manufactures sql with a binary operation' do - ConcreteBinary.new(@attribute1, @attribute2).to_sql.should be_like(" - `users`.`id` <=> `photos`.`id` - ") + before do + @another_relation = Table.new(:photos) end - it 'appropriately quotes scalars' do - ConcreteBinary.new(@attribute1, "1-asdf".bind(@relation1)).to_sql.should be_like(" - `users`.`id` <=> 1 - ") + it "descends" do + ConcreteBinary.new(@attribute1, @attribute2).bind(@another_relation). \ + should == ConcreteBinary.new(@attribute1.bind(@another_relation), @attribute2.bind(@another_relation)) end end end diff --git a/spec/active_relation/unit/predicates/relation_inclusion_spec.rb b/spec/active_relation/unit/predicates/relation_inclusion_spec.rb deleted file mode 100644 index af5846b747..0000000000 --- a/spec/active_relation/unit/predicates/relation_inclusion_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_helper') - -module ActiveRelation - describe RelationInclusion do - before do - users = Table.new(:users) - @relation = users.project(users[:id]) - @attribute = @relation[:id] - end - - describe RelationInclusion, '#to_sql' do - it "manufactures subselect sql" do - # remove when sufficient coverage of sql strategies exists - RelationInclusion.new(@attribute, @relation).to_sql.should be_like(" - `users`.`id` IN (SELECT `users`.`id` FROM `users`) - ") - end - end - end -end
\ No newline at end of file diff --git a/spec/active_relation/unit/primitives/attribute_spec.rb b/spec/active_relation/unit/primitives/attribute_spec.rb index 95c972d814..bdd22721b3 100644 --- a/spec/active_relation/unit/primitives/attribute_spec.rb +++ b/spec/active_relation/unit/primitives/attribute_spec.rb @@ -39,8 +39,10 @@ module ActiveRelation end describe '#column' do - it "" do - pending + it "returns the corresponding column in the relation" do + pending "damn mock based tests are too easy" + stub(@relation).column_for(@attribute) { 'bruisers' } + @attribute.column.should == 'bruisers' end end @@ -68,6 +70,13 @@ module ActiveRelation Attribute.new(@relation, :name).should =~ Attribute.new(@relation, :name, :ancestor => Attribute.new(@relation, :name)) end end + + describe 'hashing' do + it "implements hash equality" do + Attribute.new(@relation, 'name').should hash_the_same_as(Attribute.new(@relation, 'name')) + Attribute.new(@relation, 'name').should_not hash_the_same_as(Attribute.new(@relation, 'id')) + end + end end describe '#to_sql' do diff --git a/spec/active_relation/unit/relations/compound_spec.rb b/spec/active_relation/unit/relations/compound_spec.rb index d271529998..e8fcd12e2c 100644 --- a/spec/active_relation/unit/relations/compound_spec.rb +++ b/spec/active_relation/unit/relations/compound_spec.rb @@ -21,13 +21,5 @@ module ActiveRelation @compound_relation.attributes.should == @relation.attributes.collect { |a| a.bind(@compound_relation) } end end - - describe 'hashing' do - it 'implements hash equality' do - hash = {} - hash[@compound_relation] = 1 - hash[ConcreteCompound.new(@relation)].should == 1 - end - end end end
\ No newline at end of file diff --git a/spec/active_relation/unit/relations/deletion_spec.rb b/spec/active_relation/unit/relations/deletion_spec.rb index 1f14ae5d34..27d879e96f 100644 --- a/spec/active_relation/unit/relations/deletion_spec.rb +++ b/spec/active_relation/unit/relations/deletion_spec.rb @@ -22,5 +22,13 @@ module ActiveRelation ") 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
\ No newline at end of file diff --git a/spec/active_relation/unit/relations/insertion_spec.rb b/spec/active_relation/unit/relations/insertion_spec.rb index 91bf7773c1..f081743c50 100644 --- a/spec/active_relation/unit/relations/insertion_spec.rb +++ b/spec/active_relation/unit/relations/insertion_spec.rb @@ -4,16 +4,24 @@ module ActiveRelation describe Insertion do before do @relation = Table.new(:users) + @insertion = Insertion.new(@relation, @relation[:name] => "nick".bind(@relation)) end describe '#to_sql' do it 'manufactures sql inserting the data for one item' do - Insertion.new(@relation, @relation[:name] => "nick".bind(@relation)).to_sql.should be_like(" + @insertion.to_sql.should be_like(" INSERT INTO `users` (`users`.`name`) VALUES ('nick') ") end end + + describe '#call' do + it 'executes an insert on the connection' do + mock(connection = Object.new).insert(@insertion.to_sql) + @insertion.call(connection) + end + end end end
\ No newline at end of file diff --git a/spec/active_relation/unit/relations/join_spec.rb b/spec/active_relation/unit/relations/join_spec.rb index a424239c4b..fefe069d7b 100644 --- a/spec/active_relation/unit/relations/join_spec.rb +++ b/spec/active_relation/unit/relations/join_spec.rb @@ -25,6 +25,13 @@ module ActiveRelation end end + describe '#qualify' do + it 'descends' do + Join.new("INNER JOIN", @relation1, @relation2, @predicate).qualify. \ + should == Join.new("INNER JOIN", @relation1, @relation2, @predicate).descend(&:qualify) + end + end + describe '#descend' do it 'distributes over the relations and predicates' do Join.new("INNER JOIN", @relation1, @relation2, @predicate).qualify. \ @@ -33,8 +40,27 @@ module ActiveRelation end describe '#prefix_for' do - it 'needs a test' do - pending + describe 'when the joined relations are simple' do + it "returns the name of the relation containing the attribute" do + Join.new("INNER JOIN", @relation1, @relation2, @predicate).prefix_for(@relation1[:id]) \ + .should == @relation1.prefix_for(@relation1[:id]) + Join.new("INNER JOIN", @relation1, @relation2, @predicate).prefix_for(@relation2[:id]) \ + .should == @relation2.prefix_for(@relation2[:id]) + + end + end + + describe 'when one of the joined relations is an alias' do + before do + @aliased_relation = @relation1.as(:alias) + end + + it "returns the alias of the relation containing the attribute" do + Join.new("INNER JOIN", @aliased_relation, @relation2, @predicate).prefix_for(@aliased_relation[:id]) \ + .should == @aliased_relation.alias + Join.new("INNER JOIN", @aliased_relation, @relation2, @predicate).prefix_for(@relation2[:id]) \ + .should == @relation2.prefix_for(@relation2[:id]) + end end end @@ -77,10 +103,10 @@ module ActiveRelation describe 'with aggregated relations' do before do - @aggregation = @relation2 \ + @aggregation = @relation2 \ .aggregate(@relation2[:user_id], @relation2[:id].count) \ - .group(@relation2[:user_id]) \ - .rename(@relation2[:id].count, :cnt) \ + .group(@relation2[:user_id]) \ + .rename(@relation2[:id].count, :cnt) \ .as('photo_count') end diff --git a/spec/active_relation/unit/relations/order_spec.rb b/spec/active_relation/unit/relations/order_spec.rb index 032bd2b40f..6a9ce07024 100644 --- a/spec/active_relation/unit/relations/order_spec.rb +++ b/spec/active_relation/unit/relations/order_spec.rb @@ -15,7 +15,7 @@ module ActiveRelation end describe '#descend' do - it "distributes over the relation and attributes" do + it "distributes a block over the relation and attributes" do Order.new(@relation, @attribute).descend(&:qualify). \ should == Order.new(@relation.descend(&:qualify), @attribute.qualify) end diff --git a/spec/active_relation/unit/relations/projection_spec.rb b/spec/active_relation/unit/relations/projection_spec.rb index 01a4d74bc6..78766d3308 100644 --- a/spec/active_relation/unit/relations/projection_spec.rb +++ b/spec/active_relation/unit/relations/projection_spec.rb @@ -31,14 +31,14 @@ module ActiveRelation end describe '#qualify' do - it "distributes over the relation and attributes" do + it "descends" do Projection.new(@relation, @attribute).qualify. \ should == Projection.new(@relation, @attribute).descend(&:qualify) end end describe '#descend' do - it "distributes over the relation and attributes" do + it "distributes a block over the relation and attributes" do Projection.new(@relation, @attribute).descend(&:qualify). \ should == Projection.new(@relation.descend(&:qualify), @attribute.qualify) end @@ -52,7 +52,7 @@ module ActiveRelation ") end - it "manufactures sql with scalar selects" do + it "manufactures sql with value selects" do Projection.new(@relation, Projection.new(@relation, @relation[:name])).to_sql.should be_like(" SELECT (SELECT `users`.`name` FROM `users`) FROM `users` ") diff --git a/spec/active_relation/unit/relations/range_spec.rb b/spec/active_relation/unit/relations/range_spec.rb index 7be2ca1b03..a0207c7342 100644 --- a/spec/active_relation/unit/relations/range_spec.rb +++ b/spec/active_relation/unit/relations/range_spec.rb @@ -14,7 +14,7 @@ module ActiveRelation end describe '#descend' do - it "distributes over the relation" do + it "distributes a block over the relation" do Range.new(@relation, @range).descend(&:qualify).should == Range.new(@relation.descend(&:qualify), @range) end end diff --git a/spec/active_relation/unit/relations/relation_spec.rb b/spec/active_relation/unit/relations/relation_spec.rb index 8f35760801..b562ec2a2a 100644 --- a/spec/active_relation/unit/relations/relation_spec.rb +++ b/spec/active_relation/unit/relations/relation_spec.rb @@ -69,7 +69,7 @@ module ActiveRelation describe '#as' do it "manufactures an alias relation" do - @relation.as(:thucydides).should == Alias.new(@relation, :thucydides) + @relation.as(:paul).should == Alias.new(@relation, :paul) end end @@ -89,7 +89,7 @@ module ActiveRelation end it "accepts arbitrary strings" do - @relation.select("arbitrary").should == Selection.new(@relation, Scalar.new("arbitrary", @relation)) + @relation.select("arbitrary").should == Selection.new(@relation, Value.new("arbitrary", @relation)) end end @@ -99,6 +99,14 @@ module ActiveRelation end end + describe '#call' do + it 'executes a select_all on the connection' do + mock(connection = Object.new).select_all(@relation.to_sql) + @relation.call(connection) + end + end + + describe '#aggregate' do before do @expression1 = @attribute1.sum @@ -137,7 +145,7 @@ module ActiveRelation describe '#update' do it 'manufactures an update relation' do Session.start do - assignments = {@relation[:name] => Scalar.new('bob', @relation)} + assignments = {@relation[:name] => Value.new('bob', @relation)} mock(Session.new).update(Update.new(@relation, assignments.bind(@relation))) @relation.update(assignments).should == @relation end @@ -148,7 +156,11 @@ module ActiveRelation describe Relation::Enumerable do it "is enumerable" do - pending + pending "I don't like this mock-based test" + data = [1,2,3] + mock.instance_of(Session).read(anything) { data } + @relation.collect.should == data + @relation.first.should == data.first end end end diff --git a/spec/active_relation/unit/relations/rename_spec.rb b/spec/active_relation/unit/relations/rename_spec.rb index c960f76736..9a63c4fc80 100644 --- a/spec/active_relation/unit/relations/rename_spec.rb +++ b/spec/active_relation/unit/relations/rename_spec.rb @@ -46,7 +46,7 @@ module ActiveRelation end describe '#descend' do - it "distributes over the relation and renames" do + it "distributes a block over the relation and renames" do Rename.new(@relation, @relation[:id] => :schmid).descend(&:qualify). \ should == Rename.new(@relation.descend(&:qualify), @relation[:id].qualify => :schmid) end diff --git a/spec/active_relation/unit/relations/selection_spec.rb b/spec/active_relation/unit/relations/selection_spec.rb index d5e0c6a9f6..4bb3817bf5 100644 --- a/spec/active_relation/unit/relations/selection_spec.rb +++ b/spec/active_relation/unit/relations/selection_spec.rb @@ -23,7 +23,7 @@ module ActiveRelation end describe '#descend' do - it "distributes over the relation and predicates" do + it "distributes a block over the relation and predicates" do Selection.new(@relation, @predicate).descend(&:qualify). \ should == Selection.new(@relation.descend(&:qualify), @predicate.descend(&:qualify)) end diff --git a/spec/active_relation/unit/relations/table_spec.rb b/spec/active_relation/unit/relations/table_spec.rb index 95ad7133db..6286ea9de1 100644 --- a/spec/active_relation/unit/relations/table_spec.rb +++ b/spec/active_relation/unit/relations/table_spec.rb @@ -65,6 +65,14 @@ module ActiveRelation Attribute.new(@relation, :name) ] end + + describe '#reset' do + it "reloads columns from the database" do + pending + lambda { stub(@relation.engine).columns { [] } }.should_not change { @relation.attributes } + lambda { @relation.reset }.should change { @relation.attributes } + end + end end describe '#qualify' do @@ -75,9 +83,8 @@ module ActiveRelation describe 'hashing' do it "implements hash equality" do - hash = {} - hash[Table.new(:users)] = 1 - hash[Table.new(:users)].should == 1 + 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 @@ -91,11 +98,5 @@ module ActiveRelation Table.new(:users, engine = Engine.new).engine.should == engine end end - - describe '#reset' do - it "" do - pending - end - end end end
\ No newline at end of file diff --git a/spec/active_relation/unit/relations/update_spec.rb b/spec/active_relation/unit/relations/update_spec.rb index cad14fd5ec..848760de83 100644 --- a/spec/active_relation/unit/relations/update_spec.rb +++ b/spec/active_relation/unit/relations/update_spec.rb @@ -22,5 +22,14 @@ module ActiveRelation ") end end + + describe '#call' do + it 'executes an update on the connection' do + update = Update.new(@relation, @relation[:name] => "nick".bind(@relation)) + mock(connection = Object.new).update(update.to_sql) + update.call(connection) + end + end + end end
\ No newline at end of file diff --git a/spec/active_relation/unit/session/session_spec.rb b/spec/active_relation/unit/session/session_spec.rb index 89d96ef323..9fba6cc6b2 100644 --- a/spec/active_relation/unit/session/session_spec.rb +++ b/spec/active_relation/unit/session/session_spec.rb @@ -35,39 +35,39 @@ module ActiveRelation @insert = Insertion.new(@relation, @relation[:name] => 'nick'.bind(@relation)) @update = Update.new(@relation, @relation[:name] => 'nick'.bind(@relation)) @delete = Deletion.new(@relation) - @select = @relation + @read = @relation end describe '#create' do it "executes an insertion on the connection" do - mock(@insert.engine).insert(@insert.to_sql) + mock(@insert).call(@insert.engine.connection) @session.create(@insert) end end describe '#read' do it "executes an selection on the connection" do - mock(@select.engine).select_all(@select.to_sql).once - @session.read(@select) + mock(@read).call(@read.engine.connection) + @session.read(@read) end it "is memoized" do - mock(@select.engine).select_all(@select.to_sql).once - @session.read(@select) - @session.read(@select) + mock(@read).call(@read.engine.connection).once + @session.read(@read) + @session.read(@read) end end describe '#update' do it "executes an update on the connection" do - mock(@update.engine).update(@update.to_sql) + mock(@update).call(@update.engine.connection) @session.update(@update) end end describe '#delete' do it "executes a delete on the connection" do - mock(@delete.engine).delete(@delete.to_sql) + mock(@delete).call(@delete.engine.connection) @session.delete(@delete) end end diff --git a/spec/matchers/hash_the_same_as.rb b/spec/matchers/hash_the_same_as.rb new file mode 100644 index 0000000000..86e98f31a3 --- /dev/null +++ b/spec/matchers/hash_the_same_as.rb @@ -0,0 +1,26 @@ +module HashTheSameAsMatcher + class HashTheSameAs + def initialize(expected) + @expected = expected + end + + def matches?(target) + @target = target + hash = {} + hash[@expected] = :some_arbitrary_value + hash[@target] == :some_arbitrary_value + end + + def failure_message + "expected #{@target} to hash the same as #{@expected}; they must be `eql?` and have the same `#hash` value" + end + + def negative_failure_message + "expected #{@target} 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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cd9c8e96cb..01a703a7d4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,9 +26,9 @@ class Hash end Spec::Runner.configure do |config| - config.include(BeLikeMatcher) + config.include(BeLikeMatcher, HashTheSameAsMatcher) config.mock_with :rr config.before do - ActiveRelation::Table.engine = ActiveRecord::Base.connection + ActiveRelation::Table.engine = ActiveRelation::Engine.new(ActiveRecord::Base) end end
\ No newline at end of file |