public
Description: Fork of DataMapper 0.3 with patches to fix major show-stopping bugs
Homepage:
Clone URL: git://github.com/cardmagic/dm-works.git
dm-works / lib / data_mapper / adapters / sql / commands / load_command.rb
100644 573 lines (483 sloc) 20.814 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
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