dm-do-adapter 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,746 @@
1
+ require 'data_objects'
2
+ require 'dm-core'
3
+
4
+ module DataMapper
5
+ module Adapters
6
+
7
+ # DataObjectsAdapter is the base class for all adapers for relational
8
+ # databases. If you want to add support for a new RDBMS, it makes
9
+ # sense to make your adapter class inherit from this class.
10
+ #
11
+ # By inheriting from DataObjectsAdapter, you get a copy of all the
12
+ # standard sub-modules (Quoting, Coersion and Queries) in your own Adapter.
13
+ # You can extend and overwrite these copies without affecting the originals.
14
+ class DataObjectsAdapter < AbstractAdapter
15
+ extend Chainable
16
+ extend Deprecate
17
+
18
+ deprecate :query, :select
19
+
20
+ # Retrieve results using an SQL SELECT statement
21
+ #
22
+ # @param [String] statement
23
+ # the SQL SELECT statement
24
+ # @param [Array] *bind_values
25
+ # optional bind values to merge into the statement
26
+ #
27
+ # @return [Array]
28
+ # if fields > 1, return an Array of Struct objects
29
+ # if fields == 1, return an Array of objects
30
+ #
31
+ # @api public
32
+ def select(statement, *bind_values)
33
+ with_connection do |connection|
34
+ reader = connection.create_command(statement).execute_reader(*bind_values)
35
+ fields = reader.fields
36
+
37
+ begin
38
+ if fields.size > 1
39
+ select_fields(reader, fields)
40
+ else
41
+ select_field(reader)
42
+ end
43
+ ensure
44
+ reader.close
45
+ end
46
+ end
47
+ end
48
+
49
+ # Execute non-SELECT SQL query
50
+ #
51
+ # @param [String] statement
52
+ # the SQL statement
53
+ # @param [Array] *bind_values
54
+ # optional bind values to merge into the statement
55
+ #
56
+ # @return [DataObjects::Result]
57
+ # result with number of affected rows, and insert id if any
58
+ #
59
+ # @api public
60
+ def execute(statement, *bind_values)
61
+ with_connection do |connection|
62
+ command = connection.create_command(statement)
63
+ command.execute_non_query(*bind_values)
64
+ end
65
+ end
66
+
67
+ # For each model instance in resources, issues an SQL INSERT
68
+ # (or equivalent) statement to create a new record in the data store for
69
+ # the instance
70
+ #
71
+ # Note that this method does not update the identity map. If a plugin
72
+ # needs to use an adapter directly, it is up to plugin developer to
73
+ # keep the identity map up to date.
74
+ #
75
+ # @param [Enumerable(Resource)] resources
76
+ # The list of resources (model instances) to create
77
+ #
78
+ # @return [Integer]
79
+ # The number of records that were actually saved into the database
80
+ #
81
+ # @api semipublic
82
+ def create(resources)
83
+ name = self.name
84
+
85
+ resources.each do |resource|
86
+ model = resource.model
87
+ serial = model.serial(name)
88
+ attributes = resource.dirty_attributes
89
+
90
+ properties = []
91
+ bind_values = []
92
+
93
+ # make the order of the properties consistent
94
+ model.properties(name).each do |property|
95
+ next unless attributes.key?(property)
96
+
97
+ bind_value = attributes[property]
98
+
99
+ # skip insering NULL for columns that are serial or without a default
100
+ next if bind_value.nil? && (property.serial? || !property.default?)
101
+
102
+ # if serial is being set explicitly, do not set it again
103
+ if property.equal?(serial)
104
+ serial = nil
105
+ end
106
+
107
+ properties << property
108
+ bind_values << bind_value
109
+ end
110
+
111
+ statement = insert_statement(model, properties, serial)
112
+ result = execute(statement, *bind_values)
113
+
114
+ if result.affected_rows == 1 && serial
115
+ serial.set!(resource, result.insert_id)
116
+ end
117
+ end
118
+ end
119
+
120
+ # Constructs and executes SELECT query, then instantiates
121
+ # one or many object from result set.
122
+ #
123
+ # @param [Query] query
124
+ # composition of the query to perform
125
+ #
126
+ # @return [Array]
127
+ # result set of the query
128
+ #
129
+ # @api semipublic
130
+ def read(query)
131
+ fields = query.fields
132
+ types = fields.map { |property| property.primitive }
133
+
134
+ statement, bind_values = select_statement(query)
135
+
136
+ records = []
137
+
138
+ with_connection do |connection|
139
+ command = connection.create_command(statement)
140
+ command.set_types(types)
141
+
142
+ reader = command.execute_reader(*bind_values)
143
+
144
+ begin
145
+ while reader.next!
146
+ records << fields.zip(reader.values).to_hash
147
+ end
148
+ ensure
149
+ reader.close
150
+ end
151
+ end
152
+
153
+ records
154
+ end
155
+
156
+ # Constructs and executes UPDATE statement for given
157
+ # attributes and a query
158
+ #
159
+ # @param [Hash(Property => Object)] attributes
160
+ # hash of attribute values to set, keyed by Property
161
+ # @param [Collection] collection
162
+ # collection of records to be updated
163
+ #
164
+ # @return [Integer]
165
+ # the number of records updated
166
+ #
167
+ # @api semipublic
168
+ def update(attributes, collection)
169
+ query = collection.query
170
+
171
+ properties = []
172
+ bind_values = []
173
+
174
+ # make the order of the properties consistent
175
+ query.model.properties(name).each do |property|
176
+ next unless attributes.key?(property)
177
+ properties << property
178
+ bind_values << attributes[property]
179
+ end
180
+
181
+ statement, conditions_bind_values = update_statement(properties, query)
182
+
183
+ bind_values.concat(conditions_bind_values)
184
+
185
+ execute(statement, *bind_values).affected_rows
186
+ end
187
+
188
+ # Constructs and executes DELETE statement for given query
189
+ #
190
+ # @param [Collection] collection
191
+ # collection of records to be deleted
192
+ #
193
+ # @return [Integer]
194
+ # the number of records deleted
195
+ #
196
+ # @api semipublic
197
+ def delete(collection)
198
+ query = collection.query
199
+ statement, bind_values = delete_statement(query)
200
+ execute(statement, *bind_values).affected_rows
201
+ end
202
+
203
+ protected
204
+
205
+ # @api private
206
+ def normalized_uri
207
+ @normalized_uri ||=
208
+ begin
209
+ query = @options.except(:adapter, :user, :password, :host, :port, :path, :fragment, :scheme, :query, :username, :database)
210
+ query = nil if query.empty?
211
+
212
+ # Better error message in case port is no Numeric value
213
+ port = @options[:port].nil? ? nil : @options[:port].to_int
214
+
215
+ DataObjects::URI.new(
216
+ @options[:adapter],
217
+ @options[:user] || @options[:username],
218
+ @options[:password],
219
+ @options[:host],
220
+ port,
221
+ @options[:path] || @options[:database],
222
+ query,
223
+ @options[:fragment]
224
+ ).freeze
225
+ end
226
+ end
227
+
228
+ chainable do
229
+ protected
230
+
231
+ # Instantiates new connection object
232
+ #
233
+ # @api semipublic
234
+ def open_connection
235
+ DataObjects::Connection.new(normalized_uri)
236
+ end
237
+
238
+ # Takes connection and closes it
239
+ #
240
+ # @api semipublic
241
+ def close_connection(connection)
242
+ connection.close if connection.respond_to?(:close)
243
+ end
244
+ end
245
+
246
+ private
247
+
248
+ # @api public
249
+ def initialize(name, uri_or_options)
250
+ super
251
+
252
+ # Default the driver-specific logger to DataMapper's logger
253
+ if driver_module = DataObjects.const_get(normalized_uri.scheme.capitalize)
254
+ driver_module.logger = DataMapper.logger if driver_module.respond_to?(:logger=)
255
+ end
256
+ end
257
+
258
+ # @api private
259
+ def with_connection
260
+ yield connection = open_connection
261
+ rescue Exception => exception
262
+ DataMapper.logger.error(exception.to_s) if DataMapper.logger
263
+ raise
264
+ ensure
265
+ close_connection(connection)
266
+ end
267
+
268
+ # @api private
269
+ def select_fields(reader, fields)
270
+ fields = fields.map { |field| ActiveSupport::Inflector.underscore(field).to_sym }
271
+ struct = Struct.new(*fields)
272
+
273
+ results = []
274
+
275
+ while reader.next!
276
+ results << struct.new(*reader.values)
277
+ end
278
+
279
+ results
280
+ end
281
+
282
+ # @api private
283
+ def select_field(reader)
284
+ results = []
285
+
286
+ while reader.next!
287
+ results << reader.values.at(0)
288
+ end
289
+
290
+ results
291
+ end
292
+
293
+ # This module is just for organization. The methods are included into the
294
+ # Adapter below.
295
+ module SQL #:nodoc:
296
+ IDENTIFIER_MAX_LENGTH = 128
297
+
298
+ # @api semipublic
299
+ def property_to_column_name(property, qualify)
300
+ column_name = ''
301
+
302
+ case qualify
303
+ when true
304
+ column_name << "#{quote_name(property.model.storage_name(name))}."
305
+ when String
306
+ column_name << "#{quote_name(qualify)}."
307
+ end
308
+
309
+ column_name << quote_name(property.field)
310
+ end
311
+
312
+ private
313
+
314
+ # Adapters requiring a RETURNING syntax for INSERT statements
315
+ # should overwrite this to return true.
316
+ #
317
+ # @api private
318
+ def supports_returning?
319
+ false
320
+ end
321
+
322
+ # Adapters that do not support the DEFAULT VALUES syntax for
323
+ # INSERT statements should overwrite this to return false.
324
+ #
325
+ # @api private
326
+ def supports_default_values?
327
+ true
328
+ end
329
+
330
+ # Constructs SELECT statement for given query,
331
+ #
332
+ # @return [String] SELECT statement as a string
333
+ #
334
+ # @api private
335
+ def select_statement(query)
336
+ qualify = query.links.any?
337
+ fields = query.fields
338
+ order_by = query.order
339
+ group_by = if query.unique?
340
+ fields.select { |property| property.kind_of?(Property) }
341
+ end
342
+
343
+ conditions_statement, bind_values = conditions_statement(query.conditions, qualify)
344
+
345
+ statement = "SELECT #{columns_statement(fields, qualify)}"
346
+ statement << " FROM #{quote_name(query.model.storage_name(name))}"
347
+ statement << " #{join_statement(query, bind_values, qualify)}" if qualify
348
+ statement << " WHERE #{conditions_statement}" unless conditions_statement.blank?
349
+ statement << " GROUP BY #{columns_statement(group_by, qualify)}" if group_by && group_by.any?
350
+ statement << " ORDER BY #{order_statement(order_by, qualify)}" if order_by && order_by.any?
351
+
352
+ add_limit_offset!(statement, query.limit, query.offset, bind_values)
353
+
354
+ return statement, bind_values
355
+ end
356
+
357
+ # default construction of LIMIT and OFFSET
358
+ # overriden by some adapters (currently Oracle and SQL Server)
359
+ def add_limit_offset!(statement, limit, offset, bind_values)
360
+ if limit
361
+ statement << ' LIMIT ?'
362
+ bind_values << limit
363
+ end
364
+
365
+ if limit && offset > 0
366
+ statement << ' OFFSET ?'
367
+ bind_values << offset
368
+ end
369
+ end
370
+
371
+ # Constructs INSERT statement for given query,
372
+ #
373
+ # @return [String] INSERT statement as a string
374
+ #
375
+ # @api private
376
+ def insert_statement(model, properties, serial)
377
+ statement = "INSERT INTO #{quote_name(model.storage_name(name))} "
378
+
379
+ if supports_default_values? && properties.empty?
380
+ statement << default_values_clause
381
+ else
382
+ statement << <<-SQL.compress_lines
383
+ (#{properties.map { |property| quote_name(property.field) }.join(', ')})
384
+ VALUES
385
+ (#{(['?'] * properties.size).join(', ')})
386
+ SQL
387
+ end
388
+
389
+ if supports_returning? && serial
390
+ statement << returning_clause(serial)
391
+ end
392
+
393
+ statement
394
+ end
395
+
396
+ # by default PostgreSQL syntax
397
+ # overrided in Oracle adapter
398
+ def default_values_clause
399
+ 'DEFAULT VALUES'
400
+ end
401
+
402
+ # by default PostgreSQL syntax
403
+ # overrided in Oracle adapter
404
+ def returning_clause(serial)
405
+ " RETURNING #{quote_name(serial.field)}"
406
+ end
407
+
408
+ # Constructs UPDATE statement for given query,
409
+ #
410
+ # @return [String] UPDATE statement as a string
411
+ #
412
+ # @api private
413
+ def update_statement(properties, query)
414
+ model = query.model
415
+ name = self.name
416
+
417
+ # TODO: DRY this up with delete_statement
418
+ conditions_statement, bind_values = if query.limit || query.links.any?
419
+ subquery(query, model.key(name), false)
420
+ else
421
+ conditions_statement(query.conditions)
422
+ end
423
+
424
+ statement = "UPDATE #{quote_name(model.storage_name(name))}"
425
+ statement << " SET #{properties.map { |property| "#{quote_name(property.field)} = ?" }.join(', ')}"
426
+ statement << " WHERE #{conditions_statement}" unless conditions_statement.blank?
427
+
428
+ return statement, bind_values
429
+ end
430
+
431
+ # Constructs DELETE statement for given query,
432
+ #
433
+ # @return [String] DELETE statement as a string
434
+ #
435
+ # @api private
436
+ def delete_statement(query)
437
+ model = query.model
438
+ name = self.name
439
+
440
+ # TODO: DRY this up with update_statement
441
+ conditions_statement, bind_values = if query.limit || query.links.any?
442
+ subquery(query, model.key(name), false)
443
+ else
444
+ conditions_statement(query.conditions)
445
+ end
446
+
447
+ statement = "DELETE FROM #{quote_name(model.storage_name(name))}"
448
+ statement << " WHERE #{conditions_statement}" unless conditions_statement.blank?
449
+
450
+ return statement, bind_values
451
+ end
452
+
453
+ # Constructs comma separated list of fields
454
+ #
455
+ # @return [String]
456
+ # list of fields as a string
457
+ #
458
+ # @api private
459
+ def columns_statement(properties, qualify)
460
+ properties.map { |property| property_to_column_name(property, qualify) }.join(', ')
461
+ end
462
+
463
+ # Constructs joins clause
464
+ #
465
+ # @return [String]
466
+ # joins clause
467
+ #
468
+ # @api private
469
+ def join_statement(query, bind_values, qualify)
470
+ statements = []
471
+ join_bind_values = []
472
+
473
+ target_alias = query.model.storage_name(name)
474
+ seen = { target_alias => 0 }
475
+
476
+ query.links.reverse_each do |relationship|
477
+ storage_name = relationship.source_model.storage_name(name)
478
+ source_alias = storage_name
479
+
480
+ statements << "INNER JOIN #{quote_name(storage_name)}"
481
+
482
+ if seen.key?(source_alias)
483
+ seen[source_alias] += 1
484
+ source_alias = "#{source_alias}_#{seen[source_alias]}"
485
+ statements << quote_name(source_alias)
486
+ else
487
+ seen[source_alias] = 0
488
+ end
489
+
490
+ statements << 'ON'
491
+
492
+ add_join_conditions(relationship, target_alias, source_alias, statements)
493
+ add_extra_join_conditions(relationship, target_alias, statements, join_bind_values)
494
+
495
+ target_alias = source_alias
496
+ end
497
+
498
+ # prepend the join bind values to the statement bind values
499
+ bind_values.unshift(*join_bind_values)
500
+
501
+ statements.join(' ')
502
+ end
503
+
504
+ def add_join_conditions(relationship, target_alias, source_alias, statements)
505
+ statements << relationship.target_key.zip(relationship.source_key).map do |target_property, source_property|
506
+ "#{property_to_column_name(target_property, target_alias)} = #{property_to_column_name(source_property, source_alias)}"
507
+ end.join(' AND ')
508
+ end
509
+
510
+ def add_extra_join_conditions(relationship, target_alias, statements, bind_values)
511
+ conditions = DataMapper.repository(name).scope do
512
+ relationship.target_model.all(relationship.query).query.conditions
513
+ end
514
+
515
+ return if conditions.nil?
516
+
517
+ extra_statement, extra_bind_values = conditions_statement(conditions, target_alias)
518
+ statements << "AND #{extra_statement}"
519
+ bind_values.concat(extra_bind_values)
520
+ end
521
+
522
+ # Constructs where clause
523
+ #
524
+ # @return [String]
525
+ # where clause
526
+ #
527
+ # @api private
528
+ def conditions_statement(conditions, qualify = false)
529
+ case conditions
530
+ when Query::Conditions::NotOperation then negate_operation(conditions.operand, qualify)
531
+ when Query::Conditions::AbstractOperation then operation_statement(conditions, qualify)
532
+ when Query::Conditions::AbstractComparison then comparison_statement(conditions, qualify)
533
+ when Array
534
+ statement, bind_values = conditions # handle raw conditions
535
+ [ "(#{statement})", bind_values ].compact
536
+ end
537
+ end
538
+
539
+ # @api private
540
+ def supports_subquery?(*)
541
+ true
542
+ end
543
+
544
+ # @api private
545
+ def subquery(query, subject, qualify)
546
+ source_key, target_key = subquery_keys(subject)
547
+
548
+ if query.repository.name == name && supports_subquery?(query, source_key, target_key, qualify)
549
+ subquery_statement(query, source_key, target_key, qualify)
550
+ else
551
+ subquery_execute(query, source_key, target_key, qualify)
552
+ end
553
+ end
554
+
555
+ # @api private
556
+ def subquery_statement(query, source_key, target_key, qualify)
557
+ query = subquery_query(query, source_key)
558
+ select_statement, bind_values = select_statement(query)
559
+
560
+ statement = if target_key.size == 1
561
+ property_to_column_name(target_key.first, qualify)
562
+ else
563
+ "(#{target_key.map { |property| property_to_column_name(property, qualify) }.join(', ')})"
564
+ end
565
+
566
+ statement << " IN (#{select_statement})"
567
+
568
+ return statement, bind_values
569
+ end
570
+
571
+ # @api private
572
+ def subquery_execute(query, source_key, target_key, qualify)
573
+ query = subquery_query(query, source_key)
574
+ sources = query.model.all(query)
575
+ conditions = Query.target_conditions(sources, source_key, target_key)
576
+
577
+ if conditions.valid?
578
+ conditions_statement(conditions, qualify)
579
+ else
580
+ [ '1 = 0', [] ]
581
+ end
582
+ end
583
+
584
+ # @api private
585
+ def subquery_keys(subject)
586
+ case subject
587
+ when Associations::Relationship
588
+ relationship = subject.inverse
589
+ [ relationship.source_key, relationship.target_key ]
590
+ when PropertySet
591
+ [ subject, subject ]
592
+ end
593
+ end
594
+
595
+ # @api private
596
+ def subquery_query(query, source_key)
597
+ # force unique to be false because PostgreSQL has a problem with
598
+ # subselects that contain a GROUP BY with different columns
599
+ # than the outer-most query
600
+ query = query.merge(:fields => source_key, :unique => false)
601
+ query.update(:order => nil) unless query.limit
602
+ query
603
+ end
604
+
605
+ # Constructs order clause
606
+ #
607
+ # @return [String]
608
+ # order clause
609
+ #
610
+ # @api private
611
+ def order_statement(order, qualify)
612
+ statements = order.map do |direction|
613
+ statement = property_to_column_name(direction.target, qualify)
614
+ statement << ' DESC' if direction.operator == :desc
615
+ statement
616
+ end
617
+
618
+ statements.join(', ')
619
+ end
620
+
621
+ # @api private
622
+ def negate_operation(operand, qualify)
623
+ statement, bind_values = conditions_statement(operand, qualify)
624
+ statement = "NOT(#{statement})" unless statement.nil?
625
+ [ statement, bind_values ]
626
+ end
627
+
628
+ # @api private
629
+ def operation_statement(operation, qualify)
630
+ statements = []
631
+ bind_values = []
632
+
633
+ operation.each do |operand|
634
+ statement, values = conditions_statement(operand, qualify)
635
+ next unless statement
636
+ statements << statement
637
+ bind_values.concat(values) if values
638
+ end
639
+
640
+ statement = statements.join(" #{operation.slug.to_s.upcase} ")
641
+
642
+ if statements.size > 1
643
+ statement = "(#{statement})"
644
+ end
645
+
646
+ return statement, bind_values
647
+ end
648
+
649
+ # Constructs comparison clause
650
+ #
651
+ # @return [String]
652
+ # comparison clause
653
+ #
654
+ # @api private
655
+ def comparison_statement(comparison, qualify)
656
+ subject = comparison.subject
657
+ value = comparison.value
658
+
659
+ # TODO: move exclusive Range handling into another method, and
660
+ # update conditions_statement to use it
661
+
662
+ # break exclusive Range queries up into two comparisons ANDed together
663
+ if value.kind_of?(Range) && value.exclude_end?
664
+ operation = Query::Conditions::Operation.new(:and,
665
+ Query::Conditions::Comparison.new(:gte, subject, value.first),
666
+ Query::Conditions::Comparison.new(:lt, subject, value.last)
667
+ )
668
+
669
+ statement, bind_values = conditions_statement(operation, qualify)
670
+
671
+ return "(#{statement})", bind_values
672
+ elsif comparison.relationship?
673
+ if value.respond_to?(:query) && value.respond_to?(:loaded?) && !value.loaded?
674
+ return subquery(value.query, subject, qualify)
675
+ else
676
+ return conditions_statement(comparison.foreign_key_mapping, qualify)
677
+ end
678
+ elsif comparison.slug == :in && !value.any?
679
+ return [] # match everything
680
+ end
681
+
682
+ operator = comparison_operator(comparison)
683
+ column_name = property_to_column_name(subject, qualify)
684
+
685
+ # if operator return value contains ? then it means that it is function call
686
+ # and it contains placeholder (%s) for property name as well (used in Oracle adapter for regexp operator)
687
+ if operator.include?('?')
688
+ return operator % column_name, [ value ]
689
+ else
690
+ return "#{column_name} #{operator} #{value.nil? ? 'NULL' : '?'}", [ value ].compact
691
+ end
692
+ end
693
+
694
+ def comparison_operator(comparison)
695
+ subject = comparison.subject
696
+ value = comparison.value
697
+
698
+ case comparison.slug
699
+ when :eql then equality_operator(subject, value)
700
+ when :in then include_operator(subject, value)
701
+ when :regexp then regexp_operator(value)
702
+ when :like then like_operator(value)
703
+ when :gt then '>'
704
+ when :lt then '<'
705
+ when :gte then '>='
706
+ when :lte then '<='
707
+ end
708
+ end
709
+
710
+ # @api private
711
+ def equality_operator(property, operand)
712
+ operand.nil? ? 'IS' : '='
713
+ end
714
+
715
+ # @api private
716
+ def include_operator(property, operand)
717
+ case operand
718
+ when Array then 'IN'
719
+ when Range then 'BETWEEN'
720
+ end
721
+ end
722
+
723
+ # @api private
724
+ def regexp_operator(operand)
725
+ '~'
726
+ end
727
+
728
+ # @api private
729
+ def like_operator(operand)
730
+ 'LIKE'
731
+ end
732
+
733
+ # @api private
734
+ def quote_name(name)
735
+ "\"#{name[0, self.class::IDENTIFIER_MAX_LENGTH].gsub('"', '""')}\""
736
+ end
737
+
738
+ end
739
+
740
+ include SQL
741
+
742
+ end
743
+
744
+ const_added(:DataObjectsAdapter)
745
+ end
746
+ end