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.
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 +52 -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 +32 -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 +138 -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 +221 -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 +252 -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 +60 -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 +469 -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.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