sam-dm-core 0.9.6

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