sam-dm-core 0.9.6

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