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 +7 -0
- data/.gitignore +35 -0
- data/Gemfile +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +6 -0
- data/ardm-do-adapter.gemspec +25 -0
- data/lib/ardm-do-adapter.rb +1 -0
- data/lib/dm-do-adapter.rb +1 -0
- data/lib/dm-do-adapter/adapter.rb +761 -0
- data/lib/dm-do-adapter/spec/shared_spec.rb +415 -0
- data/lib/dm-do-adapter/version.rb +5 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +118 -0
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
data/Rakefile
ADDED
@@ -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
|