rpbertp13-dm-core 0.9.11.1
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.
- data/.autotest +26 -0
- data/.gitignore +18 -0
- data/CONTRIBUTING +51 -0
- data/FAQ +92 -0
- data/History.txt +52 -0
- data/MIT-LICENSE +22 -0
- data/Manifest.txt +130 -0
- data/QUICKLINKS +11 -0
- data/README.txt +143 -0
- data/Rakefile +32 -0
- data/SPECS +62 -0
- data/TODO +1 -0
- data/dm-core.gemspec +40 -0
- data/lib/dm-core.rb +217 -0
- data/lib/dm-core/adapters.rb +16 -0
- data/lib/dm-core/adapters/abstract_adapter.rb +209 -0
- data/lib/dm-core/adapters/data_objects_adapter.rb +716 -0
- data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
- data/lib/dm-core/adapters/mysql_adapter.rb +138 -0
- data/lib/dm-core/adapters/postgres_adapter.rb +189 -0
- data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
- data/lib/dm-core/associations.rb +207 -0
- data/lib/dm-core/associations/many_to_many.rb +147 -0
- data/lib/dm-core/associations/many_to_one.rb +107 -0
- data/lib/dm-core/associations/one_to_many.rb +315 -0
- data/lib/dm-core/associations/one_to_one.rb +61 -0
- data/lib/dm-core/associations/relationship.rb +221 -0
- data/lib/dm-core/associations/relationship_chain.rb +81 -0
- data/lib/dm-core/auto_migrations.rb +105 -0
- data/lib/dm-core/collection.rb +670 -0
- data/lib/dm-core/dependency_queue.rb +32 -0
- data/lib/dm-core/hook.rb +11 -0
- data/lib/dm-core/identity_map.rb +42 -0
- data/lib/dm-core/is.rb +16 -0
- data/lib/dm-core/logger.rb +232 -0
- data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
- data/lib/dm-core/migrator.rb +29 -0
- data/lib/dm-core/model.rb +526 -0
- data/lib/dm-core/naming_conventions.rb +84 -0
- data/lib/dm-core/property.rb +676 -0
- data/lib/dm-core/property_set.rb +169 -0
- data/lib/dm-core/query.rb +676 -0
- data/lib/dm-core/repository.rb +167 -0
- data/lib/dm-core/resource.rb +671 -0
- data/lib/dm-core/scope.rb +58 -0
- data/lib/dm-core/support.rb +7 -0
- data/lib/dm-core/support/array.rb +13 -0
- data/lib/dm-core/support/assertions.rb +8 -0
- data/lib/dm-core/support/errors.rb +23 -0
- data/lib/dm-core/support/kernel.rb +11 -0
- data/lib/dm-core/support/symbol.rb +41 -0
- data/lib/dm-core/transaction.rb +252 -0
- data/lib/dm-core/type.rb +160 -0
- data/lib/dm-core/type_map.rb +80 -0
- data/lib/dm-core/types.rb +19 -0
- data/lib/dm-core/types/boolean.rb +7 -0
- data/lib/dm-core/types/discriminator.rb +34 -0
- data/lib/dm-core/types/object.rb +24 -0
- data/lib/dm-core/types/paranoid_boolean.rb +34 -0
- data/lib/dm-core/types/paranoid_datetime.rb +33 -0
- data/lib/dm-core/types/serial.rb +9 -0
- data/lib/dm-core/types/text.rb +10 -0
- data/lib/dm-core/version.rb +3 -0
- data/script/all +4 -0
- data/script/performance.rb +282 -0
- data/script/profile.rb +87 -0
- data/spec/integration/association_spec.rb +1382 -0
- data/spec/integration/association_through_spec.rb +203 -0
- data/spec/integration/associations/many_to_many_spec.rb +449 -0
- data/spec/integration/associations/many_to_one_spec.rb +163 -0
- data/spec/integration/associations/one_to_many_spec.rb +188 -0
- data/spec/integration/auto_migrations_spec.rb +413 -0
- data/spec/integration/collection_spec.rb +1073 -0
- data/spec/integration/data_objects_adapter_spec.rb +32 -0
- data/spec/integration/dependency_queue_spec.rb +46 -0
- data/spec/integration/model_spec.rb +197 -0
- data/spec/integration/mysql_adapter_spec.rb +85 -0
- data/spec/integration/postgres_adapter_spec.rb +731 -0
- data/spec/integration/property_spec.rb +253 -0
- data/spec/integration/query_spec.rb +514 -0
- data/spec/integration/repository_spec.rb +61 -0
- data/spec/integration/resource_spec.rb +513 -0
- data/spec/integration/sqlite3_adapter_spec.rb +352 -0
- data/spec/integration/sti_spec.rb +273 -0
- data/spec/integration/strategic_eager_loading_spec.rb +156 -0
- data/spec/integration/transaction_spec.rb +60 -0
- data/spec/integration/type_spec.rb +275 -0
- data/spec/lib/logging_helper.rb +18 -0
- data/spec/lib/mock_adapter.rb +27 -0
- data/spec/lib/model_loader.rb +100 -0
- data/spec/lib/publicize_methods.rb +28 -0
- data/spec/models/content.rb +16 -0
- data/spec/models/vehicles.rb +34 -0
- data/spec/models/zoo.rb +48 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +91 -0
- data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
- data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
- data/spec/unit/adapters/data_objects_adapter_spec.rb +632 -0
- data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
- data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
- data/spec/unit/associations/many_to_many_spec.rb +32 -0
- data/spec/unit/associations/many_to_one_spec.rb +159 -0
- data/spec/unit/associations/one_to_many_spec.rb +393 -0
- data/spec/unit/associations/one_to_one_spec.rb +7 -0
- data/spec/unit/associations/relationship_spec.rb +71 -0
- data/spec/unit/associations_spec.rb +242 -0
- data/spec/unit/auto_migrations_spec.rb +111 -0
- data/spec/unit/collection_spec.rb +182 -0
- data/spec/unit/data_mapper_spec.rb +35 -0
- data/spec/unit/identity_map_spec.rb +126 -0
- data/spec/unit/is_spec.rb +80 -0
- data/spec/unit/migrator_spec.rb +33 -0
- data/spec/unit/model_spec.rb +321 -0
- data/spec/unit/naming_conventions_spec.rb +36 -0
- data/spec/unit/property_set_spec.rb +90 -0
- data/spec/unit/property_spec.rb +753 -0
- data/spec/unit/query_spec.rb +571 -0
- data/spec/unit/repository_spec.rb +93 -0
- data/spec/unit/resource_spec.rb +649 -0
- data/spec/unit/scope_spec.rb +142 -0
- data/spec/unit/transaction_spec.rb +469 -0
- data/spec/unit/type_map_spec.rb +114 -0
- data/spec/unit/type_spec.rb +119 -0
- data/tasks/ci.rb +36 -0
- data/tasks/dm.rb +63 -0
- data/tasks/doc.rb +20 -0
- data/tasks/gemspec.rb +23 -0
- data/tasks/hoe.rb +46 -0
- data/tasks/install.rb +20 -0
- metadata +215 -0
@@ -0,0 +1,716 @@
|
|
1
|
+
gem 'data_objects', '~>0.10.0'
|
2
|
+
require 'data_objects'
|
3
|
+
|
4
|
+
module DataMapper
|
5
|
+
module Adapters
|
6
|
+
# You must inherit from the DoAdapter, and implement the
|
7
|
+
# required methods to adapt a database library for use with the DataMapper.
|
8
|
+
#
|
9
|
+
# NOTE: By inheriting from DataObjectsAdapter, you get a copy of all the
|
10
|
+
# standard sub-modules (Quoting, Coersion and Queries) in your own Adapter.
|
11
|
+
# You can extend and overwrite these copies without affecting the originals.
|
12
|
+
class DataObjectsAdapter < AbstractAdapter
|
13
|
+
def create(resources)
|
14
|
+
created = 0
|
15
|
+
resources.each do |resource|
|
16
|
+
repository = resource.repository
|
17
|
+
model = resource.model
|
18
|
+
attributes = resource.dirty_attributes
|
19
|
+
|
20
|
+
# TODO: make a model.identity_field method
|
21
|
+
identity_field = model.key(repository.name).detect { |p| p.serial? }
|
22
|
+
|
23
|
+
statement = create_statement(repository, model, attributes.keys, identity_field)
|
24
|
+
bind_values = attributes.values
|
25
|
+
|
26
|
+
result = execute(statement, *bind_values)
|
27
|
+
|
28
|
+
if result.to_i == 1
|
29
|
+
if identity_field
|
30
|
+
identity_field.set!(resource, result.insert_id)
|
31
|
+
end
|
32
|
+
created += 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
created
|
36
|
+
end
|
37
|
+
|
38
|
+
def read_many(query)
|
39
|
+
Collection.new(query) do |collection|
|
40
|
+
with_connection do |connection|
|
41
|
+
command = connection.create_command(read_statement(query))
|
42
|
+
command.set_types(query.fields.map { |p| p.primitive })
|
43
|
+
|
44
|
+
begin
|
45
|
+
bind_values = query.bind_values.map do |v|
|
46
|
+
v == [] ? [nil] : v
|
47
|
+
end
|
48
|
+
reader = command.execute_reader(*bind_values)
|
49
|
+
|
50
|
+
while(reader.next!)
|
51
|
+
collection.load(reader.values)
|
52
|
+
end
|
53
|
+
ensure
|
54
|
+
reader.close if reader
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def read_one(query)
|
61
|
+
with_connection do |connection|
|
62
|
+
command = connection.create_command(read_statement(query))
|
63
|
+
command.set_types(query.fields.map { |p| p.primitive })
|
64
|
+
|
65
|
+
begin
|
66
|
+
reader = command.execute_reader(*query.bind_values)
|
67
|
+
|
68
|
+
if reader.next!
|
69
|
+
query.model.load(reader.values, query)
|
70
|
+
end
|
71
|
+
ensure
|
72
|
+
reader.close if reader
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def update(attributes, query)
|
78
|
+
statement = update_statement(attributes.keys, query)
|
79
|
+
bind_values = attributes.values + query.bind_values
|
80
|
+
execute(statement, *bind_values).to_i
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete(query)
|
84
|
+
statement = delete_statement(query)
|
85
|
+
execute(statement, *query.bind_values).to_i
|
86
|
+
end
|
87
|
+
|
88
|
+
# Database-specific method
|
89
|
+
def execute(statement, *bind_values)
|
90
|
+
with_connection do |connection|
|
91
|
+
command = connection.create_command(statement)
|
92
|
+
command.execute_non_query(*bind_values)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def query(statement, *bind_values)
|
97
|
+
with_reader(statement, bind_values) do |reader|
|
98
|
+
results = []
|
99
|
+
|
100
|
+
if (fields = reader.fields).size > 1
|
101
|
+
fields = fields.map { |field| Extlib::Inflection.underscore(field).to_sym }
|
102
|
+
struct = Struct.new(*fields)
|
103
|
+
|
104
|
+
while(reader.next!) do
|
105
|
+
results << struct.new(*reader.values)
|
106
|
+
end
|
107
|
+
else
|
108
|
+
while(reader.next!) do
|
109
|
+
results << reader.values.at(0)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
results
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
protected
|
118
|
+
|
119
|
+
def normalize_uri(uri_or_options)
|
120
|
+
if uri_or_options.kind_of?(String) || uri_or_options.kind_of?(Addressable::URI)
|
121
|
+
uri_or_options = DataObjects::URI.parse(uri_or_options)
|
122
|
+
end
|
123
|
+
|
124
|
+
if uri_or_options.kind_of?(DataObjects::URI)
|
125
|
+
return uri_or_options
|
126
|
+
end
|
127
|
+
|
128
|
+
query = uri_or_options.except(:adapter, :username, :password, :host, :port, :database).map { |pair| pair.join('=') }.join('&')
|
129
|
+
query = nil if query.blank?
|
130
|
+
|
131
|
+
return DataObjects::URI.parse(Addressable::URI.new(
|
132
|
+
:scheme => uri_or_options[:adapter].to_s,
|
133
|
+
:user => uri_or_options[:username],
|
134
|
+
:password => uri_or_options[:password],
|
135
|
+
:host => uri_or_options[:host],
|
136
|
+
:port => uri_or_options[:port],
|
137
|
+
:path => uri_or_options[:database],
|
138
|
+
:query => query
|
139
|
+
))
|
140
|
+
end
|
141
|
+
|
142
|
+
# TODO: clean up once transaction related methods move to dm-more/dm-transactions
|
143
|
+
def create_connection
|
144
|
+
if within_transaction?
|
145
|
+
current_transaction.primitive_for(self).connection
|
146
|
+
else
|
147
|
+
# DataObjects::Connection.new(uri) will give you back the right
|
148
|
+
# driver based on the Uri#scheme.
|
149
|
+
DataObjects::Connection.new(@uri)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# TODO: clean up once transaction related methods move to dm-more/dm-transactions
|
154
|
+
def close_connection(connection)
|
155
|
+
connection.close unless within_transaction? && current_transaction.primitive_for(self).connection == connection
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def initialize(name, uri_or_options)
|
161
|
+
super
|
162
|
+
|
163
|
+
# Default the driver-specifc logger to DataMapper's logger
|
164
|
+
if driver_module = DataObjects.const_get(@uri.scheme.capitalize) rescue nil
|
165
|
+
driver_module.logger = DataMapper.logger if driver_module.respond_to?(:logger=)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def with_connection
|
170
|
+
connection = nil
|
171
|
+
begin
|
172
|
+
connection = create_connection
|
173
|
+
return yield(connection)
|
174
|
+
rescue => e
|
175
|
+
DataMapper.logger.error(e.to_s)
|
176
|
+
raise e
|
177
|
+
ensure
|
178
|
+
close_connection(connection) if connection
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def with_reader(statement, bind_values = [])
|
183
|
+
with_connection do |connection|
|
184
|
+
reader = nil
|
185
|
+
begin
|
186
|
+
reader = connection.create_command(statement).execute_reader(*bind_values)
|
187
|
+
return yield(reader)
|
188
|
+
ensure
|
189
|
+
reader.close if reader
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# This model is just for organization. The methods are included into the
|
195
|
+
# Adapter below.
|
196
|
+
module SQL
|
197
|
+
private
|
198
|
+
|
199
|
+
# Adapters requiring a RETURNING syntax for INSERT statements
|
200
|
+
# should overwrite this to return true.
|
201
|
+
def supports_returning?
|
202
|
+
false
|
203
|
+
end
|
204
|
+
|
205
|
+
# Adapters that do not support the DEFAULT VALUES syntax for
|
206
|
+
# INSERT statements should overwrite this to return false.
|
207
|
+
def supports_default_values?
|
208
|
+
true
|
209
|
+
end
|
210
|
+
|
211
|
+
def create_statement(repository, model, properties, identity_field)
|
212
|
+
statement = "INSERT INTO #{quote_table_name(model.storage_name(repository.name))} "
|
213
|
+
|
214
|
+
if supports_default_values? && properties.empty?
|
215
|
+
statement << 'DEFAULT VALUES'
|
216
|
+
else
|
217
|
+
statement << <<-EOS.compress_lines
|
218
|
+
(#{properties.map { |p| quote_column_name(p.field(repository.name)) } * ', '})
|
219
|
+
VALUES
|
220
|
+
(#{(['?'] * properties.size) * ', '})
|
221
|
+
EOS
|
222
|
+
end
|
223
|
+
|
224
|
+
if supports_returning? && identity_field
|
225
|
+
statement << " RETURNING #{quote_column_name(identity_field.field(repository.name))}"
|
226
|
+
end
|
227
|
+
|
228
|
+
statement
|
229
|
+
end
|
230
|
+
|
231
|
+
def read_statement(query)
|
232
|
+
statement = "SELECT #{fields_statement(query)}"
|
233
|
+
statement << " FROM #{quote_table_name(query.model.storage_name(query.repository.name))}"
|
234
|
+
statement << links_statement(query) if query.links.any?
|
235
|
+
statement << " WHERE #{conditions_statement(query)}" if query.conditions.any?
|
236
|
+
statement << " GROUP BY #{group_by_statement(query)}" if query.unique? && query.fields.any? { |p| p.kind_of?(Property) }
|
237
|
+
statement << " ORDER BY #{order_statement(query)}" if query.order.any?
|
238
|
+
statement << " LIMIT #{quote_column_value(query.limit)}" if query.limit
|
239
|
+
statement << " OFFSET #{quote_column_value(query.offset)}" if query.offset && query.offset > 0
|
240
|
+
statement
|
241
|
+
rescue => e
|
242
|
+
DataMapper.logger.error("QUERY INVALID: #{query.inspect} (#{e})")
|
243
|
+
raise e
|
244
|
+
end
|
245
|
+
|
246
|
+
def update_statement(properties, query)
|
247
|
+
statement = "UPDATE #{quote_table_name(query.model.storage_name(query.repository.name))}"
|
248
|
+
statement << " SET #{set_statement(query.repository, properties)}"
|
249
|
+
statement << " WHERE #{conditions_statement(query)}" if query.conditions.any?
|
250
|
+
statement
|
251
|
+
end
|
252
|
+
|
253
|
+
def set_statement(repository, properties)
|
254
|
+
properties.map { |p| "#{quote_column_name(p.field(repository.name))} = ?" } * ', '
|
255
|
+
end
|
256
|
+
|
257
|
+
def delete_statement(query)
|
258
|
+
statement = "DELETE FROM #{quote_table_name(query.model.storage_name(query.repository.name))}"
|
259
|
+
statement << " WHERE #{conditions_statement(query)}" if query.conditions.any?
|
260
|
+
statement
|
261
|
+
end
|
262
|
+
|
263
|
+
def fields_statement(query)
|
264
|
+
qualify = query.links.any?
|
265
|
+
query.fields.map { |p| property_to_column_name(query.repository, p, qualify) } * ', '
|
266
|
+
end
|
267
|
+
|
268
|
+
def links_statement(query)
|
269
|
+
table_list = [query.model.storage_name(query.repository.name)]
|
270
|
+
|
271
|
+
statement = ''
|
272
|
+
query.links.each do |relationship|
|
273
|
+
parent_table_name = relationship.parent_model.storage_name(query.repository.name)
|
274
|
+
child_table_name = relationship.child_model.storage_name(query.repository.name)
|
275
|
+
|
276
|
+
join_table_name = if table_list.include?(parent_table_name)
|
277
|
+
child_table_name
|
278
|
+
elsif table_list.include?(child_table_name)
|
279
|
+
parent_table_name
|
280
|
+
else
|
281
|
+
raise ArgumentError, 'you\'re trying to join a table with no connection to this query'
|
282
|
+
end
|
283
|
+
table_list << join_table_name
|
284
|
+
|
285
|
+
# We only do INNER JOIN for now
|
286
|
+
statement << " INNER JOIN #{quote_table_name(join_table_name)} ON "
|
287
|
+
|
288
|
+
statement << relationship.parent_key.zip(relationship.child_key).map do |parent_property,child_property|
|
289
|
+
condition_statement(query, :eql, parent_property, child_property)
|
290
|
+
end * ' AND '
|
291
|
+
end
|
292
|
+
|
293
|
+
statement
|
294
|
+
end
|
295
|
+
|
296
|
+
def conditions_statement(query)
|
297
|
+
query.conditions.map { |o,p,b| condition_statement(query, o, p, b) } * ' AND '
|
298
|
+
end
|
299
|
+
|
300
|
+
def group_by_statement(query)
|
301
|
+
repository = query.repository
|
302
|
+
qualify = query.links.any?
|
303
|
+
query.fields.select { |p| p.kind_of?(Property) }.map { |p| property_to_column_name(repository, p, qualify) } * ', '
|
304
|
+
end
|
305
|
+
|
306
|
+
def order_statement(query)
|
307
|
+
repository = query.repository
|
308
|
+
qualify = query.links.any?
|
309
|
+
query.order.map { |i| order_column(repository, i, qualify) } * ', '
|
310
|
+
end
|
311
|
+
|
312
|
+
def order_column(repository, item, qualify)
|
313
|
+
property, descending = nil, false
|
314
|
+
|
315
|
+
case item
|
316
|
+
when Property
|
317
|
+
property = item
|
318
|
+
when Query::Direction
|
319
|
+
property = item.property
|
320
|
+
descending = true if item.direction == :desc
|
321
|
+
end
|
322
|
+
|
323
|
+
order_column = property_to_column_name(repository, property, qualify)
|
324
|
+
order_column << ' DESC' if descending
|
325
|
+
order_column
|
326
|
+
end
|
327
|
+
|
328
|
+
def condition_statement(query, operator, left_condition, right_condition)
|
329
|
+
return left_condition if operator == :raw
|
330
|
+
|
331
|
+
qualify = query.links.any?
|
332
|
+
|
333
|
+
conditions = [ left_condition, right_condition ].map do |condition|
|
334
|
+
if condition.kind_of?(Property) || condition.kind_of?(Query::Path)
|
335
|
+
property_to_column_name(query.repository, condition, qualify)
|
336
|
+
elsif condition.kind_of?(Query)
|
337
|
+
opposite = condition == left_condition ? right_condition : left_condition
|
338
|
+
query.merge_subquery(operator, opposite, condition)
|
339
|
+
"(#{read_statement(condition)})"
|
340
|
+
|
341
|
+
# [].all? is always true
|
342
|
+
elsif condition.kind_of?(Array) && condition.any? && condition.all? { |p| p.kind_of?(Property) }
|
343
|
+
property_values = condition.map { |p| property_to_column_name(query.repository, p, qualify) }
|
344
|
+
"(#{property_values * ', '})"
|
345
|
+
else
|
346
|
+
'?'
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
comparison = case operator
|
351
|
+
when :eql, :in then equality_operator(right_condition)
|
352
|
+
when :not then inequality_operator(right_condition)
|
353
|
+
when :like then 'LIKE'
|
354
|
+
when :gt then '>'
|
355
|
+
when :gte then '>='
|
356
|
+
when :lt then '<'
|
357
|
+
when :lte then '<='
|
358
|
+
else raise "Invalid query operator: #{operator.inspect}"
|
359
|
+
end
|
360
|
+
|
361
|
+
"(" + (conditions * " #{comparison} ") + ")"
|
362
|
+
end
|
363
|
+
|
364
|
+
def equality_operator(operand)
|
365
|
+
case operand
|
366
|
+
when Array, Query then 'IN'
|
367
|
+
when Range then 'BETWEEN'
|
368
|
+
when NilClass then 'IS'
|
369
|
+
else '='
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def inequality_operator(operand)
|
374
|
+
case operand
|
375
|
+
when Array, Query then 'NOT IN'
|
376
|
+
when Range then 'NOT BETWEEN'
|
377
|
+
when NilClass then 'IS NOT'
|
378
|
+
else '<>'
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def property_to_column_name(repository, property, qualify)
|
383
|
+
table_name = property.model.storage_name(repository.name) if property && property.respond_to?(:model)
|
384
|
+
|
385
|
+
if table_name && qualify
|
386
|
+
"#{quote_table_name(table_name)}.#{quote_column_name(property.field(repository.name))}"
|
387
|
+
else
|
388
|
+
quote_column_name(property.field(repository.name))
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# TODO: once the driver's quoting methods become public, have
|
393
|
+
# this method delegate to them instead
|
394
|
+
def quote_table_name(table_name)
|
395
|
+
table_name.gsub('"', '""').split('.').map { |part| "\"#{part}\"" } * '.'
|
396
|
+
end
|
397
|
+
|
398
|
+
# TODO: once the driver's quoting methods become public, have
|
399
|
+
# this method delegate to them instead
|
400
|
+
def quote_column_name(column_name)
|
401
|
+
"\"#{column_name.gsub('"', '""')}\""
|
402
|
+
end
|
403
|
+
|
404
|
+
# TODO: once the driver's quoting methods become public, have
|
405
|
+
# this method delegate to them instead
|
406
|
+
def quote_column_value(column_value)
|
407
|
+
return 'NULL' if column_value.nil?
|
408
|
+
|
409
|
+
case column_value
|
410
|
+
when String
|
411
|
+
if (integer = column_value.to_i).to_s == column_value
|
412
|
+
quote_column_value(integer)
|
413
|
+
elsif (float = column_value.to_f).to_s == column_value
|
414
|
+
quote_column_value(integer)
|
415
|
+
else
|
416
|
+
"'#{column_value.gsub("'", "''")}'"
|
417
|
+
end
|
418
|
+
when DateTime
|
419
|
+
quote_column_value(column_value.strftime('%Y-%m-%d %H:%M:%S'))
|
420
|
+
when Date
|
421
|
+
quote_column_value(column_value.strftime('%Y-%m-%d'))
|
422
|
+
when Time
|
423
|
+
quote_column_value(column_value.strftime('%Y-%m-%d %H:%M:%S') + ((column_value.usec > 0 ? ".#{column_value.usec.to_s.rjust(6, '0')}" : '')))
|
424
|
+
when Integer, Float
|
425
|
+
column_value.to_s
|
426
|
+
when BigDecimal
|
427
|
+
column_value.to_s('F')
|
428
|
+
else
|
429
|
+
column_value.to_s
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end #module SQL
|
433
|
+
|
434
|
+
include SQL
|
435
|
+
|
436
|
+
# TODO: move to dm-more/dm-migrations
|
437
|
+
module Migration
|
438
|
+
# TODO: move to dm-more/dm-migrations
|
439
|
+
def upgrade_model_storage(repository, model)
|
440
|
+
table_name = model.storage_name(repository.name)
|
441
|
+
|
442
|
+
if success = create_model_storage(repository, model)
|
443
|
+
return model.properties(repository.name)
|
444
|
+
end
|
445
|
+
|
446
|
+
properties = []
|
447
|
+
|
448
|
+
model.properties(repository.name).each do |property|
|
449
|
+
schema_hash = property_schema_hash(repository, property)
|
450
|
+
next if field_exists?(table_name, schema_hash[:name])
|
451
|
+
statement = alter_table_add_column_statement(table_name, schema_hash)
|
452
|
+
execute(statement)
|
453
|
+
properties << property
|
454
|
+
end
|
455
|
+
|
456
|
+
properties
|
457
|
+
end
|
458
|
+
|
459
|
+
# TODO: move to dm-more/dm-migrations
|
460
|
+
def create_model_storage(repository, model)
|
461
|
+
return false if storage_exists?(model.storage_name(repository.name))
|
462
|
+
|
463
|
+
execute(create_table_statement(repository, model))
|
464
|
+
|
465
|
+
(create_index_statements(repository, model) + create_unique_index_statements(repository, model)).each do |sql|
|
466
|
+
execute(sql)
|
467
|
+
end
|
468
|
+
|
469
|
+
true
|
470
|
+
end
|
471
|
+
|
472
|
+
# TODO: move to dm-more/dm-migrations
|
473
|
+
def destroy_model_storage(repository, model)
|
474
|
+
execute(drop_table_statement(repository, model))
|
475
|
+
true
|
476
|
+
end
|
477
|
+
|
478
|
+
# TODO: move to dm-more/dm-transactions
|
479
|
+
def transaction_primitive
|
480
|
+
DataObjects::Transaction.create_for_uri(@uri)
|
481
|
+
end
|
482
|
+
|
483
|
+
module SQL
|
484
|
+
private
|
485
|
+
|
486
|
+
# Adapters that support AUTO INCREMENT fields for CREATE TABLE
|
487
|
+
# statements should overwrite this to return true
|
488
|
+
#
|
489
|
+
# TODO: move to dm-more/dm-migrations
|
490
|
+
def supports_serial?
|
491
|
+
false
|
492
|
+
end
|
493
|
+
|
494
|
+
# TODO: move to dm-more/dm-migrations
|
495
|
+
def alter_table_add_column_statement(table_name, schema_hash)
|
496
|
+
"ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{property_schema_statement(schema_hash)}"
|
497
|
+
end
|
498
|
+
|
499
|
+
# TODO: move to dm-more/dm-migrations
|
500
|
+
def create_table_statement(repository, model)
|
501
|
+
repository_name = repository.name
|
502
|
+
|
503
|
+
statement = <<-EOS.compress_lines
|
504
|
+
CREATE TABLE #{quote_table_name(model.storage_name(repository_name))}
|
505
|
+
(#{model.properties_with_subclasses(repository_name).map { |p| property_schema_statement(property_schema_hash(repository, p)) } * ', '}
|
506
|
+
EOS
|
507
|
+
|
508
|
+
if (key = model.key(repository_name)).any?
|
509
|
+
statement << ", PRIMARY KEY(#{ key.map { |p| quote_column_name(p.field(repository_name)) } * ', '})"
|
510
|
+
end
|
511
|
+
|
512
|
+
statement << ')'
|
513
|
+
statement
|
514
|
+
end
|
515
|
+
|
516
|
+
# TODO: move to dm-more/dm-migrations
|
517
|
+
def drop_table_statement(repository, model)
|
518
|
+
"DROP TABLE IF EXISTS #{quote_table_name(model.storage_name(repository.name))}"
|
519
|
+
end
|
520
|
+
|
521
|
+
# TODO: move to dm-more/dm-migrations
|
522
|
+
def create_index_statements(repository, model)
|
523
|
+
table_name = model.storage_name(repository.name)
|
524
|
+
model.properties(repository.name).indexes.map do |index_name, fields|
|
525
|
+
<<-EOS.compress_lines
|
526
|
+
CREATE INDEX #{quote_column_name("index_#{table_name}_#{index_name}")} ON
|
527
|
+
#{quote_table_name(table_name)} (#{fields.map { |f| quote_column_name(f) } * ', '})
|
528
|
+
EOS
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# TODO: move to dm-more/dm-migrations
|
533
|
+
def create_unique_index_statements(repository, model)
|
534
|
+
table_name = model.storage_name(repository.name)
|
535
|
+
model.properties(repository.name).unique_indexes.map do |index_name, fields|
|
536
|
+
<<-EOS.compress_lines
|
537
|
+
CREATE UNIQUE INDEX #{quote_column_name("unique_index_#{table_name}_#{index_name}")} ON
|
538
|
+
#{quote_table_name(table_name)} (#{fields.map { |f| quote_column_name(f) } * ', '})
|
539
|
+
EOS
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# TODO: move to dm-more/dm-migrations
|
544
|
+
def property_schema_hash(repository, property)
|
545
|
+
schema = self.class.type_map[property.type].merge(:name => property.field(repository.name))
|
546
|
+
# TODO: figure out a way to specify the size not be included, even if
|
547
|
+
# a default is defined in the typemap
|
548
|
+
# - use this to make it so all TEXT primitive fields do not have size
|
549
|
+
if property.primitive == String && schema[:primitive] != 'TEXT'
|
550
|
+
schema[:size] = property.length
|
551
|
+
elsif property.primitive == BigDecimal || property.primitive == Float
|
552
|
+
schema[:precision] = property.precision
|
553
|
+
schema[:scale] = property.scale
|
554
|
+
end
|
555
|
+
|
556
|
+
schema[:nullable?] = property.nullable?
|
557
|
+
schema[:serial?] = property.serial?
|
558
|
+
|
559
|
+
if property.default.nil? || property.default.respond_to?(:call)
|
560
|
+
# remove the default if the property is not nullable
|
561
|
+
schema.delete(:default) unless property.nullable?
|
562
|
+
else
|
563
|
+
if property.type.respond_to?(:dump)
|
564
|
+
schema[:default] = property.type.dump(property.default, property)
|
565
|
+
else
|
566
|
+
schema[:default] = property.default
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
schema
|
571
|
+
end
|
572
|
+
|
573
|
+
# TODO: move to dm-more/dm-migrations
|
574
|
+
def property_schema_statement(schema)
|
575
|
+
statement = quote_column_name(schema[:name])
|
576
|
+
statement << " #{schema[:primitive]}"
|
577
|
+
|
578
|
+
if schema[:precision] && schema[:scale]
|
579
|
+
statement << "(#{[ :precision, :scale ].map { |k| quote_column_value(schema[k]) } * ','})"
|
580
|
+
elsif schema[:size]
|
581
|
+
statement << "(#{quote_column_value(schema[:size])})"
|
582
|
+
end
|
583
|
+
|
584
|
+
statement << ' NOT NULL' unless schema[:nullable?]
|
585
|
+
statement << " DEFAULT #{quote_column_value(schema[:default])}" if schema.has_key?(:default)
|
586
|
+
statement
|
587
|
+
end
|
588
|
+
|
589
|
+
# TODO: move to dm-more/dm-migrations
|
590
|
+
def relationship_schema_hash(relationship)
|
591
|
+
identifier, relationship = relationship
|
592
|
+
|
593
|
+
self.class.type_map[Integer].merge(:name => "#{identifier}_id") if identifier == relationship.name
|
594
|
+
end
|
595
|
+
|
596
|
+
# TODO: move to dm-more/dm-migrations
|
597
|
+
def relationship_schema_statement(hash)
|
598
|
+
property_schema_statement(hash) unless hash.nil?
|
599
|
+
end
|
600
|
+
end # module SQL
|
601
|
+
|
602
|
+
include SQL
|
603
|
+
|
604
|
+
module ClassMethods
|
605
|
+
# Default TypeMap for all data object based adapters.
|
606
|
+
#
|
607
|
+
# @return <DataMapper::TypeMap> default TypeMap for data objects adapters.
|
608
|
+
#
|
609
|
+
# TODO: move to dm-more/dm-migrations
|
610
|
+
def type_map
|
611
|
+
@type_map ||= TypeMap.new(super) do |tm|
|
612
|
+
tm.map(Integer).to('INT')
|
613
|
+
tm.map(String).to('VARCHAR').with(:size => Property::DEFAULT_LENGTH)
|
614
|
+
tm.map(Class).to('VARCHAR').with(:size => Property::DEFAULT_LENGTH)
|
615
|
+
tm.map(DM::Discriminator).to('VARCHAR').with(:size => Property::DEFAULT_LENGTH)
|
616
|
+
tm.map(BigDecimal).to('DECIMAL').with(:precision => Property::DEFAULT_PRECISION, :scale => Property::DEFAULT_SCALE_BIGDECIMAL)
|
617
|
+
tm.map(Float).to('FLOAT').with(:precision => Property::DEFAULT_PRECISION)
|
618
|
+
tm.map(DateTime).to('TIMESTAMP')
|
619
|
+
tm.map(Date).to('DATE')
|
620
|
+
tm.map(Time).to('TIMESTAMP')
|
621
|
+
tm.map(TrueClass).to('BOOLEAN')
|
622
|
+
tm.map(DM::Object).to('TEXT')
|
623
|
+
tm.map(DM::Text).to('TEXT')
|
624
|
+
end
|
625
|
+
end
|
626
|
+
end # module ClassMethods
|
627
|
+
end # module Migration
|
628
|
+
|
629
|
+
include Migration
|
630
|
+
extend Migration::ClassMethods
|
631
|
+
end # class DataObjectsAdapter
|
632
|
+
end # module Adapters
|
633
|
+
|
634
|
+
# TODO: move to dm-ar-finders
|
635
|
+
module Model
|
636
|
+
#
|
637
|
+
# Find instances by manually providing SQL
|
638
|
+
#
|
639
|
+
# @param sql<String> an SQL query to execute
|
640
|
+
# @param <Array> an Array containing a String (being the SQL query to
|
641
|
+
# execute) and the parameters to the query.
|
642
|
+
# example: ["SELECT name FROM users WHERE id = ?", id]
|
643
|
+
# @param query<DataMapper::Query> a prepared Query to execute.
|
644
|
+
# @param opts<Hash> an options hash.
|
645
|
+
# :repository<Symbol> the name of the repository to execute the query
|
646
|
+
# in. Defaults to self.default_repository_name.
|
647
|
+
# :reload<Boolean> whether to reload any instances found that already
|
648
|
+
# exist in the identity map. Defaults to false.
|
649
|
+
# :properties<Array> the Properties of the instance that the query
|
650
|
+
# loads. Must contain DataMapper::Properties.
|
651
|
+
# Defaults to self.properties.
|
652
|
+
#
|
653
|
+
# @note
|
654
|
+
# A String, Array or Query is required.
|
655
|
+
# @return <Collection> the instance matched by the query.
|
656
|
+
#
|
657
|
+
# @example
|
658
|
+
# MyClass.find_by_sql(["SELECT id FROM my_classes WHERE county = ?",
|
659
|
+
# selected_county], :properties => MyClass.property[:id],
|
660
|
+
# :repository => :county_repo)
|
661
|
+
#
|
662
|
+
# -
|
663
|
+
# @api public
|
664
|
+
def find_by_sql(*args)
|
665
|
+
sql = nil
|
666
|
+
query = nil
|
667
|
+
bind_values = []
|
668
|
+
properties = nil
|
669
|
+
do_reload = false
|
670
|
+
repository_name = default_repository_name
|
671
|
+
args.each do |arg|
|
672
|
+
if arg.is_a?(String)
|
673
|
+
sql = arg
|
674
|
+
elsif arg.is_a?(Array)
|
675
|
+
sql = arg.first
|
676
|
+
bind_values = arg[1..-1]
|
677
|
+
elsif arg.is_a?(DataMapper::Query)
|
678
|
+
query = arg
|
679
|
+
elsif arg.is_a?(Hash)
|
680
|
+
repository_name = arg.delete(:repository) if arg.include?(:repository)
|
681
|
+
properties = Array(arg.delete(:properties)) if arg.include?(:properties)
|
682
|
+
do_reload = arg.delete(:reload) if arg.include?(:reload)
|
683
|
+
raise "unknown options to #find_by_sql: #{arg.inspect}" unless arg.empty?
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
repository = repository(repository_name)
|
688
|
+
raise "#find_by_sql only available for Repositories served by a DataObjectsAdapter" unless repository.adapter.is_a?(DataMapper::Adapters::DataObjectsAdapter)
|
689
|
+
|
690
|
+
if query
|
691
|
+
sql = repository.adapter.send(:read_statement, query)
|
692
|
+
bind_values = query.bind_values
|
693
|
+
end
|
694
|
+
|
695
|
+
raise "#find_by_sql requires a query of some kind to work" unless sql
|
696
|
+
|
697
|
+
properties ||= self.properties(repository.name)
|
698
|
+
|
699
|
+
Collection.new(Query.new(repository, self)) do |collection|
|
700
|
+
repository.adapter.send(:with_connection) do |connection|
|
701
|
+
command = connection.create_command(sql)
|
702
|
+
|
703
|
+
begin
|
704
|
+
reader = command.execute_reader(*bind_values)
|
705
|
+
|
706
|
+
while(reader.next!)
|
707
|
+
collection.load(reader.values)
|
708
|
+
end
|
709
|
+
ensure
|
710
|
+
reader.close if reader
|
711
|
+
end
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end # module Model
|
716
|
+
end # module DataMapper
|