dm-core 0.9.2

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