ardm-do-adapter 1.2.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1f38f8747b0bde45cd4b0003875124b33bbfbda7
4
+ data.tar.gz: 60395ab8432a9283b71f896acf170d5f3bf58063
5
+ SHA512:
6
+ metadata.gz: 47a3826335e162e1dfcdee50eaacf4224b9ea8797835c7574c613566837dc2f41316ac130a45f57d8e193054017319692a87d065f0ccdd9614c3422f32c05500
7
+ data.tar.gz: c262903e1f1b63333ab0f04da7d7274e807fc7c877a9f014409c50340fd1c1f1796dbf9f29a07fe66f112e1bdfa2c42abffe39222191b87b34b3eb702dd50f55
data/.gitignore ADDED
@@ -0,0 +1,35 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## Rubinius
17
+ *.rbc
18
+
19
+ ## PROJECT::GENERAL
20
+ *.gem
21
+ coverage
22
+ rdoc
23
+ pkg
24
+ tmp
25
+ doc
26
+ log
27
+ .yardoc
28
+ measurements
29
+
30
+ ## BUNDLER
31
+ .bundle
32
+ Gemfile.*
33
+
34
+ ## PROJECT::SPECIFIC
35
+ spec/db/
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ require 'pathname'
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ SOURCE = ENV.fetch('SOURCE', :git).to_sym
8
+ REPO_POSTFIX = SOURCE == :path ? '' : '.git'
9
+ DATAMAPPER = SOURCE == :path ? Pathname(__FILE__).dirname.parent : 'http://github.com/ar-dm'
10
+ DM_VERSION = '~> 1.2.0'
11
+ DO_VERSION = '~> 0.10.6'
12
+ CURRENT_BRANCH = ENV.fetch('GIT_BRANCH', 'master')
13
+
14
+ do_options = {}
15
+ do_options[:git] = "#{DATAMAPPER}/do#{REPO_POSTFIX}" if ENV['DO_GIT'] == 'true'
16
+
17
+ gem 'data_objects', DO_VERSION, do_options.dup
18
+ gem 'ardm-core', DM_VERSION,
19
+ SOURCE => "#{DATAMAPPER}/ardm-core#{REPO_POSTFIX}",
20
+ :branch => CURRENT_BRANCH
21
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Dan Kubb
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = dm-do-adapter
2
+
3
+ A DataObjects Adapter for DataMapper
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ FileList['tasks/**/*.rake'].each { |task| import task }
5
+
6
+ task(:spec) {} # this adapter only provides shared specs that are exercised by real adapters
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/dm-do-adapter/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "ardm-do-adapter"
6
+ gem.version = DataMapper::DoAdapter::VERSION
7
+
8
+ gem.authors = [ 'Martin Emde', "Dan Kubb" ]
9
+ gem.email = [ 'me@martinemde.com', "dan.kubb@gmail.com" ]
10
+ gem.summary = 'Ardm fork of dm-do-adapter'
11
+ gem.description = "DataObjects Adapter for DataMapper"
12
+ gem.homepage = "http://datamapper.org"
13
+ gem.license = 'MIT'
14
+
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- {spec}/*`.split("\n")
17
+ gem.extra_rdoc_files = %w[LICENSE README.rdoc]
18
+ gem.require_paths = [ "lib" ]
19
+
20
+ gem.add_runtime_dependency 'data_objects', '~> 0.10.6'
21
+ gem.add_runtime_dependency 'ardm-core', '~> 1.2'
22
+
23
+ gem.add_development_dependency 'rake', '~> 0.9'
24
+ gem.add_development_dependency 'rspec', '~> 1.3'
25
+ end
@@ -0,0 +1 @@
1
+ require 'dm-do-adapter'
@@ -0,0 +1 @@
1
+ require 'dm-do-adapter/adapter'
@@ -0,0 +1,761 @@
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
+
113
+ result = with_connection do |connection|
114
+ connection.create_command(statement).execute_non_query(*bind_values)
115
+ end
116
+
117
+ if result.affected_rows == 1 && serial
118
+ serial.set!(resource, result.insert_id)
119
+ end
120
+ end
121
+ end
122
+
123
+ # Constructs and executes SELECT query, then instantiates
124
+ # one or many object from result set.
125
+ #
126
+ # @param [Query] query
127
+ # composition of the query to perform
128
+ #
129
+ # @return [Array]
130
+ # result set of the query
131
+ #
132
+ # @api semipublic
133
+ def read(query)
134
+ fields = query.fields
135
+ types = fields.map { |property| property.primitive }
136
+
137
+ statement, bind_values = select_statement(query)
138
+
139
+ records = []
140
+
141
+ with_connection do |connection|
142
+ command = connection.create_command(statement)
143
+ command.set_types(types)
144
+
145
+ # Handle different splat semantics for nil on 1.8 and 1.9
146
+ reader = if bind_values
147
+ command.execute_reader(*bind_values)
148
+ else
149
+ command.execute_reader
150
+ end
151
+
152
+ begin
153
+ while reader.next!
154
+ records << Hash[ fields.zip(reader.values) ]
155
+ end
156
+ ensure
157
+ reader.close
158
+ end
159
+ end
160
+
161
+ records
162
+ end
163
+
164
+ # Constructs and executes UPDATE statement for given
165
+ # attributes and a query
166
+ #
167
+ # @param [Hash(Property => Object)] attributes
168
+ # hash of attribute values to set, keyed by Property
169
+ # @param [Collection] collection
170
+ # collection of records to be updated
171
+ #
172
+ # @return [Integer]
173
+ # the number of records updated
174
+ #
175
+ # @api semipublic
176
+ def update(attributes, collection)
177
+ query = collection.query
178
+
179
+ properties = []
180
+ bind_values = []
181
+
182
+ # make the order of the properties consistent
183
+ query.model.properties(name).each do |property|
184
+ next unless attributes.key?(property)
185
+ properties << property
186
+ bind_values << attributes[property]
187
+ end
188
+
189
+ statement, conditions_bind_values = update_statement(properties, query)
190
+
191
+ bind_values.concat(conditions_bind_values)
192
+
193
+ with_connection do |connection|
194
+ connection.create_command(statement).execute_non_query(*bind_values)
195
+ end.affected_rows
196
+ end
197
+
198
+ # Constructs and executes DELETE statement for given query
199
+ #
200
+ # @param [Collection] collection
201
+ # collection of records to be deleted
202
+ #
203
+ # @return [Integer]
204
+ # the number of records deleted
205
+ #
206
+ # @api semipublic
207
+ def delete(collection)
208
+ query = collection.query
209
+ statement, bind_values = delete_statement(query)
210
+
211
+ with_connection do |connection|
212
+ connection.create_command(statement).execute_non_query(*bind_values)
213
+ end.affected_rows
214
+ end
215
+
216
+ protected
217
+
218
+ # @api private
219
+ def normalized_uri
220
+ @normalized_uri ||=
221
+ begin
222
+ keys = [
223
+ :adapter, :user, :password, :host, :port, :path, :fragment,
224
+ :scheme, :query, :username, :database ]
225
+ query = DataMapper::Ext::Hash.except(@options, keys)
226
+ query = nil if query.empty?
227
+
228
+ # Better error message in case port is no Numeric value
229
+ port = @options[:port].nil? ? nil : @options[:port].to_int
230
+
231
+ DataObjects::URI.new(
232
+ :scheme => @options[:adapter],
233
+ :user => @options[:user] || @options[:username],
234
+ :password => @options[:password],
235
+ :host => @options[:host],
236
+ :port => port,
237
+ :path => @options[:path] || @options[:database],
238
+ :query => query,
239
+ :fragment => @options[:fragment]
240
+ ).freeze
241
+ end
242
+ end
243
+
244
+ chainable do
245
+ protected
246
+
247
+ # Instantiates new connection object
248
+ #
249
+ # @api semipublic
250
+ def open_connection
251
+ DataObjects::Connection.new(normalized_uri)
252
+ end
253
+
254
+ # Takes connection and closes it
255
+ #
256
+ # @api semipublic
257
+ def close_connection(connection)
258
+ connection.close if connection.respond_to?(:close)
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ # @api public
265
+ def initialize(name, uri_or_options)
266
+ super
267
+
268
+ # Default the driver-specific logger to DataMapper's logger
269
+ if driver_module = DataObjects.const_get(normalized_uri.scheme.capitalize)
270
+ driver_module.logger = DataMapper.logger if driver_module.respond_to?(:logger=)
271
+ end
272
+ end
273
+
274
+ # @api private
275
+ def with_connection
276
+ yield connection = open_connection
277
+ rescue Exception => exception
278
+ DataMapper.logger.error(exception.to_s) if DataMapper.logger
279
+ raise
280
+ ensure
281
+ close_connection(connection)
282
+ end
283
+
284
+ # @api private
285
+ def select_fields(reader, fields)
286
+ fields = fields.map { |field| DataMapper::Inflector.underscore(field).to_sym }
287
+ struct = Struct.new(*fields)
288
+
289
+ results = []
290
+
291
+ while reader.next!
292
+ results << struct.new(*reader.values)
293
+ end
294
+
295
+ results
296
+ end
297
+
298
+ # @api private
299
+ def select_field(reader)
300
+ results = []
301
+
302
+ while reader.next!
303
+ results << reader.values.at(0)
304
+ end
305
+
306
+ results
307
+ end
308
+
309
+ # This module is just for organization. The methods are included into the
310
+ # Adapter below.
311
+ module SQL #:nodoc:
312
+ IDENTIFIER_MAX_LENGTH = 128
313
+
314
+ # @api semipublic
315
+ def property_to_column_name(property, qualify)
316
+ column_name = ''
317
+
318
+ case qualify
319
+ when true
320
+ column_name << "#{quote_name(property.model.storage_name(name))}."
321
+ when String
322
+ column_name << "#{quote_name(qualify)}."
323
+ end
324
+
325
+ column_name << quote_name(property.field)
326
+ end
327
+
328
+ private
329
+
330
+ # Adapters requiring a RETURNING syntax for INSERT statements
331
+ # should overwrite this to return true.
332
+ #
333
+ # @api private
334
+ def supports_returning?
335
+ false
336
+ end
337
+
338
+ # Adapters that do not support the DEFAULT VALUES syntax for
339
+ # INSERT statements should overwrite this to return false.
340
+ #
341
+ # @api private
342
+ def supports_default_values?
343
+ true
344
+ end
345
+
346
+ # Constructs SELECT statement for given query,
347
+ #
348
+ # @return [String] SELECT statement as a string
349
+ #
350
+ # @api private
351
+ def select_statement(query)
352
+ qualify = query.links.any?
353
+ fields = query.fields
354
+ order_by = query.order
355
+ group_by = if query.unique?
356
+ fields.select { |property| property.kind_of?(Property) }
357
+ end
358
+
359
+ conditions_statement, bind_values = conditions_statement(query.conditions, qualify)
360
+
361
+ statement = "SELECT #{columns_statement(fields, qualify)}"
362
+ statement << " FROM #{quote_name(query.model.storage_name(name))}"
363
+ statement << " #{join_statement(query, bind_values, qualify)}" if qualify
364
+ statement << " WHERE #{conditions_statement}" unless DataMapper::Ext.blank?(conditions_statement)
365
+ statement << " GROUP BY #{columns_statement(group_by, qualify)}" if group_by && group_by.any?
366
+ statement << " ORDER BY #{order_statement(order_by, qualify)}" if order_by && order_by.any?
367
+
368
+ add_limit_offset!(statement, query.limit, query.offset, bind_values)
369
+
370
+ return statement, bind_values
371
+ end
372
+
373
+ # default construction of LIMIT and OFFSET
374
+ # overriden by some adapters (currently Oracle and SQL Server)
375
+ def add_limit_offset!(statement, limit, offset, bind_values)
376
+ if limit
377
+ statement << ' LIMIT ?'
378
+ bind_values << limit
379
+ end
380
+
381
+ if limit && offset > 0
382
+ statement << ' OFFSET ?'
383
+ bind_values << offset
384
+ end
385
+ end
386
+
387
+ # Constructs INSERT statement for given query,
388
+ #
389
+ # @return [String] INSERT statement as a string
390
+ #
391
+ # @api private
392
+ def insert_statement(model, properties, serial)
393
+ statement = "INSERT INTO #{quote_name(model.storage_name(name))} "
394
+
395
+ if supports_default_values? && properties.empty?
396
+ statement << default_values_clause
397
+ else
398
+ statement << DataMapper::Ext::String.compress_lines(<<-SQL)
399
+ (#{properties.map { |property| quote_name(property.field) }.join(', ')})
400
+ VALUES
401
+ (#{(['?'] * properties.size).join(', ')})
402
+ SQL
403
+ end
404
+
405
+ if supports_returning? && serial
406
+ statement << returning_clause(serial)
407
+ end
408
+
409
+ statement
410
+ end
411
+
412
+ # by default PostgreSQL syntax
413
+ # overrided in Oracle adapter
414
+ def default_values_clause
415
+ 'DEFAULT VALUES'
416
+ end
417
+
418
+ # by default PostgreSQL syntax
419
+ # overrided in Oracle adapter
420
+ def returning_clause(serial)
421
+ " RETURNING #{quote_name(serial.field)}"
422
+ end
423
+
424
+ # Constructs UPDATE statement for given query,
425
+ #
426
+ # @return [String] UPDATE statement as a string
427
+ #
428
+ # @api private
429
+ def update_statement(properties, query)
430
+ model = query.model
431
+ name = self.name
432
+
433
+ # TODO: DRY this up with delete_statement
434
+ conditions_statement, bind_values = if query.limit || query.links.any?
435
+ subquery(query, model.key(name), false)
436
+ else
437
+ conditions_statement(query.conditions)
438
+ end
439
+
440
+ statement = "UPDATE #{quote_name(model.storage_name(name))}"
441
+ statement << " SET #{properties.map { |property| "#{quote_name(property.field)} = ?" }.join(', ')}"
442
+ statement << " WHERE #{conditions_statement}" unless DataMapper::Ext.blank?(conditions_statement)
443
+
444
+ return statement, bind_values
445
+ end
446
+
447
+ # Constructs DELETE statement for given query,
448
+ #
449
+ # @return [String] DELETE statement as a string
450
+ #
451
+ # @api private
452
+ def delete_statement(query)
453
+ model = query.model
454
+ name = self.name
455
+
456
+ # TODO: DRY this up with update_statement
457
+ conditions_statement, bind_values = if query.limit || query.links.any?
458
+ subquery(query, model.key(name), false)
459
+ else
460
+ conditions_statement(query.conditions)
461
+ end
462
+
463
+ statement = "DELETE FROM #{quote_name(model.storage_name(name))}"
464
+ statement << " WHERE #{conditions_statement}" unless DataMapper::Ext.blank?(conditions_statement)
465
+
466
+ return statement, bind_values
467
+ end
468
+
469
+ # Constructs comma separated list of fields
470
+ #
471
+ # @return [String]
472
+ # list of fields as a string
473
+ #
474
+ # @api private
475
+ def columns_statement(properties, qualify)
476
+ properties.map { |property| property_to_column_name(property, qualify) }.join(', ')
477
+ end
478
+
479
+ # Constructs joins clause
480
+ #
481
+ # @return [String]
482
+ # joins clause
483
+ #
484
+ # @api private
485
+ def join_statement(query, bind_values, qualify)
486
+ statements = []
487
+ join_bind_values = []
488
+
489
+ target_alias = query.model.storage_name(name)
490
+ seen = { target_alias => 0 }
491
+
492
+ query.links.reverse_each do |relationship|
493
+ target_alias = relationship.target_model.storage_name(name)
494
+ storage_name = relationship.source_model.storage_name(name)
495
+ source_alias = storage_name
496
+
497
+ statements << "INNER JOIN #{quote_name(storage_name)}"
498
+
499
+ if seen.key?(source_alias)
500
+ seen[source_alias] += 1
501
+ source_alias = "#{source_alias}_#{seen[source_alias]}"
502
+ statements << quote_name(source_alias)
503
+ else
504
+ seen[source_alias] = 0
505
+ end
506
+
507
+ statements << 'ON'
508
+
509
+ add_join_conditions(relationship, target_alias, source_alias, statements)
510
+ add_extra_join_conditions(relationship, target_alias, statements, join_bind_values)
511
+ end
512
+
513
+ # prepend the join bind values to the statement bind values
514
+ bind_values.unshift(*join_bind_values)
515
+
516
+ statements.join(' ')
517
+ end
518
+
519
+ def add_join_conditions(relationship, target_alias, source_alias, statements)
520
+ statements << relationship.target_key.zip(relationship.source_key).map do |target_property, source_property|
521
+ "#{property_to_column_name(target_property, target_alias)} = #{property_to_column_name(source_property, source_alias)}"
522
+ end.join(' AND ')
523
+ end
524
+
525
+ def add_extra_join_conditions(relationship, target_alias, statements, bind_values)
526
+ conditions = DataMapper.repository(name).scope do
527
+ relationship.target_model.all(relationship.query).query.conditions
528
+ end
529
+
530
+ return if conditions.nil?
531
+
532
+ extra_statement, extra_bind_values = conditions_statement(conditions, target_alias)
533
+ statements << "AND #{extra_statement}"
534
+ bind_values.concat(extra_bind_values)
535
+ end
536
+
537
+ # Constructs where clause
538
+ #
539
+ # @return [String]
540
+ # where clause
541
+ #
542
+ # @api private
543
+ def conditions_statement(conditions, qualify = false)
544
+ case conditions
545
+ when Query::Conditions::NotOperation then negate_operation(conditions.operand, qualify)
546
+ when Query::Conditions::AbstractOperation then operation_statement(conditions, qualify)
547
+ when Query::Conditions::AbstractComparison then comparison_statement(conditions, qualify)
548
+ when Array
549
+ statement, bind_values = conditions # handle raw conditions
550
+ [ "(#{statement})", bind_values ].compact
551
+ end
552
+ end
553
+
554
+ # @api private
555
+ def supports_subquery?(*)
556
+ true
557
+ end
558
+
559
+ # @api private
560
+ def subquery(query, subject, qualify)
561
+ source_key, target_key = subquery_keys(subject)
562
+
563
+ if query.repository.name == name && supports_subquery?(query, source_key, target_key, qualify)
564
+ subquery_statement(query, source_key, target_key, qualify)
565
+ else
566
+ subquery_execute(query, source_key, target_key, qualify)
567
+ end
568
+ end
569
+
570
+ # @api private
571
+ def subquery_statement(query, source_key, target_key, qualify)
572
+ query = subquery_query(query, source_key)
573
+ select_statement, bind_values = select_statement(query)
574
+
575
+ statement = if target_key.size == 1
576
+ property_to_column_name(target_key.first, qualify)
577
+ else
578
+ "(#{target_key.map { |property| property_to_column_name(property, qualify) }.join(', ')})"
579
+ end
580
+
581
+ statement << " IN (#{select_statement})"
582
+
583
+ return statement, bind_values
584
+ end
585
+
586
+ # @api private
587
+ def subquery_execute(query, source_key, target_key, qualify)
588
+ query = subquery_query(query, source_key)
589
+ sources = query.model.all(query)
590
+ conditions = Query.target_conditions(sources, source_key, target_key)
591
+
592
+ if conditions.valid?
593
+ conditions_statement(conditions, qualify)
594
+ else
595
+ [ '1 = 0', [] ]
596
+ end
597
+ end
598
+
599
+ # @api private
600
+ def subquery_keys(subject)
601
+ case subject
602
+ when Associations::Relationship
603
+ relationship = subject.inverse
604
+ [ relationship.source_key, relationship.target_key ]
605
+ when PropertySet
606
+ [ subject, subject ]
607
+ end
608
+ end
609
+
610
+ # @api private
611
+ def subquery_query(query, source_key)
612
+ # force unique to be false because PostgreSQL has a problem with
613
+ # subselects that contain a GROUP BY with different columns
614
+ # than the outer-most query
615
+ query = query.merge(:fields => source_key, :unique => false)
616
+ query.update(:order => nil) unless query.limit
617
+ query
618
+ end
619
+
620
+ # Constructs order clause
621
+ #
622
+ # @return [String]
623
+ # order clause
624
+ #
625
+ # @api private
626
+ def order_statement(order, qualify)
627
+ statements = order.map do |direction|
628
+ statement = property_to_column_name(direction.target, qualify)
629
+ statement << ' DESC' if direction.operator == :desc
630
+ statement
631
+ end
632
+
633
+ statements.join(', ')
634
+ end
635
+
636
+ # @api private
637
+ def negate_operation(operand, qualify)
638
+ statement, bind_values = conditions_statement(operand, qualify)
639
+ statement = "NOT(#{statement})" unless statement.nil?
640
+ [ statement, bind_values ]
641
+ end
642
+
643
+ # @api private
644
+ def operation_statement(operation, qualify)
645
+ statements = []
646
+ bind_values = []
647
+
648
+ operation.each do |operand|
649
+ statement, values = conditions_statement(operand, qualify)
650
+ next unless statement
651
+ statements << statement
652
+ bind_values.concat(values) if values
653
+ end
654
+
655
+ statement = statements.join(" #{operation.slug.to_s.upcase} ")
656
+
657
+ if statements.size > 1
658
+ statement = "(#{statement})"
659
+ end
660
+
661
+ return statement, bind_values
662
+ end
663
+
664
+ # Constructs comparison clause
665
+ #
666
+ # @return [String]
667
+ # comparison clause
668
+ #
669
+ # @api private
670
+ def comparison_statement(comparison, qualify)
671
+ subject = comparison.subject
672
+ value = comparison.value
673
+
674
+ # TODO: move exclusive Range handling into another method, and
675
+ # update conditions_statement to use it
676
+
677
+ # break exclusive Range queries up into two comparisons ANDed together
678
+ if value.kind_of?(Range) && value.exclude_end?
679
+ operation = Query::Conditions::Operation.new(:and,
680
+ Query::Conditions::Comparison.new(:gte, subject, value.first),
681
+ Query::Conditions::Comparison.new(:lt, subject, value.last)
682
+ )
683
+
684
+ statement, bind_values = conditions_statement(operation, qualify)
685
+
686
+ return "(#{statement})", bind_values
687
+ elsif comparison.relationship?
688
+ if value.respond_to?(:query) && value.respond_to?(:loaded?) && !value.loaded?
689
+ return subquery(value.query, subject, qualify)
690
+ else
691
+ return conditions_statement(comparison.foreign_key_mapping, qualify)
692
+ end
693
+ elsif comparison.slug == :in && !value.any?
694
+ return [] # match everything
695
+ end
696
+
697
+ operator = comparison_operator(comparison)
698
+ column_name = property_to_column_name(subject, qualify)
699
+
700
+ # if operator return value contains ? then it means that it is function call
701
+ # and it contains placeholder (%s) for property name as well (used in Oracle adapter for regexp operator)
702
+ if operator.include?('?')
703
+ return operator % column_name, [ value ]
704
+ else
705
+ return "#{column_name} #{operator} #{value.nil? ? 'NULL' : '?'}", [ value ].compact
706
+ end
707
+ end
708
+
709
+ def comparison_operator(comparison)
710
+ subject = comparison.subject
711
+ value = comparison.value
712
+
713
+ case comparison.slug
714
+ when :eql then equality_operator(subject, value)
715
+ when :in then include_operator(subject, value)
716
+ when :regexp then regexp_operator(value)
717
+ when :like then like_operator(value)
718
+ when :gt then '>'
719
+ when :lt then '<'
720
+ when :gte then '>='
721
+ when :lte then '<='
722
+ end
723
+ end
724
+
725
+ # @api private
726
+ def equality_operator(property, operand)
727
+ operand.nil? ? 'IS' : '='
728
+ end
729
+
730
+ # @api private
731
+ def include_operator(property, operand)
732
+ case operand
733
+ when Array then 'IN'
734
+ when Range then 'BETWEEN'
735
+ end
736
+ end
737
+
738
+ # @api private
739
+ def regexp_operator(operand)
740
+ '~'
741
+ end
742
+
743
+ # @api private
744
+ def like_operator(operand)
745
+ 'LIKE'
746
+ end
747
+
748
+ # @api private
749
+ def quote_name(name)
750
+ "\"#{name[0, self.class::IDENTIFIER_MAX_LENGTH].gsub('"', '""')}\""
751
+ end
752
+
753
+ end
754
+
755
+ include SQL
756
+
757
+ end
758
+
759
+ const_added(:DataObjectsAdapter)
760
+ end
761
+ end