diff options
author | Nick Kallen <nkallen@nick-kallens-computer-2.local> | 2008-01-07 18:37:20 -0800 |
---|---|---|
committer | Nick Kallen <nkallen@nick-kallens-computer-2.local> | 2008-01-07 18:37:20 -0800 |
commit | 311f5f8eb588d4cde051762ace87a61425300bec (patch) | |
tree | 5e087ddd6257ad793c225555a6e48ad17bbdde70 /spec/active_relation | |
parent | d43a4e9fc1316fc9eb8ff087c52c7ca7a475c041 (diff) | |
download | rails-311f5f8eb588d4cde051762ace87a61425300bec.tar.gz rails-311f5f8eb588d4cde051762ace87a61425300bec.tar.bz2 rails-311f5f8eb588d4cde051762ace87a61425300bec.zip |
minor
Diffstat (limited to 'spec/active_relation')
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 |