dm-do-adapter 1.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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