datamapper-dm-core 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. data/.autotest +26 -0
  2. data/.gitignore +18 -0
  3. data/CONTRIBUTING +51 -0
  4. data/FAQ +92 -0
  5. data/History.txt +41 -0
  6. data/MIT-LICENSE +22 -0
  7. data/Manifest.txt +130 -0
  8. data/QUICKLINKS +11 -0
  9. data/README.txt +143 -0
  10. data/Rakefile +30 -0
  11. data/SPECS +62 -0
  12. data/TODO +1 -0
  13. data/dm-core.gemspec +40 -0
  14. data/lib/dm-core.rb +217 -0
  15. data/lib/dm-core/adapters.rb +16 -0
  16. data/lib/dm-core/adapters/abstract_adapter.rb +209 -0
  17. data/lib/dm-core/adapters/data_objects_adapter.rb +716 -0
  18. data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
  19. data/lib/dm-core/adapters/mysql_adapter.rb +136 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +189 -0
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  22. data/lib/dm-core/associations.rb +207 -0
  23. data/lib/dm-core/associations/many_to_many.rb +147 -0
  24. data/lib/dm-core/associations/many_to_one.rb +107 -0
  25. data/lib/dm-core/associations/one_to_many.rb +315 -0
  26. data/lib/dm-core/associations/one_to_one.rb +61 -0
  27. data/lib/dm-core/associations/relationship.rb +229 -0
  28. data/lib/dm-core/associations/relationship_chain.rb +81 -0
  29. data/lib/dm-core/auto_migrations.rb +105 -0
  30. data/lib/dm-core/collection.rb +670 -0
  31. data/lib/dm-core/dependency_queue.rb +32 -0
  32. data/lib/dm-core/hook.rb +11 -0
  33. data/lib/dm-core/identity_map.rb +42 -0
  34. data/lib/dm-core/is.rb +16 -0
  35. data/lib/dm-core/logger.rb +232 -0
  36. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  37. data/lib/dm-core/migrator.rb +29 -0
  38. data/lib/dm-core/model.rb +526 -0
  39. data/lib/dm-core/naming_conventions.rb +84 -0
  40. data/lib/dm-core/property.rb +676 -0
  41. data/lib/dm-core/property_set.rb +169 -0
  42. data/lib/dm-core/query.rb +676 -0
  43. data/lib/dm-core/repository.rb +167 -0
  44. data/lib/dm-core/resource.rb +671 -0
  45. data/lib/dm-core/scope.rb +58 -0
  46. data/lib/dm-core/support.rb +7 -0
  47. data/lib/dm-core/support/array.rb +13 -0
  48. data/lib/dm-core/support/assertions.rb +8 -0
  49. data/lib/dm-core/support/errors.rb +23 -0
  50. data/lib/dm-core/support/kernel.rb +11 -0
  51. data/lib/dm-core/support/symbol.rb +41 -0
  52. data/lib/dm-core/transaction.rb +267 -0
  53. data/lib/dm-core/type.rb +160 -0
  54. data/lib/dm-core/type_map.rb +80 -0
  55. data/lib/dm-core/types.rb +19 -0
  56. data/lib/dm-core/types/boolean.rb +7 -0
  57. data/lib/dm-core/types/discriminator.rb +34 -0
  58. data/lib/dm-core/types/object.rb +24 -0
  59. data/lib/dm-core/types/paranoid_boolean.rb +34 -0
  60. data/lib/dm-core/types/paranoid_datetime.rb +33 -0
  61. data/lib/dm-core/types/serial.rb +9 -0
  62. data/lib/dm-core/types/text.rb +10 -0
  63. data/lib/dm-core/version.rb +3 -0
  64. data/script/all +4 -0
  65. data/script/performance.rb +282 -0
  66. data/script/profile.rb +87 -0
  67. data/spec/integration/association_spec.rb +1382 -0
  68. data/spec/integration/association_through_spec.rb +203 -0
  69. data/spec/integration/associations/many_to_many_spec.rb +449 -0
  70. data/spec/integration/associations/many_to_one_spec.rb +163 -0
  71. data/spec/integration/associations/one_to_many_spec.rb +188 -0
  72. data/spec/integration/auto_migrations_spec.rb +413 -0
  73. data/spec/integration/collection_spec.rb +1073 -0
  74. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  75. data/spec/integration/dependency_queue_spec.rb +46 -0
  76. data/spec/integration/model_spec.rb +197 -0
  77. data/spec/integration/mysql_adapter_spec.rb +85 -0
  78. data/spec/integration/postgres_adapter_spec.rb +731 -0
  79. data/spec/integration/property_spec.rb +253 -0
  80. data/spec/integration/query_spec.rb +514 -0
  81. data/spec/integration/repository_spec.rb +61 -0
  82. data/spec/integration/resource_spec.rb +513 -0
  83. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  84. data/spec/integration/sti_spec.rb +273 -0
  85. data/spec/integration/strategic_eager_loading_spec.rb +156 -0
  86. data/spec/integration/transaction_spec.rb +75 -0
  87. data/spec/integration/type_spec.rb +275 -0
  88. data/spec/lib/logging_helper.rb +18 -0
  89. data/spec/lib/mock_adapter.rb +27 -0
  90. data/spec/lib/model_loader.rb +100 -0
  91. data/spec/lib/publicize_methods.rb +28 -0
  92. data/spec/models/content.rb +16 -0
  93. data/spec/models/vehicles.rb +34 -0
  94. data/spec/models/zoo.rb +48 -0
  95. data/spec/spec.opts +3 -0
  96. data/spec/spec_helper.rb +91 -0
  97. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  98. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  99. data/spec/unit/adapters/data_objects_adapter_spec.rb +632 -0
  100. data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
  101. data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
  102. data/spec/unit/associations/many_to_many_spec.rb +32 -0
  103. data/spec/unit/associations/many_to_one_spec.rb +159 -0
  104. data/spec/unit/associations/one_to_many_spec.rb +393 -0
  105. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  106. data/spec/unit/associations/relationship_spec.rb +71 -0
  107. data/spec/unit/associations_spec.rb +242 -0
  108. data/spec/unit/auto_migrations_spec.rb +111 -0
  109. data/spec/unit/collection_spec.rb +182 -0
  110. data/spec/unit/data_mapper_spec.rb +35 -0
  111. data/spec/unit/identity_map_spec.rb +126 -0
  112. data/spec/unit/is_spec.rb +80 -0
  113. data/spec/unit/migrator_spec.rb +33 -0
  114. data/spec/unit/model_spec.rb +321 -0
  115. data/spec/unit/naming_conventions_spec.rb +36 -0
  116. data/spec/unit/property_set_spec.rb +90 -0
  117. data/spec/unit/property_spec.rb +753 -0
  118. data/spec/unit/query_spec.rb +571 -0
  119. data/spec/unit/repository_spec.rb +93 -0
  120. data/spec/unit/resource_spec.rb +649 -0
  121. data/spec/unit/scope_spec.rb +142 -0
  122. data/spec/unit/transaction_spec.rb +493 -0
  123. data/spec/unit/type_map_spec.rb +114 -0
  124. data/spec/unit/type_spec.rb +119 -0
  125. data/tasks/ci.rb +36 -0
  126. data/tasks/dm.rb +63 -0
  127. data/tasks/doc.rb +20 -0
  128. data/tasks/gemspec.rb +23 -0
  129. data/tasks/hoe.rb +46 -0
  130. data/tasks/install.rb +20 -0
  131. metadata +215 -0
@@ -0,0 +1,716 @@
1
+ gem 'data_objects', '~>0.9.12'
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('DATETIME')
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