module DataMapper
module Adapters
module Sql
module Commands
class LoadCommand
class Loader
def initialize(load_command, klass)
@load_command, @klass = load_command, klass
@columns = {}
@key = nil
@key_index = nil
@type_override_present = false
@type_override_index = nil
@type_override = nil
@database_context = load_command.database_context
@reload = load_command.reload?
@set = []
end
def add_column(column, index)
if column.key?
@key = column
@key_index = index
end
if column.type == :class
@type_override_present = true
@type_override_index = index
@type_override = column
end
@columns[index] = column
self
end
def materialize(values)
instance_id = @key.type_cast_value(values[@key_index])
instance = create_instance(instance_id,
if @type_override_present
@type_override.type_cast_value(values[@type_override_index]) || @klass
else
@klass
end
)
@klass.callbacks.execute(:before_materialize, instance)
type_casted_values = {}
@columns.each_pair do |index, column|
# This may be a little confusing, but we're
# setting both the original_value, and the
# instance-variable through method chaining to avoid
# lots of extra short-lived local variables.
begin
type_casted_values[column.name] = instance.instance_variable_set(
column.instance_variable_name,
column.type_cast_value(values[index])
)
rescue => e
raise MaterializationError.new("Failed to materialize column #{column.name.inspect} with value #{values[index].inspect}\n#{e.display}")
end
end
instance.original_values = type_casted_values
instance.instance_variable_set(:@loaded_set, @set)
@set << instance
@klass.callbacks.execute(:after_materialize, instance)
return instance
rescue => e
if e.is_a?(MaterializationError)
raise e
else
raise MaterializationError.new("Failed to materialize row: #{values.inspect}\n#{e.display}")
end
end
def loaded_set
@set
end
private
def create_instance(instance_id, instance_type)
instance = @database_context.identity_map.get(@klass, instance_id)
if instance.nil? || @reload
instance = instance_type.allocate() if instance.nil?
instance.instance_variable_set(:@__key, instance_id)
instance.instance_variable_set(:@new_record, false)
@database_context.identity_map.set(instance)
elsif instance.new_record?
instance.instance_variable_set(:@__key, instance_id)
instance.instance_variable_set(:@new_record, false)
end
instance.database_context = @database_context
return instance
end
end
class ConditionsError < StandardError
attr_reader :inner_error
def initialize(clause, value, inner_error)
@clause, @value, @inner_error = clause, value, inner_error
end
def message
"Conditions (:clause => #{@clause.inspect}, :value => #{@value.inspect}) failed: #{@inner_error}"
end
def backtrace
@inner_error.backtrace
end
end
attr_reader :conditions, :database_context, :options, :select, :from, :joins
def initialize(adapter, database_context, primary_class, options = {})
@adapter, @database_context, @primary_class = adapter, database_context, primary_class
# BEGIN: Partion out the options hash into general options,
# and conditions.
standard_find_options = @adapter.class::FIND_OPTIONS
conditions_hash = {}
@options = {}
options.each do |key,value|
if standard_find_options.include?(key) && key != :conditions
@options[key] = value
else
conditions_hash[key] = value
end
end
# END
@select = @options[:select] if @options[:select].is_a?(String)
@from = @options[:from]
@group = @options[:group]
@order = @options[:order]
@limit = @options[:limit]
@joins = @options[:joins]
@offset = @options[:offset]
@reload = @options[:reload]
@instance_id = conditions_hash[:id]
@conditions = parse_conditions(conditions_hash)
@loaders = Hash.new { |h,k| h[k] = Loader.new(self, k) }
end
# Display an overview of load options at a glance.
def inspect
<<-EOS.compress_lines % (object_id * 2)
#<#{self.class.name}:0x%x
@database=#{@adapter.name}
@reload=#{@reload.inspect}
@from=#{@from.inspect}
@select=#{@select.inspect}
@joins=#{@joins.inspect}
@order=#{@order.inspect}
@limit=#{@limit.inspect}
@offset=#{@offset.inspect}
@options=#{@options.inspect}>
EOS
end
# Access the Conditions instance
def conditions
@conditions
end
# If +true+ then force the command to reload any objects
# already existing in the IdentityMap when executing.
def reload?
@reload
end
# Determine if there is a limitation on the number of
# instances returned in the results. If +nil+, no limit
# is set. Can be used in conjunction with #offset for
# paging through a set of results.
def limit
@limit
end
# Used in conjunction with #limit to page through a set
# of results.
def offset
@offset
end
def call
# Check to see if the query is for a specific id and return if found
#
# NOTE: If the :id option is an Array:
# We could search for loaded instance ids and reject from
# the Array for already loaded instances, but working under the
# assumption that we'll probably have to issue a query to find
# at-least some of the instances we're looking for, it's faster to
# just skip that and go straight for the query.
unless reload? || @instance_id.blank? || @instance_id.is_a?(Array)
# If the id is for only a single record, attempt to find it.
if instance = @database_context.identity_map.get(@primary_class, @instance_id)
return [instance]
end
end
results = []
# Execute the statement and load the objects.
@adapter.connection do |db|
sql, *parameters = to_parameterized_sql
command = db.create_command(sql)
command.execute_reader(*parameters) do |reader|
if @options.has_key?(:intercept_load)
load(reader, &@options[:intercept_load])
else
load(reader)
end
end
end
results += @loaders[@primary_class].loaded_set
return results
end
def load(reader)
# The following blocks are identical aside from the yield.
# It's written this way to avoid a conditional within each
# iterator, and to take advantage of the performance of
# yield vs. Proc#call.
if block_given?
reader.each do
@loaders.each_pair do |klass,loader|
row = reader.current_row
yield(loader.materialize(row), @columns, row)
end
end
else
reader.each do
@loaders.each_pair do |klass,loader|
loader.materialize(reader.current_row)
end
end
end
end
# Are any conditions present?
def conditions_empty?
@conditions.empty?
end
# Generate a select statement based on the initialization
# arguments.
def to_parameterized_sql
parameters = []
cfs = columns_for_select
if @select.nil?
sql = 'SELECT ' << cfs.join(', ')
else
sql = 'SELECT ' << @select.to_s
end
if @from.nil?
sql << ' FROM ' << from_table_name
else
sql << ' FROM ' << @from.to_s
end
included_associations.each do |association|
sql << ' ' << association.to_sql
end
shallow_included_associations.each do |association|
sql << ' ' << association.to_shallow_sql
end
unless @joins.nil?
sql << @joins.to_s
end
unless conditions_empty?
sql << ' WHERE ('
last_index = @conditions.size
current_index = 0
@conditions.each do |condition|
case condition
when String then sql << condition
when Array then
sql << condition.shift
parameters += condition
else
raise "Unable to parse condition: #{condition.inspect}" if condition
end
if (current_index += 1) == last_index
sql << ')'
else
sql << ') AND ('
end
end
end # unless conditions_empty?
unless @group.nil?
sql << 'GROUP BY ' << @group.to_s
end
unless @order.nil?
sql << ' ORDER BY ' << @order.to_s
end
unless @limit.nil?
sql << ' LIMIT ' << @limit.to_s
end
unless @offset.nil?
sql << ' OFFSET ' << @offset.to_s
end
parameters.unshift(sql)
end
# If more than one table is involved in the query, the column definitions should
# be qualified by the table name. ie: people.name
# This method determines wether that needs to happen or not.
# Note: After the first call, the calculations are avoided by overwriting this
# method with a simple getter.
def qualify_columns?
@qualify_columns = !(included_associations.empty? && shallow_included_associations.empty?)
def self.qualify_columns?
@qualify_columns
end
@qualify_columns
end
# expression_to_sql takes a set of arguments, and turns them into a an
# Array of generated SQL, followed by optional Values to interpolate as SQL-Parameters.
#
# Parameters:
# +clause+ The name of the column as a Symbol, a raw-SQL String, a Mappings::Column
# instance, or a Symbol::Operator.
# +value+ The Value for the condition.
# +collector+ An Array representing all conditions that is appended to by expression_to_sql
#
# Returns: Undefined Output. The work performed is added to the +collector+ argument.
# Example:
# conditions = []
# expression_to_sql(:name, 'Bob', conditions)
# => +undefined return value+
# conditions.inspect
# => ["name = ?", 'Bob']
def expression_to_sql(clause, value, collector)
qualify_columns = qualify_columns?
case clause
when Symbol::Operator then
operator = case clause.type
when :gt then '>'
when :gte then '>='
when :lt then '<'
when :lte then '<='
when :not then inequality_operator(value)
when :eql then equality_operator(value)
when :like then equality_operator(value, 'LIKE')
when :in then equality_operator(value)
else raise ArgumentError.new('Operator type not supported')
end
collector << ["#{primary_class_table[clause].to_sql(qualify_columns)} #{operator} ?", value]
when Symbol then
collector << ["#{primary_class_table[clause].to_sql(qualify_columns)} #{equality_operator(value)} ?", value]
when String then
collector << [clause, *value]
when Mappings::Column then
collector << ["#{clause.to_sql(qualify_columns)} #{equality_operator(value)} ?", value]
else raise "CAN HAS CRASH? #{clause.inspect}"
end
rescue => e
if e.is_a?(ConditionsError)
raise e
else
raise ConditionsError.new(clause, value, e)
end
end
private
# Return the Sql-escaped columns names to be selected in the results.
def columns_for_select
@columns_for_select || begin
qualify_columns = qualify_columns?
@columns_for_select = []
i = 0
columns.each do |column|
class_for_loader = column.table.klass
@loaders[class_for_loader].add_column(column, i) if class_for_loader
@columns_for_select << column.to_sql(qualify_columns)
i += 1
end
@columns_for_select
end
end
# Returns the DataMapper::Adapters::Sql::Mappings::Column instances to
# be selected in the results.
def columns
@columns || begin
@columns = primary_class_columns
@columns += included_columns
included_associations.each do |assoc|
@columns += assoc.associated_columns
end
shallow_included_associations.each do |assoc|
@columns += assoc.join_columns
end
@columns
end
end
# Returns the default columns for the primary_class_table,
# or maps symbols specified in a +:select+ option to columns
# in the primary_class_table.
def primary_class_columns
@primary_class_columns || @primary_class_columns = begin
if @options.has_key?(:select)
case x = @options[:select]
when Array then x
when Symbol then [x]
when String
primary_class_table.columns.reject { |column| column.lazy? }
else raise ':select option must be a Symbol, or an Array of Symbols'
end.map { |name| primary_class_table[name] }
else
primary_class_table.columns.reject { |column| column.lazy? }
end
end
end
def included_associations
@included_associations || @included_associations = begin
associations = primary_class_table.associations
include_options.map do |name|
associations[name]
end.compact
end
end
def shallow_included_associations
@shallow_included_associations || @shallow_included_associations = begin
associations = primary_class_table.associations
shallow_include_options.map do |name|
associations[name]
end.compact
end
end
def included_columns
@included_columns || @included_columns = begin
include_options.map do |name|
primary_class_table[name]
end.compact
end
end
def include_options
@include_options || @include_options = begin
case x = @options[:include]
when Array then x
when Symbol then [x]
else []
end
end
end
def shallow_include_options
@shallow_include_options || @shallow_include_options = begin
case x = @options[:shallow_include]
when Array then x
when Symbol then [x]
else []
end
end
end
# Determine if a Column should be included based on the
# value of the +:include+ option.
def include_column?(name)
!primary_class_table[name].lazy? || include_options.includes?(name)
end
# Return the Sql-escaped table name of the +primary_class+.
def from_table_name
@from_table_name || (@from_table_name = @adapter.table(@primary_class).to_sql)
end
# Returns the DataMapper::Adapters::Sql::Mappings::Table for the +primary_class+.
def primary_class_table
@primary_class_table || (@primary_class_table = @adapter.table(@primary_class))
end
def parse_conditions(conditions_hash)
collection = []
case x = conditions_hash.delete(:conditions)
when Array then
# DO NOT mutate incoming Array values!!!
# Otherwise the mutated version may impact all the
# way up to the options passed to the finders,
# and have unintended side-effects.
array_copy = x.dup
clause = array_copy.shift
expression_to_sql(clause, array_copy, collection)
when Hash then
x.each_pair do |key,value|
expression_to_sql(key, value, collection)
end
else
raise "Unable to parse conditions: #{x.inspect}" if x
end
if primary_class_table.paranoid?
conditions_hash[primary_class_table.paranoid_column.name] = nil
end
conditions_hash.each_pair do |key,value|
expression_to_sql(key, value, collection)
end
collection
end
def equality_operator(value, default = '=')
case value
when NilClass then 'IS'
when Array then 'IN'
else default
end
end
def inequality_operator(value, default = '<>')
case value
when NilClass then 'IS NOT'
when Array then 'NOT IN'
else default
end
end
end # class LoadCommand
end # module Commands
end # module Sql
end # module Adapters
end # module DataMapper