Skip to content

Commit

Permalink
Merge branch 'master' of git@github.com:sam/dm-core
Browse files Browse the repository at this point in the history
* 'master' of git@github.com:sam/dm-core:
  Associations may now be added inside a repository do ... end block
  Fixed repository(...) do ... end inside a Resource class declaration.
  Made Resource::ClassMethods#find_by_sql slightly more practically useful
  Added Resource::ClassMethods#find_by_sql.
  Added better documentation for Resource.including_classes.
  Enabled DataMapper::Resource to know what classes have included it.
  Resolved ticket #210.
  Resolved ticket #212.
  Added specs for Property type casting
  Fixed failing specs
  Fixes stupid thing I did.
  Assorted fixes.
  Added documentation for the new property parameter
  Added support to pass property information own to custom types in the dump/load
  • Loading branch information
wycats committed Apr 19, 2008
2 parents cee40c0 + c5bbdd0 commit 5a3189c
Show file tree
Hide file tree
Showing 20 changed files with 589 additions and 118 deletions.
138 changes: 116 additions & 22 deletions lib/data_mapper/adapters/data_objects_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,77 @@

module DataMapper

module Resource

module ClassMethods
#
# Find instances by manually providing SQL
#
# ==== Parameters
# <String>:: An SQL query to execute
# <Array>:: 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]
# <DataMapper::Query>:: A prepared Query to execute.
# <Hash>:: An options hash.
#
# A String, Array or Query is required.
#
# ==== Options (the options hash)
# :repository<Symbol>:: The name of the repository to execute the query in. Defaults to self.default_repository_name.
# :reload<Boolean>:: Whether to reload any instances found that allready exist in the identity map. Defaults to false.
# :properties<Array>:: 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)
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)
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
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
Expand Down Expand Up @@ -122,20 +193,24 @@ def read(repository, resource, key)

def update(repository, resource)
properties = resource.dirty_attributes

sql = update_statement(resource.class, properties)
values = properties.map { |property| resource.instance_variable_get(property.instance_variable_name) }
parameters = (values + resource.key)
DataMapper.logger.debug { "UPDATE: #{sql} PARAMETERS: #{parameters.inspect}" }

connection = create_connection
command = connection.create_command(sql)

affected_rows = command.execute_non_query(*parameters).to_i

close_connection(connection)

affected_rows == 1

if properties.empty?
return false
else
sql = update_statement(resource.class, properties)
values = properties.map { |property| resource.instance_variable_get(property.instance_variable_name) }
parameters = (values + resource.key)
DataMapper.logger.debug { "UPDATE: #{sql} PARAMETERS: #{parameters.inspect}" }

connection = create_connection
command = connection.create_command(sql)

affected_rows = command.execute_non_query(*parameters).to_i

close_connection(connection)

affected_rows == 1
end
end

def delete(repository, resource)
Expand All @@ -151,15 +226,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<DataMapper::Repository>:: The repository to read from.
# model<Object>:: The class of the instances to read.
# properties<Array>:: The properties to read. Must contain Symbols, Strings or DM::Properties.
# sql<String>:: The query to execute.
# parameters<Array>:: The conditions to the query.
# do_reload<Boolean>:: 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
Expand All @@ -169,7 +253,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
Expand All @@ -183,6 +267,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
Expand Down
2 changes: 1 addition & 1 deletion lib/data_mapper/associations/many_to_one.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def many_to_one(name, options = {})

