From 10b3266038343cd4276df08e205a180fcace6001 Mon Sep 17 00:00:00 2001 From: Martin Kihlgren Date: Fri, 18 Apr 2008 13:54:40 +0200 Subject: [PATCH 1/3] Added better documentation for Resource.including_classes. Fixed the resource_spec to be more stable. --- lib/data_mapper/resource.rb | 7 +++++++ spec/unit/resource_spec.rb | 5 ++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/data_mapper/resource.rb b/lib/data_mapper/resource.rb index d9924045..010f9852 100644 --- a/lib/data_mapper/resource.rb +++ b/lib/data_mapper/resource.rb @@ -23,6 +23,13 @@ def self.included(base) @@including_classes << base end + # Return all classes that include the DataMapper::Resource module + # + # ==== Returns + # Set:: A Set containing the including classes + # + # - + # @public def self.including_classes @@including_classes end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index d2aeb77f..24ed4483 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -40,10 +40,9 @@ class Moon end it "should track the classes that include it" do - DataMapper::Resource.including_classes.should == Set.new([Vehicle, Article, Manufacturer, Planet, Supplier, Comment]) + DataMapper::Resource.including_classes.clear Moon.class_eval do include(DataMapper::Resource) end - DataMapper::Resource.including_classes.size.should == 7 - DataMapper::Resource.including_classes.should == Set.new([Vehicle, Article, Manufacturer, Planet, Supplier, Comment, Moon]) + DataMapper::Resource.including_classes.should == Set.new([Moon]) end it "should return an instance of the created object" do From 6971e491bd545439d5d6f50061d805cb79801603 Mon Sep 17 00:00:00 2001 From: Martin Kihlgren Date: Fri, 18 Apr 2008 14:09:59 +0200 Subject: [PATCH 2/3] Added Resource::ClassMethods#find_by_sql. * If DataMapper::Adapters::DataObjectsAdapter is loaded, Resource::ClassMethods will get #find_by_sql defined. * #find_by_sql only works if the DataMapper::Repository has an adapter that is a DataMapper::Adapters::DataObjectsAdapter. * #find_by_sql takes a few different arguments structures, see documentation comment for details, among them the one familiar from ActiveRecord. --- .../adapters/data_objects_adapter.rb | 99 +++++++++++++++++-- .../adapters/data_objects_adapter_spec.rb | 82 +++++++++++++++ 2 files changed, 173 insertions(+), 8 deletions(-) diff --git a/lib/data_mapper/adapters/data_objects_adapter.rb b/lib/data_mapper/adapters/data_objects_adapter.rb index 0b5f609d..df8cee9d 100644 --- a/lib/data_mapper/adapters/data_objects_adapter.rb +++ b/lib/data_mapper/adapters/data_objects_adapter.rb @@ -9,6 +9,70 @@ module DataMapper + module Resource + + module ClassMethods + # + # Find instances by manually providing SQL + # + # ==== Parameters + # :: An SQL query to execute + # :: An Array containing a String (being the SQL query to execute) and the parameters to the query. + # example: ["SELECT name FROM users WHERE id = ?", id] + # :: A prepared Query to execute. + # :: An options hash. + # + # ==== Options (the options hash) + # :repository:: The name of the repository to execute the query in. + # :reload:: Whether to reload any instances found that allready exist in the identity map. + # + # ==== Returns + # LoadedSet:: The instance matched by the query. + # + # - + # @public + def find_by_sql(*args) + sql = nil + query = nil + params = [] + properties = nil + do_reload = false + repository_name = default_repository_name + args.each do |arg| + if arg.is_a?(String) + sql = arg + elsif arg.is_a?(Array) + sql = arg.first + params = arg[1..-1] + elsif arg.is_a?(DataMapper::Query) + query = arg + elsif arg.is_a?(Hash) + repository_name = arg.delete(:repository) if arg.include?(:repository) + do_reload = arg.delete(:reload) if arg.include?(:reload) + raise "unknown options to #find_by_sql: #{arg.inspect}" unless arg.empty? + end + end + + the_repository = repository(repository_name) + raise "#find_by_sql only available for Repositories served by a DataObjectsAdapter" unless the_repository.adapter.is_a?(DataMapper::Adapters::DataObjectsAdapter) + + if query + sql = the_repository.adapter.query_read_statement(query) + params = query.fields + end + + raise "#find_by_sql requires a query of some kind to work" unless sql + + properties ||= self.properties + + DataMapper.logger.debug { "FIND_BY_SQL: #{sql} PARAMETERS: #{params.inspect}" } + + repository.adapter.read_set_with_sql(repository, self, properties, sql, params, do_reload) + end + end + + end + module Adapters # You must inherit from the DoAdapter, and implement the @@ -155,15 +219,24 @@ def delete(repository, resource) affected_rows == 1 end - # Methods dealing with finding stuff by some query parameters - def read_set(repository, query) - properties = query.fields - + # + # used by find_by_sql and read_set + # + # ==== Parameters + # repository:: The repository to read from. + # model:: The class of the instances to read. + # properties:: The properties to read. Must contain Symbols, Strings or DM::Properties. + # sql:: The query to execute. + # parameters:: The conditions to the query. + # do_reload:: Whether to reload objects already found in the identity map. + # + # ==== Returns + # LoadedSet:: A set of the found instances. + # + def read_set_with_sql(repository, model, properties, sql, parameters, do_reload) properties_with_indexes = Hash[*properties.zip((0...properties.length).to_a).flatten] - set = LoadedSet.new(repository, query.model, properties_with_indexes) + set = LoadedSet.new(repository, model, properties_with_indexes) - sql = query_read_statement(query) - parameters = query.parameters DataMapper.logger.debug { "READ_SET: #{sql} PARAMETERS: #{parameters.inspect}" } connection = create_connection @@ -173,7 +246,7 @@ def read_set(repository, query) reader = command.execute_reader(*parameters) while(reader.next!) - set.add(reader.values, query.reload?) + set.add(reader.values, do_reload) end reader.close @@ -187,6 +260,16 @@ def read_set(repository, query) set end + # Methods dealing with finding stuff by some query parameters + def read_set(repository, query) + read_set_with_sql(repository, + query.model, + query.fields, + query_read_statement(query), + query.parameters, + query.reload?) + end + def delete_set(repository, query) raise NotImplementedError end diff --git a/spec/unit/adapters/data_objects_adapter_spec.rb b/spec/unit/adapters/data_objects_adapter_spec.rb index 61af7862..2b9a266d 100644 --- a/spec/unit/adapters/data_objects_adapter_spec.rb +++ b/spec/unit/adapters/data_objects_adapter_spec.rb @@ -12,6 +12,88 @@ it_should_behave_like 'a DataMapper Adapter' + describe "#find_by_sql" do + + before do + class Plupp + include DataMapper::Resource + property :id, Fixnum, :key => true + property :name, String + end + end + + it "should be added to DataMapper::Resource::ClassMethods" do + DataMapper::Resource::ClassMethods.instance_methods.include?("find_by_sql").should == true + Plupp.methods.include?("find_by_sql").should == true + end + + describe "when called" do + + before do + @reader = mock("reader") + @reader.stub!(:next!).and_return(false) + @reader.stub!(:close) + @connection = mock("connection") + @connection.stub!(:close) + @command = mock("command") + @adapter = Plupp.repository.adapter + @repository = Plupp.repository + @repository.stub!(:adapter).and_return(@adapter) + @adapter.stub!(:create_connection).and_return(@connection) + @adapter.should_receive(:is_a?).any_number_of_times.with(DataMapper::Adapters::DataObjectsAdapter).and_return(true) + end + + it "should accept a single String argument with or without options hash" do + @connection.should_receive(:create_command).twice.with("SELECT * FROM plupps").and_return(@command) + @command.should_receive(:set_types).twice.with([Fixnum, String]) + @command.should_receive(:execute_reader).twice.and_return(@reader) + Plupp.should_receive(:repository).any_number_of_times.and_return(@repository) + Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository) + Plupp.find_by_sql("SELECT * FROM plupps") + Plupp.find_by_sql("SELECT * FROM plupps", :repository => :plupp_repo) + end + + it "should accept an Array argument with or without options hash" do + @connection.should_receive(:create_command).twice.with("SELECT * FROM plupps WHERE plur = ?").and_return(@command) + @command.should_receive(:set_types).twice.with([Fixnum, String]) + @command.should_receive(:execute_reader).twice.with("my pretty plur").and_return(@reader) + Plupp.should_receive(:repository).any_number_of_times.and_return(@repository) + Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository) + Plupp.find_by_sql(["SELECT * FROM plupps WHERE plur = ?", "my pretty plur"]) + Plupp.find_by_sql(["SELECT * FROM plupps WHERE plur = ?", "my pretty plur"], :repository => :plupp_repo) + end + + it "should accept a Query argument with or without options hash" do + @connection.should_receive(:create_command).twice.with("SELECT \"name\" FROM \"plupps\" WHERE (\"name\" = ?)").and_return(@command) + @command.should_receive(:set_types).twice.with([Fixnum, String]) + @command.should_receive(:execute_reader).twice.with(Plupp.properties[:name]).and_return(@reader) + Plupp.should_receive(:repository).any_number_of_times.and_return(@repository) + Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository) + Plupp.find_by_sql(DataMapper::Query.new(Plupp, "name" => "my pretty plur", :fields => ["name"])) + Plupp.find_by_sql(DataMapper::Query.new(Plupp, "name" => "my pretty plur", :fields => ["name"]), :repository => :plupp_repo) + end + + it "requires a Repository that is a DataObjectsRepository to work" do + non_do_adapter = mock("non do adapter") + non_do_repo = mock("non do repo") + non_do_repo.stub!(:adapter).and_return(non_do_adapter) + Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(non_do_repo) + Proc.new do + Plupp.find_by_sql(:repository => :plupp_repo) + end.should raise_error(Exception, /DataObjectsAdapter/) + end + + it "requires some kind of query to work at all" do + Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository) + Proc.new do + Plupp.find_by_sql(:repository => :plupp_repo) + end.should raise_error(Exception, /requires a query/) + end + + end + + end + describe '#execute' do before do @mock_command = mock('Command', :execute_non_query => nil) From 557fb65fb0743dbce5ae5cea39027d98cf5d5654 Mon Sep 17 00:00:00 2001 From: Martin Kihlgren Date: Fri, 18 Apr 2008 14:38:33 +0200 Subject: [PATCH 3/3] Made Resource::ClassMethods#find_by_sql slightly more practically useful by allowing the user to define what properties the query returns. * Added :property as an option to the call signature. --- lib/data_mapper/adapters/data_objects_adapter.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/data_mapper/adapters/data_objects_adapter.rb b/lib/data_mapper/adapters/data_objects_adapter.rb index df8cee9d..f1d8ae29 100644 --- a/lib/data_mapper/adapters/data_objects_adapter.rb +++ b/lib/data_mapper/adapters/data_objects_adapter.rb @@ -22,13 +22,19 @@ module ClassMethods # :: A prepared Query to execute. # :: An options hash. # + # A String, Array or Query is required. + # # ==== Options (the options hash) - # :repository:: The name of the repository to execute the query in. - # :reload:: Whether to reload any instances found that allready exist in the identity map. + # :repository:: The name of the repository to execute the query in. Defaults to self.default_repository_name. + # :reload:: Whether to reload any instances found that allready exist in the identity map. Defaults to false. + # :properties:: The Properties of the instance that the query loads. Must contain DataMapper::Properties. Defaults to self.properties. # # ==== Returns # LoadedSet:: The instance matched by the query. # + # ==== Example + # MyClass.find_by_sql(["SELECT id FROM my_classes WHERE county = ?", selected_county], :properties => MyClass.property[:id], :repository => :county_repo) + # # - # @public def find_by_sql(*args) @@ -48,6 +54,7 @@ def find_by_sql(*args) query = arg elsif arg.is_a?(Hash) repository_name = arg.delete(:repository) if arg.include?(:repository) + properties = Array(arg.delete(:properties)) if arg.include?(:properties) do_reload = arg.delete(:reload) if arg.include?(:reload) raise "unknown options to #find_by_sql: #{arg.inspect}" unless arg.empty? end