dm-core 0.9.2

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 (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