relationships[name] = Relationship.new(
name,
options[:repository_name] || repository.name,
repository.name,
child_model_name,
nil,
parent_model_name,
Expand Down
2 changes: 1 addition & 1 deletion lib/data_mapper/associations/one_to_many.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def one_to_many(name, options = {})

relationships[name] = Relationship.new(
DataMapper::Inflection.underscore(parent_model_name).to_sym,
options[:repository_name] || repository.name,
repository.name,
child_model_name,
nil,
parent_model_name,
Expand Down
2 changes: 1 addition & 1 deletion lib/data_mapper/associations/one_to_one.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def one_to_one(name, options = {})

relationships[name] = Relationship.new(
DataMapper::Inflection.underscore(parent_model_name).to_sym,
options[:repository_name] || repository.name,
repository.name,
child_model_name,
nil,
parent_model_name,
Expand Down
18 changes: 12 additions & 6 deletions lib/data_mapper/hook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ module ClassMethods
def before(target_method, method_sym = nil, &block)
install_hook :before, target_method, method_sym, &block
end

def after(target_method, method_sym = nil, &block)
install_hook :after, target_method, method_sym, &block
end

def install_hook(type, name, method_sym = nil, &block)
raise ArgumentError.new("You need to pass 2 arguments to \"#{type}\".") if ! block_given? and method_sym.nil?
raise ArgumentError.new("target_method should be a symbol") unless name.is_a?(Symbol)
raise ArgumentError.new("method_sym should be a symbol") if method_sym && ! method_sym.is_a?(Symbol)

(hooks[name][type] ||= []) << if block
new_meth_name = "__hooks_#{type}_#{quote_method(name)}_#{hooks[name][type].length}".to_sym
define_method new_meth_name, block
Expand All @@ -21,14 +29,16 @@ def install_hook(type, name, method_sym = nil, &block)
class_eval define_advised_method(name), __FILE__, __LINE__
end

# FIXME Return the method value
def define_advised_method(name)
args = args_for(hooks[name][:old_method] ||= instance_method(name))

<<-EOD
def #{name}(#{args})
#{inline_hooks(name, :before, args)}
#{inline_call(name, args)}
retval = #{inline_call(name, args)}
#{inline_hooks(name, :after, args)}
retval
end
EOD
end
Expand Down Expand Up @@ -80,11 +90,7 @@ def hooks
end

def quote_method(name)
name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_')
end

def after(target_method, method_sym = nil, &block)
install_hook :after, target_method, method_sym, &block
name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_')
end
end
end # module Hook
Expand Down
67 changes: 32 additions & 35 deletions lib/data_mapper/property.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ module DataMapper
# property :body, DataMapper::Types::Text # Is lazily loaded by default
# end
#
# If you want to over-ride the lazy loading on any field you can set it to a
# If you want to over-ride the lazy loading on any field you can set it to a
# context or false to disable it with the :lazy option. Contexts allow multipule
# lazy properties to be loaded at one time. If you set :lazy to true, it is placed
# in the :default context
Expand Down Expand Up @@ -230,6 +230,18 @@ def field
@field ||= @options.fetch(:field, repository.adapter.field_naming_convention.call(name))
end

def hash
return @model.hash + @name.hash
end

def eql?(o)
if o.is_a?(Property)
return o.model == @model && o.name == @name
else
return false
end
end

def lazy?
@lazy
end
Expand Down Expand Up @@ -258,33 +270,29 @@ def set(value, resource)
resource[@name] = value
end

def inspect
"#<Property:#{@model}:#{@name}>"
end

def typecast(value)
if type == TrueClass
value == true || value == "true"
elsif type == String
value.to_s
elsif [Float, Fixnum, BigDecimal].include?(type)
value.to_f
elsif type == DateTime
Time.parse(value)
elsif type == Date
Date.parse(value)
elsif type == Class
Object.recursive_const_get(value)
else
value
return value if type === value || value.nil?

if type == TrueClass then true == value || 'true' == value
elsif type == String then value.to_s
elsif type == Float then value.to_f
elsif type == Fixnum then value.to_i
elsif type == BigDecimal then BigDecimal.new(value.to_s)
elsif type == DateTime then DateTime.parse(value.to_s)
elsif type == Date then Date.parse(value.to_s)
elsif type == Class then Object.recursive_const_get(value)
end
end

def inspect
"#<Property:#{@model}:#{@name}>"
end

private

def initialize(model, name, type, options)
raise ArgumentError, "+model+ is a #{model.class}, but is not a type of Resource" unless Resource === model
raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}" unless Symbol === name
def initialize(model, name, type, options = {})
raise ArgumentError, "+model+ is a #{model.class}, but is not a type of Resource" unless Resource === model
raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}" unless Symbol === name
raise ArgumentError, "+type+ was #{type.inspect}, which is not a supported type: #{TYPES * ', '}" unless TYPES.include?(type) || (type.respond_to?(:ancestors) && type.ancestors.include?(DataMapper::Type) && TYPES.include?(type.primitive))

if (unknown_options = options.keys - PROPERTY_OPTIONS).any?
Expand Down Expand Up @@ -329,30 +337,19 @@ def create_getter
@model.class_eval <<-EOS, __FILE__, __LINE__
#{reader_visibility}
def #{@getter}
unless @new_record || defined?(#{@instance_variable_name})
if @loaded_set
@loaded_set.reload!(:fields => self.class.properties(self.class.repository.name).lazy_load_context(#{name.inspect}))
end
end
#{custom? ? "#{@type.inspect}.load(#{@instance_variable_name})" : @instance_variable_name}
self[#{name.inspect}]
end
EOS
rescue SyntaxError
raise SyntaxError, name
end

# defines the setter for the property
def create_setter
@model.class_eval <<-EOS, __FILE__, __LINE__
#{writer_visibility}
def #{name}=(value)
#{lock? ? "@shadow_#{name} = #{@instance_variable_name}" : ''}
dirty_attributes << self.class.properties(repository.name)[#{name.inspect}]
#{@instance_variable_name} = #{custom? ? "#{@type.inspect}.dump(value)" : 'value'}
self[#{name.inspect}] = value
end
EOS
rescue SyntaxError
raise SyntaxError, name
end
end # class Property
end # module DataMapper
Loading

0 comments on commit 5a3189c

Please sign in to comment.