ardm-do-adapter 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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