aboutsummaryrefslogtreecommitdiffstats
path: root/spec/active_relation
diff options
context:
space:
mode:
authorNick Kallen <nkallen@nick-kallens-computer-2.local>2008-01-07 18:37:20 -0800
committerNick Kallen <nkallen@nick-kallens-computer-2.local>2008-01-07 18:37:20 -0800
commit311f5f8eb588d4cde051762ace87a61425300bec (patch)
tree5e087ddd6257ad793c225555a6e48ad17bbdde70 /spec/active_relation
parentd43a4e9fc1316fc9eb8ff087c52c7ca7a475c041 (diff)
downloadrails-311f5f8eb588d4cde051762ace87a61425300bec.tar.gz
rails-311f5f8eb588d4cde051762ace87a61425300bec.tar.bz2
rails-311f5f8eb588d4cde051762ace87a61425300bec.zip
minor
Diffstat (limited to 'spec/active_relation')
-rw-r--r--spec/active_relation/integration/scratch_spec.rb271
-rw-r--r--spec/active_relation/predicates/binary_predicate_spec.rb51
-rw-r--r--spec/active_relation/predicates/equality_predicate_spec.rb25
-rw-r--r--spec/active_relation/predicates/relation_inclusion_predicate_spec.rb16
-rw-r--r--spec/active_relation/relations/attribute_spec.rb90
-rw-r--r--spec/active_relation/relations/deletion_relation_spec.rb22
-rw-r--r--spec/active_relation/relations/insertion_relation_spec.rb37
-rw-r--r--spec/active_relation/relations/join_operation_spec.rb39
-rw-r--r--spec/active_relation/relations/join_relation_spec.rb59
-rw-r--r--spec/active_relation/relations/order_relation_spec.rb41
-rw-r--r--spec/active_relation/relations/projection_relation_spec.rb36
-rw-r--r--spec/active_relation/relations/range_relation_spec.rb41
-rw-r--r--spec/active_relation/relations/relation_spec.rb92
-rw-r--r--spec/active_relation/relations/rename_relation_spec.rb64
-rw-r--r--spec/active_relation/relations/selection_relation_spec.rb53
-rw-r--r--spec/active_relation/relations/table_relation_spec.rb33
-rw-r--r--spec/active_relation/sql_builder/conditions_spec.rb18
-rw-r--r--spec/active_relation/sql_builder/delete_builder_spec.rb22
-rw-r--r--spec/active_relation/sql_builder/insert_builder_spec.rb24
-rw-r--r--spec/active_relation/sql_builder/select_builder_spec.rb148
20 files changed, 1182 insertions, 0 deletions
diff --git a/spec/active_relation/integration/scratch_spec.rb b/spec/active_relation/integration/scratch_spec.rb
new file mode 100644
index 0000000000..fba587e4ba
--- /dev/null
+++ b/spec/active_relation/integration/scratch_spec.rb
@@ -0,0 +1,271 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe 'ActiveRelation', 'A proposed refactoring to ActiveRecord, introducing both a SQL
+ Builder and a Relational Algebra to mediate between
+ ActiveRecord and the database. The goal of the refactoring is
+ to remove code duplication concerning AR associations; remove
+ complexity surrounding eager loading; comprehensively solve
+ quoting issues; remove the with_scope merging logic; minimize
+ the need for with_scope in general; simplify the
+ implementation of plugins like HasFinder and ActsAsParanoid;
+ introduce an identity map; and allow for query optimization.
+ All this while remaining backwards-compatible with the
+ existing ActiveRecord interface.
+ The Relational Algebra makes these ambitious goals
+ possible. There\'s no need to be scared by the math, it\'s
+ actually quite simple. Relational Algebras have some nice
+ advantages over flexible SQL builders like Sequel and and
+ SqlAlchemy (a beautiful Python library). Principally, a
+ relation is writable as well as readable. This obviates the
+ :create with_scope, and perhaps also
+ #set_belongs_to_association_for.
+ With so much complexity removed from ActiveRecord, I
+ propose a mild reconsideration of the architecture of Base,
+ AssocationProxy, AssociationCollection, and so forth. These
+ should all be understood as \'Repositories\': a factory that
+ given a relation can manufacture objects, and given an object
+ can manipulate a relation. This may sound trivial, but I
+ think it has the potential to make the code smaller and
+ more consistent.' do
+ before do
+ class User < ActiveRecord::Base; has_many :photos end
+ class Photo < ActiveRecord::Base; belongs_to :camera end
+ class Camera < ActiveRecord::Base; end
+ end
+
+ before do
+ # Rather than being associated with a table, an ActiveRecord is now associated with
+ # a relation.
+ @users = User.relation
+ @photos = Photo.relation
+ @cameras = Camera.relation
+ # A first taste of a Relational Algebra: User.find(1)
+ @user = @users.select(@users[:id] == 1)
+ # == is overridden on attributes to return a predicate, not true or false
+ end
+
+ # In a Relational Algebra, the various ActiveRecord associations become a simple
+ # mapping from one relation to another. The Reflection object parameterizes the
+ # mapping.
+ def user_has_many_photos(user_relation)
+ primary_key = User.reflections[:photos].klass.primary_key.to_sym
+ foreign_key = User.reflections[:photos].primary_key_name.to_sym
+
+ # << is the left outer join operator
+ (user_relation << @photos).on(user_relation[primary_key] == @photos[foreign_key])
+ end
+
+ def photo_belongs_to_camera(photo_relation)
+ primary_key = Photo.reflections[:camera].klass.primary_key.to_sym
+ foreign_key = Photo.reflections[:camera].primary_key_name.to_sym
+
+ (photo_relation << @cameras).on(photo_relation[foreign_key] == @cameras[primary_key])
+ end
+
+ describe 'Relational Algebra', 'a relational algebra allows the implementation of
+ associations like has_many to be specified once,
+ regardless of eager-joins, has_many :through, and so
+ forth' do
+ it 'generates the query for User.has_many :photos' do
+ user_photos = user_has_many_photos(@user)
+ # the 'project' operator limits the columns that come back from the query.
+ # Note how all the operators are compositional: 'project' is applied to a query
+ # that previously had been joined and selected.
+ user_photos.project(*@photos.attributes).to_s.should be_like("""
+ SELECT `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id`
+ FROM `users`
+ LEFT OUTER JOIN `photos`
+ ON `users`.`id` = `photos`.`user_id`
+ WHERE
+ `users`.`id` = 1
+ """)
+ # Also note the correctly quoted columns and tables. In this instance the
+ # MysqlAdapter from ActiveRecord is used to do the escaping.
+ end
+
+ it 'generates the query for User.has_many :cameras :through => :photos' do
+ # note, again, the compositionality of the operators:
+ user_cameras = photo_belongs_to_camera(user_has_many_photos(@user))
+ user_cameras.project(*@cameras.attributes).to_s.should be_like("""
+ SELECT `cameras`.`id`
+ FROM `users`
+ LEFT OUTER JOIN `photos`
+ ON `users`.`id` = `photos`.`user_id`
+ LEFT OUTER JOIN `cameras`
+ ON `photos`.`camera_id` = `cameras`.`id`
+ WHERE
+ `users`.`id` = 1
+ """)
+ end
+
+ it 'generates the query for an eager join for a collection using the same logic as
+ for an association on an individual row' do
+ users_cameras = photo_belongs_to_camera(user_has_many_photos(@users))
+ users_cameras.to_s.should be_like("""
+ SELECT `users`.`name`, `users`.`id`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id`, `cameras`.`id`
+ FROM `users`
+ LEFT OUTER JOIN `photos`
+ ON `users`.`id` = `photos`.`user_id`
+ LEFT OUTER JOIN `cameras`
+ ON `photos`.`camera_id` = `cameras`.`id`
+ """)
+ end
+
+ it 'is trivial to disambiguate columns' do
+ users_cameras = photo_belongs_to_camera(user_has_many_photos(@users)).qualify
+ users_cameras.to_s.should be_like("""
+ SELECT `users`.`name` AS 'users.name', `users`.`id` AS 'users.id', `photos`.`id` AS 'photos.id', `photos`.`user_id` AS 'photos.user_id', `photos`.`camera_id` AS 'photos.camera_id', `cameras`.`id` AS 'cameras.id'
+ FROM `users`
+ LEFT OUTER JOIN `photos`
+ ON `users`.`id` = `photos`.`user_id`
+ LEFT OUTER JOIN `cameras`
+ ON `photos`.`camera_id` = `cameras`.`id`
+ """)
+ end
+
+ it 'allows arbitrary sql to be passed through' do
+ (@users << @photos).on("asdf").to_s.should be_like("""
+ SELECT `users`.`name`, `users`.`id`, `photos`.`id`, `photos`.`user_id`, `photos`.`camera_id`
+ FROM `users`
+ LEFT OUTER JOIN `photos`
+ ON asdf
+ """)
+ @users.select("asdf").to_s.should be_like("""
+ SELECT `users`.`name`, `users`.`id`
+ FROM `users`
+ WHERE asdf
+ """)
+ end
+
+ describe 'write operations' do
+ it 'generates the query for user.destroy' do
+ @user.delete.to_s.should be_like("""
+ DELETE
+ FROM `users`
+ WHERE `users`.`id` = 1
+ """)
+ end
+
+ it 'generates an efficient query for two User.creates -- UnitOfWork is within reach!' do
+ @users.insert(@users[:name] => "humpty").insert(@users[:name] => "dumpty").to_s.should be_like("""
+ INSERT
+ INTO `users`
+ (`users`.`name`) VALUES ('humpty'), ('dumpty')
+ """)
+ end
+ end
+
+ describe 'with_scope' do
+ it 'obviates the need for with_scope merging logic since, e.g.,
+ `with_scope :conditions => ...` is just a #select operation on the relation' do
+ end
+
+ it 'may eliminate the need for with_scope altogether since the associations no longer
+ need it: the relation underlying the association fully encapsulates the scope' do
+ end
+ end
+ end
+
+ describe 'Repository', 'ActiveRecord::Base, HasManyAssociation, and so forth are
+ all repositories: given a relation, they manufacture objects' do
+ before do
+ class << ActiveRecord::Base; public :instantiate end
+ end
+
+ it 'manufactures objects' do
+ User.instantiate(@users.first).attributes.should == {"name" => "hai", "id" => 1}
+ end
+
+ it 'frees ActiveRecords from being tied to tables' do
+ pending # pending, but trivial to implement:
+
+ class User < ActiveRecord::Base
+ # acts_as_paranoid without alias_method_chain:
+ set_relation @users.select(@users[:deleted_at] != nil)
+ end
+
+ class Person < ActiveRecord::Base
+ set_relation @accounts.join(@profiles).on(@accounts[:id] == @profiles[:account_id])
+ end
+ # I know this sounds crazy, but even writes are possible in the last example.
+ # calling #save on a person can write to two tables!
+ end
+
+ describe 'the n+1 problem' do
+ describe 'the eager join algorithm is vastly simpler' do
+ it 'loads three active records with only one query' do
+ # using 'rr' mocking framework: the real #select_all is called, but we assert
+ # that it only happens once:
+ mock.proxy(ActiveRecord::Base.connection).select_all.with_any_args.once
+
+ users_cameras = photo_belongs_to_camera(user_has_many_photos(@users)).qualify
+ user = User.instantiate(users_cameras.first, [:photos => [:camera]])
+ user.photos.first.camera.attributes.should == {"id" => 1}
+ end
+
+ before do
+ class << ActiveRecord::Base
+ # An identity map makes this algorithm efficient.
+ def instantiate_with_cache(record)
+ cache.get(record) { instantiate_without_cache(record) }
+ end
+ alias_method_chain :instantiate, :cache
+
+ # for each row in the result set, which may contain data from n tables,
+ # - instantiate that slice of the data corresponding to the current class
+ # - recusively walk the dependency chain and repeat.
+ def instantiate_with_joins(data, joins = [])
+ record = unqualify(data)
+ returning instantiate_without_joins(record) do |object|
+ joins.each do |join|
+ case join
+ when Symbol
+ object.send(association = join).instantiate(data)
+ when Hash
+ join.each do |association, nested_associations|
+ object.send(association).instantiate(data, nested_associations)
+ end
+ end
+ end
+ end
+ end
+ alias_method_chain :instantiate, :joins
+
+ private
+ # Sometimes, attributes are qualified to remove ambiguity. Here, bring back
+ # ambiguity by translating 'users.id' to 'id' so we can call #attributes=.
+ # This code should work correctly if the attributes are qualified or not.
+ def unqualify(qualified_attributes)
+ qualified_attributes_for_this_class = qualified_attributes. \
+ slice(*relation.attributes.collect(&:qualified_name))
+ qualified_attributes_for_this_class.alias do |qualified_name|
+ qualified_name.split('.')[1] || qualified_name # the latter means it must not really be qualified
+ end
+ end
+ end
+ end
+
+ it "is possible to be smarter about eager loading. DataMapper is smart enough
+ to notice when you do users.each { |u| u.photos } and make this two queries
+ rather than n+1: the first invocation of #photos is lazy but it preloads
+ photos for all subsequent users. This is substantially easier with the
+ Algebra since we can do @user.join(@photos).on(...) and transform that to
+ @users.join(@photos).on(...), relying on the IdentityMap to eliminate
+ the n+1 problem." do
+ pending
+ end
+ end
+ end
+ end
+
+ describe 'The Architecture', 'I propose to produce a new gem, ActiveRelation, which encaplulates
+ the existing ActiveRecord Connection Adapter, the new SQL Builder,
+ and the Relational Algebra. ActiveRecord, then, should no longer
+ interact with the connection object directly.' do
+ end
+
+ describe 'Miscellaneous Ideas' do
+ it 'may be easy to write a SQL parser that can take arbitrary SQL and produce a relation.
+ This has the advantage of permitting e.g., pagination with custom finder_sql'
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/predicates/binary_predicate_spec.rb b/spec/active_relation/predicates/binary_predicate_spec.rb
new file mode 100644
index 0000000000..5de559df41
--- /dev/null
+++ b/spec/active_relation/predicates/binary_predicate_spec.rb
@@ -0,0 +1,51 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe BinaryPredicate do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ @attribute1 = Attribute.new(@relation1, :name1)
+ @attribute2 = Attribute.new(@relation2, :name2)
+ class ConcreteBinaryPredicate < BinaryPredicate
+ def predicate_name
+ :equals
+ end
+ end
+ end
+
+ describe '#initialize' do
+ it "requires that both columns come from the same relation" do
+ pending
+ end
+ end
+
+ describe '==' do
+ it "obtains if attribute1 and attribute2 are identical" do
+ BinaryPredicate.new(@attribute1, @attribute2).should == BinaryPredicate.new(@attribute1, @attribute2)
+ BinaryPredicate.new(@attribute1, @attribute2).should_not == BinaryPredicate.new(@attribute1, @attribute1)
+ end
+
+ it "obtains if the concrete type of the BinaryPredicates are identical" do
+ ConcreteBinaryPredicate.new(@attribute1, @attribute2).should == ConcreteBinaryPredicate.new(@attribute1, @attribute2)
+ BinaryPredicate.new(@attribute1, @attribute2).should_not == ConcreteBinaryPredicate.new(@attribute1, @attribute2)
+ end
+ end
+
+ describe '#qualify' do
+ it "distributes over the predicates and attributes" do
+ ConcreteBinaryPredicate.new(@attribute1, @attribute2).qualify. \
+ should == ConcreteBinaryPredicate.new(@attribute1.qualify, @attribute2.qualify)
+ end
+ end
+
+ describe '#to_sql' do
+ it 'manufactures correct sql' do
+ ConcreteBinaryPredicate.new(@attribute1, @attribute2).to_sql.should == ConditionsBuilder.new do
+ equals do
+ column :foo, :name1
+ column :bar, :name2
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/predicates/equality_predicate_spec.rb b/spec/active_relation/predicates/equality_predicate_spec.rb
new file mode 100644
index 0000000000..af43b754e0
--- /dev/null
+++ b/spec/active_relation/predicates/equality_predicate_spec.rb
@@ -0,0 +1,25 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe EqualityPredicate do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ @attribute1 = Attribute.new(@relation1, :name)
+ @attribute2 = Attribute.new(@relation2, :name)
+ end
+
+ describe '==' do
+ it "obtains if attribute1 and attribute2 are identical" do
+ EqualityPredicate.new(@attribute1, @attribute2).should == EqualityPredicate.new(@attribute1, @attribute2)
+ EqualityPredicate.new(@attribute1, @attribute2).should_not == EqualityPredicate.new(@attribute1, @attribute1)
+ end
+
+ it "obtains if the concrete type of the predicates are identical" do
+ EqualityPredicate.new(@attribute1, @attribute2).should_not == BinaryPredicate.new(@attribute1, @attribute2)
+ end
+
+ it "is commutative on the attributes" do
+ EqualityPredicate.new(@attribute1, @attribute2).should == EqualityPredicate.new(@attribute2, @attribute1)
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/predicates/relation_inclusion_predicate_spec.rb b/spec/active_relation/predicates/relation_inclusion_predicate_spec.rb
new file mode 100644
index 0000000000..f8c911429b
--- /dev/null
+++ b/spec/active_relation/predicates/relation_inclusion_predicate_spec.rb
@@ -0,0 +1,16 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe RelationInclusionPredicate do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ @attribute = @relation1[:baz]
+ end
+
+ describe RelationInclusionPredicate, '==' do
+ it "obtains if attribute1 and attribute2 are identical" do
+ RelationInclusionPredicate.new(@attribute, @relation1).should == RelationInclusionPredicate.new(@attribute, @relation1)
+ RelationInclusionPredicate.new(@attribute, @relation1).should_not == RelationInclusionPredicate.new(@attribute, @relation2)
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/attribute_spec.rb b/spec/active_relation/relations/attribute_spec.rb
new file mode 100644
index 0000000000..ddfc22fe28
--- /dev/null
+++ b/spec/active_relation/relations/attribute_spec.rb
@@ -0,0 +1,90 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe Attribute do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ end
+
+ describe '#aliazz' do
+ it "manufactures an aliased attributed" do
+ pending
+ end
+
+ it "should be renamed to #alias!" do
+ pending
+ @relation1.alias
+ end
+ end
+
+ describe '#qualified_name' do
+ it "manufactures an attribute name prefixed with the relation's name" do
+ @relation1[:id].qualified_name.should == 'foo.id'
+ end
+ end
+
+ describe '#qualify' do
+ it "manufactures an attribute aliased with that attributes qualified name" do
+ @relation1[:id].qualify == @relation1[:id].qualify
+ end
+ end
+
+ describe '#eql?' do
+ it "obtains if the relation and attribute name are identical" do
+ Attribute.new(@relation1, :name).should be_eql(Attribute.new(@relation1, :name))
+ Attribute.new(@relation1, :name).should_not be_eql(Attribute.new(@relation1, :another_name))
+ Attribute.new(@relation1, :name).should_not be_eql(Attribute.new(@relation2, :name))
+ end
+ end
+
+ describe 'predications' do
+ before do
+ @attribute1 = Attribute.new(@relation1, :name)
+ @attribute2 = Attribute.new(@relation2, :name)
+ end
+
+ describe '==' do
+ it "manufactures an equality predicate" do
+ (@attribute1 == @attribute2).should == EqualityPredicate.new(@attribute1, @attribute2)
+ end
+ end
+
+ describe '<' do
+ it "manufactures a less-than predicate" do
+ (@attribute1 < @attribute2).should == LessThanPredicate.new(@attribute1, @attribute2)
+ end
+ end
+
+ describe '<=' do
+ it "manufactures a less-than or equal-to predicate" do
+ (@attribute1 <= @attribute2).should == LessThanOrEqualToPredicate.new(@attribute1, @attribute2)
+ end
+ end
+
+ describe '>' do
+ it "manufactures a greater-than predicate" do
+ (@attribute1 > @attribute2).should == GreaterThanPredicate.new(@attribute1, @attribute2)
+ end
+ end
+
+ describe '>=' do
+ it "manufactures a greater-than or equal to predicate" do
+ (@attribute1 >= @attribute2).should == GreaterThanOrEqualToPredicate.new(@attribute1, @attribute2)
+ end
+ end
+
+ describe '=~' do
+ it "manufactures a match predicate" do
+ (@attribute1 =~ /.*/).should == MatchPredicate.new(@attribute1, @attribute2)
+ end
+ end
+ end
+
+ describe '#to_sql' do
+ it "manufactures a column" do
+ Attribute.new(@relation1, :name, :alias).to_sql.should == SelectsBuilder.new do
+ column :foo, :name, :alias
+ end
+ end
+ end
+end
diff --git a/spec/active_relation/relations/deletion_relation_spec.rb b/spec/active_relation/relations/deletion_relation_spec.rb
new file mode 100644
index 0000000000..4f75a261f4
--- /dev/null
+++ b/spec/active_relation/relations/deletion_relation_spec.rb
@@ -0,0 +1,22 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe DeletionRelation do
+ before do
+ @relation = TableRelation.new(:users)
+ end
+
+ describe '#to_sql' do
+ it 'manufactures sql deleting the relation' do
+ DeletionRelation.new(@relation.select(@relation[:id] == 1)).to_sql.to_s.should == DeleteBuilder.new do
+ delete
+ from :users
+ where do
+ equals do
+ column :users, :id
+ value 1
+ end
+ end
+ end.to_s
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/insertion_relation_spec.rb b/spec/active_relation/relations/insertion_relation_spec.rb
new file mode 100644
index 0000000000..6bafabb473
--- /dev/null
+++ b/spec/active_relation/relations/insertion_relation_spec.rb
@@ -0,0 +1,37 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe InsertionRelation do
+ before do
+ @relation = TableRelation.new(:users)
+ end
+
+ describe '#to_sql' do
+ it 'manufactures sql inserting the data for one item' do
+ InsertionRelation.new(@relation, @relation[:name] => "nick").to_sql.should == InsertBuilder.new do
+ insert
+ into :users
+ columns do
+ column :users, :name
+ end
+ values do
+ row "nick"
+ end
+ end
+ end
+
+ it 'manufactures sql inserting the data for multiple items' do
+ nested_insertion = InsertionRelation.new(@relation, @relation[:name] => "cobra")
+ InsertionRelation.new(nested_insertion, nested_insertion[:name] => "commander").to_sql.to_s.should == InsertBuilder.new do
+ insert
+ into :users
+ columns do
+ column :users, :name
+ end
+ values do
+ row "cobra"
+ row "commander"
+ end
+ end.to_s
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/join_operation_spec.rb b/spec/active_relation/relations/join_operation_spec.rb
new file mode 100644
index 0000000000..a8ab85123b
--- /dev/null
+++ b/spec/active_relation/relations/join_operation_spec.rb
@@ -0,0 +1,39 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe 'between two relations' do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ end
+
+ describe '==' do
+ it "obtains if the relations of both joins are identical" do
+ JoinOperation.new(@relation1, @relation2).should == JoinOperation.new(@relation1, @relation2)
+ JoinOperation.new(@relation1, @relation2).should_not == JoinOperation.new(@relation1, @relation1)
+ end
+
+ it "is commutative on the relations" do
+ JoinOperation.new(@relation1, @relation2).should == JoinOperation.new(@relation2, @relation1)
+ end
+ end
+
+ describe 'on' do
+ before do
+ @predicate = Predicate.new
+ @join_operation = JoinOperation.new(@relation1, @relation2)
+ class << @join_operation
+ def relation_class
+ JoinRelation
+ end
+ end
+ end
+
+ it "manufactures a join relation of the appropriate type" do
+ @join_operation.on(@predicate).should == JoinRelation.new(@relation1, @relation2, @predicate)
+ end
+
+ it "accepts arbitrary strings" do
+ @join_operation.on("arbitrary").should == JoinRelation.new(@relation1, @relation2, "arbitrary")
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/join_relation_spec.rb b/spec/active_relation/relations/join_relation_spec.rb
new file mode 100644
index 0000000000..d0be270837
--- /dev/null
+++ b/spec/active_relation/relations/join_relation_spec.rb
@@ -0,0 +1,59 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe JoinRelation do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ @predicate = EqualityPredicate.new(@relation1[:id], @relation2[:id])
+ end
+
+ describe '==' do
+ it 'obtains if the two relations and the predicate are identical' do
+ JoinRelation.new(@relation1, @relation2, @predicate).should == JoinRelation.new(@relation1, @relation2, @predicate)
+ JoinRelation.new(@relation1, @relation2, @predicate).should_not == JoinRelation.new(@relation1, @relation1, @predicate)
+ end
+
+ it 'is commutative on the relations' do
+ JoinRelation.new(@relation1, @relation2, @predicate).should == JoinRelation.new(@relation2, @relation1, @predicate)
+ end
+ end
+
+ describe '#qualify' do
+ it 'distributes over the relations and predicates' do
+ InnerJoinRelation.new(@relation1, @relation2, @predicate).qualify. \
+ should == InnerJoinRelation.new(@relation1.qualify, @relation2.qualify, @predicate.qualify)
+ end
+ end
+
+ describe '#to_sql' do
+ before do
+ @relation1 = @relation1.select(@relation1[:id] == @relation2[:foo_id])
+ end
+
+ it 'manufactures sql joining the two tables on the predicate, merging the selects' do
+ InnerJoinRelation.new(@relation1, @relation2, @predicate).to_s.should == SelectBuilder.new do
+ select do
+ column :foo, :name
+ column :foo, :id
+ column :bar, :name
+ column :bar, :foo_id
+ column :bar, :id
+ end
+ from :foo do
+ inner_join :bar do
+ equals do
+ column :foo, :id
+ column :bar, :id
+ end
+ end
+ end
+ where do
+ equals do
+ column :foo, :id
+ column :bar, :foo_id
+ end
+ end
+ end.to_s
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/order_relation_spec.rb b/spec/active_relation/relations/order_relation_spec.rb
new file mode 100644
index 0000000000..17f730b564
--- /dev/null
+++ b/spec/active_relation/relations/order_relation_spec.rb
@@ -0,0 +1,41 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe OrderRelation do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ @attribute1 = @relation1[:id]
+ @attribute2 = @relation2[:id]
+ end
+
+ describe '==' do
+ it "obtains if the relation and attributes are identical" do
+ OrderRelation.new(@relation1, @attribute1, @attribute2).should == OrderRelation.new(@relation1, @attribute1, @attribute2)
+ OrderRelation.new(@relation1, @attribute1).should_not == OrderRelation.new(@relation2, @attribute1)
+ OrderRelation.new(@relation1, @attribute1, @attribute2).should_not == OrderRelation.new(@relation1, @attribute2, @attribute1)
+ end
+ end
+
+ describe '#qualify' do
+ it "distributes over the relation and attributes" do
+ OrderRelation.new(@relation1, @attribute1).qualify. \
+ should == OrderRelation.new(@relation1.qualify, @attribute1.qualify)
+ end
+ end
+
+ describe '#to_sql' do
+ it "manufactures sql with an order clause" do
+ OrderRelation.new(@relation1, @attribute1).to_s.should == SelectBuilder.new do
+ select do
+ column :foo, :name
+ column :foo, :id
+ end
+ from :foo
+ order_by do
+ column :foo, :id
+ end
+ end.to_s
+ end
+ end
+
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/projection_relation_spec.rb b/spec/active_relation/relations/projection_relation_spec.rb
new file mode 100644
index 0000000000..164c485761
--- /dev/null
+++ b/spec/active_relation/relations/projection_relation_spec.rb
@@ -0,0 +1,36 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe ProjectionRelation do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ @attribute1 = @relation1[:id]
+ @attribute2 = @relation2[:id]
+ end
+
+ describe '==' do
+ it "obtains if the relations and attributes are identical" do
+ ProjectionRelation.new(@relation1, @attribute1, @attribute2).should == ProjectionRelation.new(@relation1, @attribute1, @attribute2)
+ ProjectionRelation.new(@relation1, @attribute1).should_not == ProjectionRelation.new(@relation2, @attribute1)
+ ProjectionRelation.new(@relation1, @attribute1).should_not == ProjectionRelation.new(@relation1, @attribute2)
+ end
+ end
+
+ describe '#qualify' do
+ it "distributes over teh relation and attributes" do
+ ProjectionRelation.new(@relation1, @attribute1).qualify. \
+ should == ProjectionRelation.new(@relation1.qualify, @attribute1.qualify)
+ end
+ end
+
+ describe '#to_sql' do
+ it "manufactures sql with a limited select clause" do
+ ProjectionRelation.new(@relation1, @attribute1).to_s.should == SelectBuilder.new do
+ select do
+ column :foo, :id
+ end
+ from :foo
+ end.to_s
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/range_relation_spec.rb b/spec/active_relation/relations/range_relation_spec.rb
new file mode 100644
index 0000000000..ac9f887d9b
--- /dev/null
+++ b/spec/active_relation/relations/range_relation_spec.rb
@@ -0,0 +1,41 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe RangeRelation do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ @range1 = 1..2
+ @range2 = 4..9
+ end
+
+ describe '==' do
+ it "obtains if the relation and range are identical" do
+ RangeRelation.new(@relation1, @range1).should == RangeRelation.new(@relation1, @range1)
+ RangeRelation.new(@relation1, @range1).should_not == RangeRelation.new(@relation2, @range1)
+ RangeRelation.new(@relation1, @range1).should_not == RangeRelation.new(@relation1, @range2)
+ end
+ end
+
+ describe '#qualify' do
+ it "distributes over the relation and attributes" do
+ pending
+ end
+ end
+
+ describe '#to_sql' do
+ it "manufactures sql with limit and offset" do
+ range_size = @range2.last - @range2.first + 1
+ range_start = @range2.first
+ RangeRelation.new(@relation1, @range2).to_s.should == SelectBuilder.new do
+ select do
+ column :foo, :name
+ column :foo, :id
+ end
+ from :foo
+ limit range_size
+ offset range_start
+ end.to_s
+ end
+ end
+
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/relation_spec.rb b/spec/active_relation/relations/relation_spec.rb
new file mode 100644
index 0000000000..9ed42a98cb
--- /dev/null
+++ b/spec/active_relation/relations/relation_spec.rb
@@ -0,0 +1,92 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe Relation do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ @attribute1 = Attribute.new(@relation1, :id)
+ @attribute2 = Attribute.new(@relation1, :name)
+ end
+
+ describe '[]' do
+ it "manufactures an attribute when given a symbol" do
+ @relation1[:id].should be_eql(Attribute.new(@relation1, :id))
+ end
+
+ it "manufactures a range relation when given a range" do
+ @relation1[1..2].should == RangeRelation.new(@relation1, 1..2)
+ end
+ end
+
+ describe '#include?' do
+ it "manufactures an inclusion predicate" do
+ @relation1.include?(@attribute1).should == RelationInclusionPredicate.new(@attribute1, @relation1)
+ end
+ end
+
+ describe 'read operations' do
+ describe 'joins' do
+ describe '<=>' do
+ it "manufactures an inner join operation between those two relations" do
+ (@relation1 <=> @relation2).should == InnerJoinOperation.new(@relation1, @relation2)
+ end
+ end
+
+ describe '<<' do
+ it "manufactures a left outer join operation between those two relations" do
+ (@relation1 << @relation2).should == LeftOuterJoinOperation.new(@relation1, @relation2)
+ end
+ end
+ end
+
+ describe '#project' do
+ it "collapses identical projections" do
+ pending
+ end
+
+ it "manufactures a projection relation" do
+ @relation1.project(@attribute1, @attribute2).should == ProjectionRelation.new(@relation1, @attribute1, @attribute2)
+ end
+ end
+
+ describe '#rename' do
+ it "manufactures a rename relation" do
+ @relation1.rename(@attribute1, :foo).should == RenameRelation.new(@relation1, @attribute1 => :foo)
+ end
+ end
+
+ describe '#select' do
+ before do
+ @predicate = EqualityPredicate.new(@attribute1, @attribute2)
+ end
+
+ it "manufactures a selection relation" do
+ @relation1.select(@predicate).should == SelectionRelation.new(@relation1, @predicate)
+ end
+
+ it "accepts arbitrary strings" do
+ @relation1.select("arbitrary").should == SelectionRelation.new(@relation1, "arbitrary")
+ end
+ end
+
+ describe '#order' do
+ it "manufactures an order relation" do
+ @relation1.order(@attribute1, @attribute2).should == OrderRelation.new(@relation1, @attribute1, @attribute2)
+ end
+ end
+ end
+
+ describe 'write operations' do
+ describe '#delete' do
+ it 'manufactures a deletion relation' do
+ @relation1.delete.should == DeletionRelation.new(@relation1)
+ end
+ end
+
+ describe '#insert' do
+ it 'manufactures an insertion relation' do
+ @relation1.insert(tuple = {:id => 1}).should == InsertionRelation.new(@relation1, tuple)
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/rename_relation_spec.rb b/spec/active_relation/relations/rename_relation_spec.rb
new file mode 100644
index 0000000000..9b1d2d5cc8
--- /dev/null
+++ b/spec/active_relation/relations/rename_relation_spec.rb
@@ -0,0 +1,64 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe RenameRelation do
+ before do
+ @relation = TableRelation.new(:foo)
+ @renamed_relation = RenameRelation.new(@relation, @relation[:id] => :schmid)
+ end
+
+ describe '#initialize' do
+ it "manufactures nested rename relations if multiple renames are provided" do
+ RenameRelation.new(@relation, @relation[:id] => :humpty, @relation[:name] => :dumpty). \
+ should == RenameRelation.new(RenameRelation.new(@relation, @relation[:id] => :humpty), @relation[:name] => :dumpty)
+ end
+
+ it "raises an exception if the alias provided is already used" do
+ pending
+ end
+ end
+
+ describe '==' do
+ it "obtains if the relation, attribute, and alias are identical" do
+ pending
+ end
+ end
+
+ describe '#attributes' do
+ it "manufactures a list of attributes with the renamed attribute aliased" do
+ RenameRelation.new(@relation, @relation[:id] => :schmid).attributes.should ==
+ (@relation.attributes - [@relation[:id]]) + [@relation[:id].aliazz(:schmid)]
+ end
+ end
+
+ describe '[]' do
+ it 'indexes attributes by alias' do
+ @renamed_relation[:id].should be_nil
+ @renamed_relation[:schmid].should == @relation[:id]
+ end
+ end
+
+ describe '#schmattribute' do
+ it "should be renamed" do
+ pending
+ end
+ end
+
+ describe '#qualify' do
+ it "distributes over the relation and renames" do
+ RenameRelation.new(@relation, @relation[:id] => :schmid).qualify. \
+ should == RenameRelation.new(@relation.qualify, @relation[:id].qualify => :schmid)
+ end
+ end
+
+ describe '#to_sql' do
+ it 'manufactures sql aliasing the attribute' do
+ @renamed_relation.to_s.should == SelectBuilder.new do
+ select do
+ column :foo, :name
+ column :foo, :id, :schmid
+ end
+ from :foo
+ end.to_s
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/selection_relation_spec.rb b/spec/active_relation/relations/selection_relation_spec.rb
new file mode 100644
index 0000000000..c4aadc807b
--- /dev/null
+++ b/spec/active_relation/relations/selection_relation_spec.rb
@@ -0,0 +1,53 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe SelectionRelation do
+ before do
+ @relation1 = TableRelation.new(:foo)
+ @relation2 = TableRelation.new(:bar)
+ @predicate1 = EqualityPredicate.new(@relation1[:id], @relation2[:foo_id])
+ @predicate2 = LessThanPredicate.new(@relation1[:age], 2)
+ end
+
+ describe '==' do
+ it "obtains if both the predicate and the relation are identical" do
+ SelectionRelation.new(@relation1, @predicate1). \
+ should == SelectionRelation.new(@relation1, @predicate1)
+ SelectionRelation.new(@relation1, @predicate1). \
+ should_not == SelectionRelation.new(@relation2, @predicate1)
+ SelectionRelation.new(@relation1, @predicate1). \
+ should_not == SelectionRelation.new(@relation1, @predicate2)
+ end
+ end
+
+ describe '#initialize' do
+ it "manufactures nested selection relations if multiple predicates are provided" do
+ SelectionRelation.new(@relation1, @predicate1, @predicate2). \
+ should == SelectionRelation.new(SelectionRelation.new(@relation1, @predicate2), @predicate1)
+ end
+ end
+
+ describe '#qualify' do
+ it "distributes over the relation and predicates" do
+ SelectionRelation.new(@relation1, @predicate1).qualify. \
+ should == SelectionRelation.new(@relation1.qualify, @predicate1.qualify)
+ end
+ end
+
+ describe '#to_sql' do
+ it "manufactures sql with where clause conditions" do
+ SelectionRelation.new(@relation1, @predicate1).to_s.should == SelectBuilder.new do
+ select do
+ column :foo, :name
+ column :foo, :id
+ end
+ from :foo
+ where do
+ equals do
+ column :foo, :id
+ column :bar, :foo_id
+ end
+ end
+ end.to_s
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/relations/table_relation_spec.rb b/spec/active_relation/relations/table_relation_spec.rb
new file mode 100644
index 0000000000..c943fe6c92
--- /dev/null
+++ b/spec/active_relation/relations/table_relation_spec.rb
@@ -0,0 +1,33 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe TableRelation do
+ before do
+ @relation = TableRelation.new(:users)
+ end
+
+ describe '#to_sql' do
+ it "returns a simple SELECT query" do
+ @relation.to_sql.should == SelectBuilder.new do |s|
+ select do
+ column :users, :name
+ column :users, :id
+ end
+ from :users
+ end
+ end
+ end
+
+ describe '#attributes' do
+ it 'manufactures attributes corresponding to columns in the table' do
+ pending
+ end
+ end
+
+ describe '#qualify' do
+ it 'manufactures a rename relation with all attribute names qualified' do
+ @relation.qualify.should == RenameRelation.new(
+ RenameRelation.new(@relation, @relation[:id] => 'users.id'), @relation[:name] => 'users.name'
+ )
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/sql_builder/conditions_spec.rb b/spec/active_relation/sql_builder/conditions_spec.rb
new file mode 100644
index 0000000000..dc2d10a2f6
--- /dev/null
+++ b/spec/active_relation/sql_builder/conditions_spec.rb
@@ -0,0 +1,18 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe ConditionsBuilder do
+ describe '#to_s' do
+ describe 'with aliased columns' do
+ it 'manufactures correct sql' do
+ ConditionsBuilder.new do
+ equals do
+ column(:a, :b)
+ column(:c, :d, 'e')
+ end
+ end.to_s.should be_like("""
+ `a`.`b` = `c`.`d`
+ """)
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/sql_builder/delete_builder_spec.rb b/spec/active_relation/sql_builder/delete_builder_spec.rb
new file mode 100644
index 0000000000..fd62fde155
--- /dev/null
+++ b/spec/active_relation/sql_builder/delete_builder_spec.rb
@@ -0,0 +1,22 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe DeleteBuilder do
+ describe '#to_s' do
+ it 'manufactures correct sql' do
+ DeleteBuilder.new do
+ delete
+ from :users
+ where do
+ equals do
+ column :users, :id
+ value 1
+ end
+ end
+ end.to_s.should be_like("""
+ DELETE
+ FROM `users`
+ WHERE `users`.`id` = 1
+ """)
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/sql_builder/insert_builder_spec.rb b/spec/active_relation/sql_builder/insert_builder_spec.rb
new file mode 100644
index 0000000000..dddc971986
--- /dev/null
+++ b/spec/active_relation/sql_builder/insert_builder_spec.rb
@@ -0,0 +1,24 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe InsertBuilder do
+ describe '#to_s' do
+ it 'manufactures correct sql' do
+ InsertBuilder.new do
+ insert
+ into :users
+ columns do
+ column :users, :id
+ column :users, :name
+ end
+ values do
+ row 1, 'bob'
+ row 2, 'moe'
+ end
+ end.to_s.should be_like("""
+ INSERT
+ INTO `users`
+ (`users`.`id`, `users`.`name`) VALUES (1, 'bob'), (2, 'moe')
+ """)
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/active_relation/sql_builder/select_builder_spec.rb b/spec/active_relation/sql_builder/select_builder_spec.rb
new file mode 100644
index 0000000000..6539afe0c4
--- /dev/null
+++ b/spec/active_relation/sql_builder/select_builder_spec.rb
@@ -0,0 +1,148 @@
+require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
+
+describe SelectBuilder do
+ describe '#to_s' do
+ describe 'with select and from clauses' do
+ it 'manufactures correct sql' do
+ SelectBuilder.new do
+ select do
+ all
+ end
+ from :users
+ end.to_s.should be_like("""
+ SELECT *
+ FROM `users`
+ """)
+ end
+ end
+
+ describe 'with specified columns and column aliases' do
+ it 'manufactures correct sql' do
+ SelectBuilder.new do
+ select do
+ column :a, :b, 'c'
+ column :e, :f
+ end
+ from :users
+ end.to_s.should be_like("""
+ SELECT `a`.`b` AS 'c', `e`.`f`
+ FROM `users`
+ """)
+ end
+ end
+
+ describe 'with where clause' do
+ it 'manufactures correct sql' do
+ SelectBuilder.new do
+ select do
+ all
+ end
+ from :users
+ where do
+ equals do
+ value 1
+ column :b, :c
+ end
+ end
+ end.to_s.should be_like("""
+ SELECT *
+ FROM `users`
+ WHERE 1 = `b`.`c`
+ """)
+ end
+
+ it 'accepts arbitrary strings' do
+ SelectBuilder.new do
+ select do
+ all
+ end
+ from :users
+ where do
+ value "'a' = 'a'"
+ end
+ end.to_s.should be_like("""
+ SELECT *
+ FROM `users`
+ WHERE 'a' = 'a'
+ """)
+ end
+ end
+
+ describe 'with inner join' do
+ it 'manufactures correct sql' do
+ SelectBuilder.new do
+ select do
+ all
+ end
+ from :users do
+ inner_join(:friendships) do
+ equals do
+ column :users, :id
+ column :friendships, :user_id
+ end
+ end
+ end
+ end.to_s.should be_like("""
+ SELECT *
+ FROM `users`
+ INNER JOIN `friendships`
+ ON `users`.`id` = `friendships`.`user_id`
+ """)
+ end
+
+ it 'accepts arbitrary on strings' do
+ SelectBuilder.new do
+ select do
+ all
+ end
+ from :users do
+ inner_join :friendships do
+ value "arbitrary"
+ end
+ end
+ end.to_s.should be_like("""
+ SELECT *
+ FROM `users`
+ INNER JOIN `friendships` ON arbitrary
+ """)
+ end
+ end
+
+ describe 'with order' do
+ it 'manufactures correct sql' do
+ SelectBuilder.new do
+ select do
+ all
+ end
+ from :users
+ order_by do
+ column :users, :id
+ column :users, :created_at, 'alias'
+ end
+ end.to_s.should be_like("""
+ SELECT *
+ FROM `users`
+ ORDER BY `users`.`id`, `users`.`created_at`
+ """)
+ end
+ end
+
+ describe 'with limit and/or offset' do
+ it 'manufactures correct sql' do
+ SelectBuilder.new do
+ select do
+ all
+ end
+ from :users
+ limit 10
+ offset 10
+ end.to_s.should be_like("""
+ SELECT *
+ FROM `users`
+ LIMIT 10
+ OFFSET 10
+ """)
+ end
+ end
+ end
+end \ No newline at end of file