datamapper-dm-core 0.9.11 → 0.10.0

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 (192) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -39
  5. data/Manifest.txt +67 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +16 -15
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/dm-core.gemspec +11 -15
  12. data/lib/dm-core/adapters/abstract_adapter.rb +182 -185
  13. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  14. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  15. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  16. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  19. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  20. data/lib/dm-core/adapters.rb +135 -16
  21. data/lib/dm-core/associations/many_to_many.rb +372 -90
  22. data/lib/dm-core/associations/many_to_one.rb +220 -73
  23. data/lib/dm-core/associations/one_to_many.rb +319 -255
  24. data/lib/dm-core/associations/one_to_one.rb +66 -53
  25. data/lib/dm-core/associations/relationship.rb +560 -158
  26. data/lib/dm-core/collection.rb +1104 -381
  27. data/lib/dm-core/core_ext/kernel.rb +12 -0
  28. data/lib/dm-core/core_ext/symbol.rb +10 -0
  29. data/lib/dm-core/identity_map.rb +4 -34
  30. data/lib/dm-core/migrations.rb +1283 -0
  31. data/lib/dm-core/model/descendant_set.rb +81 -0
  32. data/lib/dm-core/model/hook.rb +45 -0
  33. data/lib/dm-core/model/is.rb +32 -0
  34. data/lib/dm-core/model/property.rb +248 -0
  35. data/lib/dm-core/model/relationship.rb +335 -0
  36. data/lib/dm-core/model/scope.rb +90 -0
  37. data/lib/dm-core/model.rb +570 -369
  38. data/lib/dm-core/property.rb +753 -280
  39. data/lib/dm-core/property_set.rb +141 -98
  40. data/lib/dm-core/query/conditions/comparison.rb +814 -0
  41. data/lib/dm-core/query/conditions/operation.rb +247 -0
  42. data/lib/dm-core/query/direction.rb +43 -0
  43. data/lib/dm-core/query/operator.rb +42 -0
  44. data/lib/dm-core/query/path.rb +102 -0
  45. data/lib/dm-core/query/sort.rb +45 -0
  46. data/lib/dm-core/query.rb +974 -492
  47. data/lib/dm-core/repository.rb +147 -107
  48. data/lib/dm-core/resource.rb +644 -429
  49. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  50. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  51. data/lib/dm-core/support/chainable.rb +20 -0
  52. data/lib/dm-core/support/deprecate.rb +12 -0
  53. data/lib/dm-core/support/equalizer.rb +23 -0
  54. data/lib/dm-core/support/logger.rb +13 -0
  55. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  56. data/lib/dm-core/transaction.rb +333 -92
  57. data/lib/dm-core/type.rb +98 -60
  58. data/lib/dm-core/types/boolean.rb +1 -1
  59. data/lib/dm-core/types/discriminator.rb +34 -20
  60. data/lib/dm-core/types/object.rb +7 -4
  61. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  62. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  63. data/lib/dm-core/types/serial.rb +3 -3
  64. data/lib/dm-core/types/text.rb +3 -4
  65. data/lib/dm-core/version.rb +1 -1
  66. data/lib/dm-core.rb +106 -110
  67. data/script/performance.rb +102 -109
  68. data/script/profile.rb +169 -38
  69. data/spec/lib/adapter_helpers.rb +105 -0
  70. data/spec/lib/collection_helpers.rb +18 -0
  71. data/spec/lib/counter_adapter.rb +34 -0
  72. data/spec/lib/pending_helpers.rb +27 -0
  73. data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
  74. data/spec/public/associations/many_to_many_spec.rb +193 -0
  75. data/spec/public/associations/many_to_one_spec.rb +73 -0
  76. data/spec/public/associations/one_to_many_spec.rb +77 -0
  77. data/spec/public/associations/one_to_one_spec.rb +156 -0
  78. data/spec/public/collection_spec.rb +65 -0
  79. data/spec/public/model/relationship_spec.rb +924 -0
  80. data/spec/public/model_spec.rb +159 -0
  81. data/spec/public/property_spec.rb +829 -0
  82. data/spec/public/resource_spec.rb +71 -0
  83. data/spec/public/sel_spec.rb +44 -0
  84. data/spec/public/setup_spec.rb +145 -0
  85. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  86. data/spec/public/shared/collection_shared_spec.rb +1723 -0
  87. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  88. data/spec/public/shared/resource_shared_spec.rb +924 -0
  89. data/spec/public/shared/sel_shared_spec.rb +112 -0
  90. data/spec/public/transaction_spec.rb +129 -0
  91. data/spec/public/types/discriminator_spec.rb +130 -0
  92. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  93. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  94. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  95. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  96. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  97. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  99. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  100. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  101. data/spec/semipublic/associations_spec.rb +177 -0
  102. data/spec/semipublic/collection_spec.rb +142 -0
  103. data/spec/semipublic/property_spec.rb +61 -0
  104. data/spec/semipublic/query/conditions_spec.rb +528 -0
  105. data/spec/semipublic/query/path_spec.rb +443 -0
  106. data/spec/semipublic/query_spec.rb +2626 -0
  107. data/spec/semipublic/resource_spec.rb +47 -0
  108. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  109. data/spec/spec.opts +3 -1
  110. data/spec/spec_helper.rb +80 -57
  111. data/tasks/ci.rb +19 -31
  112. data/tasks/dm.rb +43 -48
  113. data/tasks/doc.rb +8 -11
  114. data/tasks/gemspec.rb +5 -5
  115. data/tasks/hoe.rb +15 -16
  116. data/tasks/install.rb +8 -10
  117. metadata +72 -93
  118. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  119. data/lib/dm-core/associations.rb +0 -207
  120. data/lib/dm-core/auto_migrations.rb +0 -105
  121. data/lib/dm-core/dependency_queue.rb +0 -32
  122. data/lib/dm-core/hook.rb +0 -11
  123. data/lib/dm-core/is.rb +0 -16
  124. data/lib/dm-core/logger.rb +0 -232
  125. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  126. data/lib/dm-core/migrator.rb +0 -29
  127. data/lib/dm-core/scope.rb +0 -58
  128. data/lib/dm-core/support/array.rb +0 -13
  129. data/lib/dm-core/support/assertions.rb +0 -8
  130. data/lib/dm-core/support/errors.rb +0 -23
  131. data/lib/dm-core/support/kernel.rb +0 -11
  132. data/lib/dm-core/support/symbol.rb +0 -41
  133. data/lib/dm-core/support.rb +0 -7
  134. data/lib/dm-core/type_map.rb +0 -80
  135. data/lib/dm-core/types.rb +0 -19
  136. data/script/all +0 -4
  137. data/spec/integration/association_spec.rb +0 -1382
  138. data/spec/integration/association_through_spec.rb +0 -203
  139. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  140. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  141. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  142. data/spec/integration/auto_migrations_spec.rb +0 -413
  143. data/spec/integration/collection_spec.rb +0 -1073
  144. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  145. data/spec/integration/dependency_queue_spec.rb +0 -46
  146. data/spec/integration/model_spec.rb +0 -197
  147. data/spec/integration/mysql_adapter_spec.rb +0 -85
  148. data/spec/integration/postgres_adapter_spec.rb +0 -731
  149. data/spec/integration/property_spec.rb +0 -253
  150. data/spec/integration/query_spec.rb +0 -514
  151. data/spec/integration/repository_spec.rb +0 -61
  152. data/spec/integration/resource_spec.rb +0 -513
  153. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  154. data/spec/integration/sti_spec.rb +0 -273
  155. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  156. data/spec/integration/transaction_spec.rb +0 -75
  157. data/spec/integration/type_spec.rb +0 -275
  158. data/spec/lib/logging_helper.rb +0 -18
  159. data/spec/lib/mock_adapter.rb +0 -27
  160. data/spec/lib/model_loader.rb +0 -100
  161. data/spec/lib/publicize_methods.rb +0 -28
  162. data/spec/models/content.rb +0 -16
  163. data/spec/models/vehicles.rb +0 -34
  164. data/spec/models/zoo.rb +0 -48
  165. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  166. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  167. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  168. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  169. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  170. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  171. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  172. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  173. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  174. data/spec/unit/associations/relationship_spec.rb +0 -71
  175. data/spec/unit/associations_spec.rb +0 -242
  176. data/spec/unit/auto_migrations_spec.rb +0 -111
  177. data/spec/unit/collection_spec.rb +0 -182
  178. data/spec/unit/data_mapper_spec.rb +0 -35
  179. data/spec/unit/identity_map_spec.rb +0 -126
  180. data/spec/unit/is_spec.rb +0 -80
  181. data/spec/unit/migrator_spec.rb +0 -33
  182. data/spec/unit/model_spec.rb +0 -321
  183. data/spec/unit/naming_conventions_spec.rb +0 -36
  184. data/spec/unit/property_set_spec.rb +0 -90
  185. data/spec/unit/property_spec.rb +0 -753
  186. data/spec/unit/query_spec.rb +0 -571
  187. data/spec/unit/repository_spec.rb +0 -93
  188. data/spec/unit/resource_spec.rb +0 -649
  189. data/spec/unit/scope_spec.rb +0 -142
  190. data/spec/unit/transaction_spec.rb +0 -493
  191. data/spec/unit/type_map_spec.rb +0 -114
  192. data/spec/unit/type_spec.rb +0 -119
@@ -1,91 +1,161 @@
1
- gem 'data_objects', '~>0.9.12'
2
1
  require 'data_objects'
3
2
 
4
3
  module DataMapper
5
4
  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.
5
+ # DataObjectsAdapter is the base class for all adapers for relational
6
+ # databases. If you want to add support for a new RDBMS, it makes
7
+ # sense to make your adapter class inherit from this class.
8
8
  #
9
- # NOTE: By inheriting from DataObjectsAdapter, you get a copy of all the
9
+ # By inheriting from DataObjectsAdapter, you get a copy of all the
10
10
  # standard sub-modules (Quoting, Coersion and Queries) in your own Adapter.
11
11
  # You can extend and overwrite these copies without affecting the originals.
12
12
  class DataObjectsAdapter < AbstractAdapter
13
+ extend Chainable
14
+
15
+ # For each model instance in resources, issues an SQL INSERT
16
+ # (or equivalent) statement to create a new record in the data store for
17
+ # the instance
18
+ #
19
+ # Note that this method does not update identity map. A plugin needs to use
20
+ # adapter directly, it is up to plugin developer to keep identity map
21
+ # up to date.
22
+ #
23
+ # @param [Enumerable(Resource)] resources
24
+ # The list of resources (model instances) to create
25
+ #
26
+ # @return [Integer]
27
+ # The number of records that were actually saved into the database
28
+ #
29
+ # @api semipublic
13
30
  def create(resources)
14
- created = 0
15
31
  resources.each do |resource|
16
- repository = resource.repository
17
32
  model = resource.model
33
+ serial = model.serial(name)
18
34
  attributes = resource.dirty_attributes
19
35
 
20
- # TODO: make a model.identity_field method
21
- identity_field = model.key(repository.name).detect { |p| p.serial? }
36
+ properties = []
37
+ bind_values = []
22
38
 
23
- statement = create_statement(repository, model, attributes.keys, identity_field)
24
- bind_values = attributes.values
39
+ # make the order of the properties consistent
40
+ model.properties(name).each do |property|
41
+ next unless attributes.key?(property)
25
42
 
26
- result = execute(statement, *bind_values)
43
+ bind_value = attributes[property]
27
44
 
28
- if result.to_i == 1
29
- if identity_field
30
- identity_field.set!(resource, result.insert_id)
45
+ # skip insering NULL for columns that are serial or without a default
46
+ next if bind_value.nil? && (property.serial? || !property.default?)
47
+
48
+ # if serial is being set explicitly, do not set it again
49
+ if property.equal?(serial)
50
+ serial = nil
31
51
  end
32
- created += 1
33
- end
34
- end
35
- created
36
- end
37
52
 
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 })
53
+ properties << property
54
+ bind_values << bind_value
55
+ end
43
56
 
44
- begin
45
- bind_values = query.bind_values.map do |v|
46
- v == [] ? [nil] : v
47
- end
48
- reader = command.execute_reader(*bind_values)
57
+ statement = insert_statement(model, properties, serial)
58
+ result = execute(statement, *bind_values)
49
59
 
50
- while(reader.next!)
51
- collection.load(reader.values)
52
- end
53
- ensure
54
- reader.close if reader
55
- end
60
+ if result.to_i == 1 && serial
61
+ serial.set!(resource, result.insert_id)
56
62
  end
57
63
  end
58
64
  end
59
65
 
60
- def read_one(query)
66
+ # Constructs and executes SELECT query, then instantiates
67
+ # one or many object from result set.
68
+ #
69
+ # @param [Query] query
70
+ # composition of the query to perform
71
+ #
72
+ # @return [Array]
73
+ # result set of the query
74
+ #
75
+ # @api semipublic
76
+ def read(query)
77
+ fields = query.fields
78
+ types = fields.map { |property| property.primitive }
79
+
80
+ statement, bind_values = select_statement(query)
81
+
82
+ records = []
83
+
61
84
  with_connection do |connection|
62
- command = connection.create_command(read_statement(query))
63
- command.set_types(query.fields.map { |p| p.primitive })
85
+ command = connection.create_command(statement)
86
+ command.set_types(types)
64
87
 
65
- begin
66
- reader = command.execute_reader(*query.bind_values)
88
+ reader = command.execute_reader(*bind_values)
67
89
 
68
- if reader.next!
69
- query.model.load(reader.values, query)
90
+ begin
91
+ while reader.next!
92
+ records << fields.zip(reader.values).to_hash
70
93
  end
71
94
  ensure
72
- reader.close if reader
95
+ reader.close
73
96
  end
74
97
  end
98
+
99
+ records
75
100
  end
76
101
 
77
- def update(attributes, query)
78
- statement = update_statement(attributes.keys, query)
79
- bind_values = attributes.values + query.bind_values
102
+ # Constructs and executes UPDATE statement for given
103
+ # attributes and a query
104
+ #
105
+ # @param [Hash(Property => Object)] attributes
106
+ # hash of attribute values to set, keyed by Property
107
+ # @param [Collection] collection
108
+ # collection of records to be updated
109
+ #
110
+ # @return [Integer]
111
+ # the number of records updated
112
+ #
113
+ # @api semipublic
114
+ def update(attributes, collection)
115
+ query = collection.query
116
+
117
+ # TODO: if the query contains any links, a limit or an offset
118
+ # use a subselect to get the rows to be updated
119
+
120
+ properties = []
121
+ bind_values = []
122
+
123
+ # make the order of the properties consistent
124
+ query.model.properties(name).each do |property|
125
+ next unless attributes.key?(property)
126
+ properties << property
127
+ bind_values << attributes[property]
128
+ end
129
+
130
+ statement, conditions_bind_values = update_statement(properties, query)
131
+
132
+ bind_values.concat(conditions_bind_values)
133
+
80
134
  execute(statement, *bind_values).to_i
81
135
  end
82
136
 
83
- def delete(query)
84
- statement = delete_statement(query)
85
- execute(statement, *query.bind_values).to_i
137
+ # Constructs and executes DELETE statement for given query
138
+ #
139
+ # @param [Collection] collection
140
+ # collection of records to be deleted
141
+ #
142
+ # @return [Integer]
143
+ # the number of records deleted
144
+ #
145
+ # @api semipublic
146
+ def delete(collection)
147
+ query = collection.query
148
+
149
+ # TODO: if the query contains any links, a limit or an offset
150
+ # use a subselect to get the rows to be deleted
151
+
152
+ statement, bind_values = delete_statement(query)
153
+ execute(statement, *bind_values).to_i
86
154
  end
87
155
 
88
156
  # Database-specific method
157
+ # TODO: document
158
+ # @api public
89
159
  def execute(statement, *bind_values)
90
160
  with_connection do |connection|
91
161
  command = connection.create_command(statement)
@@ -93,21 +163,30 @@ module DataMapper
93
163
  end
94
164
  end
95
165
 
166
+ # TODO: document
167
+ # @api public
96
168
  def query(statement, *bind_values)
97
- with_reader(statement, bind_values) do |reader|
169
+ with_connection do |connection|
170
+ reader = connection.create_command(statement).execute_reader(*bind_values)
171
+ fields = reader.fields
172
+
98
173
  results = []
99
174
 
100
- if (fields = reader.fields).size > 1
101
- fields = fields.map { |field| Extlib::Inflection.underscore(field).to_sym }
102
- struct = Struct.new(*fields)
175
+ begin
176
+ if fields.size > 1
177
+ fields = fields.map { |field| Extlib::Inflection.underscore(field).to_sym }
178
+ struct = Struct.new(*fields)
103
179
 
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)
180
+ while reader.next!
181
+ results << struct.new(*reader.values)
182
+ end
183
+ else
184
+ while reader.next!
185
+ results << reader.values.at(0)
186
+ end
110
187
  end
188
+ ensure
189
+ reader.close
111
190
  end
112
191
 
113
192
  results
@@ -116,601 +195,470 @@ module DataMapper
116
195
 
117
196
  protected
118
197
 
119
- def normalize_uri(uri_or_options)
120
- if uri_or_options.kind_of?(String) || uri_or_options.kind_of?(Addressable::URI)
121
- uri_or_options = DataObjects::URI.parse(uri_or_options)
122
- end
123
-
124
- if uri_or_options.kind_of?(DataObjects::URI)
125
- return uri_or_options
126
- end
127
-
128
- query = uri_or_options.except(:adapter, :username, :password, :host, :port, :database).map { |pair| pair.join('=') }.join('&')
129
- query = nil if query.blank?
130
-
131
- return DataObjects::URI.parse(Addressable::URI.new(
132
- :scheme => uri_or_options[:adapter].to_s,
133
- :user => uri_or_options[:username],
134
- :password => uri_or_options[:password],
135
- :host => uri_or_options[:host],
136
- :port => uri_or_options[:port],
137
- :path => uri_or_options[:database],
138
- :query => query
139
- ))
198
+ # TODO: document
199
+ # @api private
200
+ def normalized_uri
201
+ @normalized_uri ||=
202
+ begin
203
+ query = @options.except(:adapter, :user, :password, :host, :port, :path, :fragment, :scheme, :query, :username, :database)
204
+ query = nil if query.empty?
205
+
206
+ DataObjects::URI.new(
207
+ @options[:adapter],
208
+ @options[:user] || @options[:username],
209
+ @options[:password],
210
+ @options[:host],
211
+ @options[:port],
212
+ @options[:path] || @options[:database],
213
+ query,
214
+ @options[:fragment]
215
+ ).freeze
216
+ end
140
217
  end
141
218
 
142
- # TODO: clean up once transaction related methods move to dm-more/dm-transactions
143
- def create_connection
144
- if within_transaction?
145
- current_transaction.primitive_for(self).connection
146
- else
219
+ chainable do
220
+ protected
221
+
222
+ # Instantiates new connection object
223
+ #
224
+ # @api semipublic
225
+ def open_connection
147
226
  # DataObjects::Connection.new(uri) will give you back the right
148
- # driver based on the Uri#scheme.
149
- DataObjects::Connection.new(@uri)
227
+ # driver based on the DataObjects::URI#scheme
228
+ connection = connection_stack.last || DataObjects::Connection.new(normalized_uri)
229
+ connection_stack << connection
230
+ connection
150
231
  end
151
- end
152
232
 
153
- # TODO: clean up once transaction related methods move to dm-more/dm-transactions
154
- def close_connection(connection)
155
- connection.close unless within_transaction? && current_transaction.primitive_for(self).connection == connection
233
+ # Takes connection and closes it
234
+ #
235
+ # @api semipublic
236
+ def close_connection(connection)
237
+ connection_stack.pop
238
+ connection.close if connection_stack.empty?
239
+ end
156
240
  end
157
241
 
158
242
  private
159
243
 
244
+ # TODO: document
245
+ # @api public
160
246
  def initialize(name, uri_or_options)
161
247
  super
162
248
 
163
- # Default the driver-specifc logger to DataMapper's logger
164
- if driver_module = DataObjects.const_get(@uri.scheme.capitalize) rescue nil
249
+ # Default the driver-specific logger to DataMapper's logger
250
+ if driver_module = DataObjects.const_get(normalized_uri.scheme.capitalize)
165
251
  driver_module.logger = DataMapper.logger if driver_module.respond_to?(:logger=)
166
252
  end
167
253
  end
168
254
 
255
+ # TODO: document
256
+ # @api private
257
+ def connection_stack
258
+ connection_stack_for = Thread.current[:dm_do_connection_stack] ||= {}
259
+ connection_stack_for[self] ||= []
260
+ end
261
+
262
+ # TODO: document
263
+ # @api private
169
264
  def with_connection
170
- connection = nil
171
265
  begin
172
- connection = create_connection
173
- return yield(connection)
174
- rescue => e
175
- DataMapper.logger.error(e.to_s)
176
- raise e
266
+ yield connection = open_connection
267
+ rescue Exception => exception
268
+ DataMapper.logger.error(exception.to_s)
269
+ raise exception
177
270
  ensure
178
271
  close_connection(connection) if connection
179
272
  end
180
273
  end
181
274
 
182
- def with_reader(statement, bind_values = [])
183
- with_connection do |connection|
184
- reader = nil
185
- begin
186
- reader = connection.create_command(statement).execute_reader(*bind_values)
187
- return yield(reader)
188
- ensure
189
- reader.close if reader
275
+ # This module is just for organization. The methods are included into the
276
+ # Adapter below.
277
+ module SQL #:nodoc:
278
+ IDENTIFIER_MAX_LENGTH = 128
279
+
280
+ # TODO: document
281
+ # @api semipublic
282
+ def property_to_column_name(property, qualify)
283
+ if qualify
284
+ table_name = property.model.storage_name(name)
285
+ "#{quote_name(table_name)}.#{quote_name(property.field)}"
286
+ else
287
+ quote_name(property.field)
190
288
  end
191
289
  end
192
- end
193
290
 
194
- # This model is just for organization. The methods are included into the
195
- # Adapter below.
196
- module SQL
197
291
  private
198
292
 
199
293
  # Adapters requiring a RETURNING syntax for INSERT statements
200
294
  # should overwrite this to return true.
295
+ #
296
+ # @api private
201
297
  def supports_returning?
202
298
  false
203
299
  end
204
300
 
205
301
  # Adapters that do not support the DEFAULT VALUES syntax for
206
302
  # INSERT statements should overwrite this to return false.
303
+ #
304
+ # @api private
207
305
  def supports_default_values?
208
306
  true
209
307
  end
210
308
 
211
- def create_statement(repository, model, properties, identity_field)
212
- statement = "INSERT INTO #{quote_table_name(model.storage_name(repository.name))} "
309
+ # Constructs SELECT statement for given query,
310
+ #
311
+ # @return [String] SELECT statement as a string
312
+ #
313
+ # @api private
314
+ def select_statement(query)
315
+ model = query.model
316
+ fields = query.fields
317
+ conditions = query.conditions
318
+ limit = query.limit
319
+ offset = query.offset
320
+ order_by = query.order
321
+ group_by = nil
322
+
323
+ # FIXME: using a boolean for qualify does not work in some cases,
324
+ # such as when you have a self-referrential many to many association.
325
+ # if you don't qualfiy the columns with a unique alias, then the
326
+ # SQL query will fail. This may mean though, that it might not
327
+ # be enough to pass in a Property, but we may need to know the
328
+ # table and the alias we should use for the column.
213
329
 
214
- if supports_default_values? && properties.empty?
215
- statement << 'DEFAULT VALUES'
216
- else
217
- statement << <<-EOS.compress_lines
218
- (#{properties.map { |p| quote_column_name(p.field(repository.name)) } * ', '})
219
- VALUES
220
- (#{(['?'] * properties.size) * ', '})
221
- EOS
222
- end
330
+ qualify = query.links.any?
223
331
 
224
- if supports_returning? && identity_field
225
- statement << " RETURNING #{quote_column_name(identity_field.field(repository.name))}"
332
+ if qualify || query.unique?
333
+ group_by = fields.select { |property| property.kind_of?(Property) }
226
334
  end
227
335
 
228
- statement
229
- end
230
-
231
- def read_statement(query)
232
- statement = "SELECT #{fields_statement(query)}"
233
- statement << " FROM #{quote_table_name(query.model.storage_name(query.repository.name))}"
234
- statement << links_statement(query) if query.links.any?
235
- statement << " WHERE #{conditions_statement(query)}" if query.conditions.any?
236
- statement << " GROUP BY #{group_by_statement(query)}" if query.unique? && query.fields.any? { |p| p.kind_of?(Property) }
237
- statement << " ORDER BY #{order_statement(query)}" if query.order.any?
238
- statement << " LIMIT #{quote_column_value(query.limit)}" if query.limit
239
- statement << " OFFSET #{quote_column_value(query.offset)}" if query.offset && query.offset > 0
240
- statement
241
- rescue => e
242
- DataMapper.logger.error("QUERY INVALID: #{query.inspect} (#{e})")
243
- raise e
244
- end
245
-
246
- def update_statement(properties, query)
247
- statement = "UPDATE #{quote_table_name(query.model.storage_name(query.repository.name))}"
248
- statement << " SET #{set_statement(query.repository, properties)}"
249
- statement << " WHERE #{conditions_statement(query)}" if query.conditions.any?
250
- statement
251
- end
252
-
253
- def set_statement(repository, properties)
254
- properties.map { |p| "#{quote_column_name(p.field(repository.name))} = ?" } * ', '
255
- end
256
-
257
- def delete_statement(query)
258
- statement = "DELETE FROM #{quote_table_name(query.model.storage_name(query.repository.name))}"
259
- statement << " WHERE #{conditions_statement(query)}" if query.conditions.any?
260
- statement
261
- end
336
+ unless (limit && limit > 1) || offset > 0 || qualify
337
+ # TODO: move this method to Query, so that it walks the conditions
338
+ # and finds an OR operator
262
339
 
263
- def fields_statement(query)
264
- qualify = query.links.any?
265
- query.fields.map { |p| property_to_column_name(query.repository, p, qualify) } * ', '
266
- end
340
+ # TODO: handle cases where two or more properties need to be
341
+ # used together to be unique
267
342
 
268
- def links_statement(query)
269
- table_list = [query.model.storage_name(query.repository.name)]
270
-
271
- statement = ''
272
- query.links.each do |relationship|
273
- parent_table_name = relationship.parent_model.storage_name(query.repository.name)
274
- child_table_name = relationship.child_model.storage_name(query.repository.name)
275
-
276
- join_table_name = if table_list.include?(parent_table_name)
277
- child_table_name
278
- elsif table_list.include?(child_table_name)
279
- parent_table_name
280
- else
281
- raise ArgumentError, 'you\'re trying to join a table with no connection to this query'
343
+ # if a unique property is used, and there is no OR operator, then an ORDER
344
+ # and LIMIT are unecessary because it should only return a single row
345
+ if conditions.kind_of?(Query::Conditions::AndOperation) &&
346
+ conditions.any? { |operand| operand.kind_of?(Query::Conditions::EqualToComparison) && operand.subject.respond_to?(:unique?) && operand.subject.unique? } &&
347
+ !conditions.any? { |operand| operand.kind_of?(Query::Conditions::OrOperation) }
348
+ order_by = nil
349
+ limit = nil
282
350
  end
283
- table_list << join_table_name
284
-
285
- # We only do INNER JOIN for now
286
- statement << " INNER JOIN #{quote_table_name(join_table_name)} ON "
287
-
288
- statement << relationship.parent_key.zip(relationship.child_key).map do |parent_property,child_property|
289
- condition_statement(query, :eql, parent_property, child_property)
290
- end * ' AND '
291
351
  end
292
352
 
293
- statement
294
- end
353
+ conditions_statement, bind_values = conditions_statement(conditions, qualify)
295
354
 
296
- def conditions_statement(query)
297
- query.conditions.map { |o,p,b| condition_statement(query, o, p, b) } * ' AND '
298
- end
355
+ statement = "SELECT #{columns_statement(fields, qualify)}"
356
+ statement << " FROM #{quote_name(model.storage_name(name))}"
357
+ statement << join_statement(query, qualify) if qualify
358
+ statement << " WHERE #{conditions_statement}" unless conditions_statement.blank?
359
+ statement << " GROUP BY #{columns_statement(group_by, qualify)}" if group_by && group_by.any?
360
+ statement << " ORDER BY #{order_statement(order_by, qualify)}" if order_by && order_by.any?
299
361
 
300
- def group_by_statement(query)
301
- repository = query.repository
302
- qualify = query.links.any?
303
- query.fields.select { |p| p.kind_of?(Property) }.map { |p| property_to_column_name(repository, p, qualify) } * ', '
304
- end
362
+ add_limit_offset!(statement, limit, offset, bind_values)
305
363
 
306
- def order_statement(query)
307
- repository = query.repository
308
- qualify = query.links.any?
309
- query.order.map { |i| order_column(repository, i, qualify) } * ', '
364
+ return statement, bind_values
310
365
  end
311
366
 
312
- def order_column(repository, item, qualify)
313
- property, descending = nil, false
314
-
315
- case item
316
- when Property
317
- property = item
318
- when Query::Direction
319
- property = item.property
320
- descending = true if item.direction == :desc
367
+ # default construction of LIMIT and OFFSET
368
+ # overriden in Oracle adapter
369
+ def add_limit_offset!(statement, limit, offset, bind_values)
370
+ if limit
371
+ statement << ' LIMIT ?'
372
+ bind_values << limit
321
373
  end
322
374
 
323
- order_column = property_to_column_name(repository, property, qualify)
324
- order_column << ' DESC' if descending
325
- order_column
375
+ if limit && offset > 0
376
+ statement << ' OFFSET ?'
377
+ bind_values << offset
378
+ end
326
379
  end
327
380
 
328
- def condition_statement(query, operator, left_condition, right_condition)
329
- return left_condition if operator == :raw
381
+ # Constructs INSERT statement for given query,
382
+ #
383
+ # @return [String] INSERT statement as a string
384
+ #
385
+ # @api private
386
+ def insert_statement(model, properties, serial)
387
+ statement = "INSERT INTO #{quote_name(model.storage_name(name))} "
330
388
 
331
- qualify = query.links.any?
332
-
333
- conditions = [ left_condition, right_condition ].map do |condition|
334
- if condition.kind_of?(Property) || condition.kind_of?(Query::Path)
335
- property_to_column_name(query.repository, condition, qualify)
336
- elsif condition.kind_of?(Query)
337
- opposite = condition == left_condition ? right_condition : left_condition
338
- query.merge_subquery(operator, opposite, condition)
339
- "(#{read_statement(condition)})"
340
-
341
- # [].all? is always true
342
- elsif condition.kind_of?(Array) && condition.any? && condition.all? { |p| p.kind_of?(Property) }
343
- property_values = condition.map { |p| property_to_column_name(query.repository, p, qualify) }
344
- "(#{property_values * ', '})"
345
- else
346
- '?'
347
- end
389
+ if supports_default_values? && properties.empty?
390
+ statement << default_values_clause
391
+ else
392
+ statement << <<-SQL.compress_lines
393
+ (#{properties.map { |property| quote_name(property.field) }.join(', ')})
394
+ VALUES
395
+ (#{(['?'] * properties.size).join(', ')})
396
+ SQL
348
397
  end
349
398
 
350
- comparison = case operator
351
- when :eql, :in then equality_operator(right_condition)
352
- when :not then inequality_operator(right_condition)
353
- when :like then 'LIKE'
354
- when :gt then '>'
355
- when :gte then '>='
356
- when :lt then '<'
357
- when :lte then '<='
358
- else raise "Invalid query operator: #{operator.inspect}"
399
+ if supports_returning? && serial
400
+ statement << returning_clause(serial)
359
401
  end
360
402
 
361
- "(" + (conditions * " #{comparison} ") + ")"
403
+ statement
362
404
  end
363
405
 
364
- def equality_operator(operand)
365
- case operand
366
- when Array, Query then 'IN'
367
- when Range then 'BETWEEN'
368
- when NilClass then 'IS'
369
- else '='
370
- end
406
+ # by default PostgreSQL syntax
407
+ # overrided in Oracle adapter
408
+ def default_values_clause
409
+ 'DEFAULT VALUES'
371
410
  end
372
411
 
373
- def inequality_operator(operand)
374
- case operand
375
- when Array, Query then 'NOT IN'
376
- when Range then 'NOT BETWEEN'
377
- when NilClass then 'IS NOT'
378
- else '<>'
379
- end
412
+ # by default PostgreSQL syntax
413
+ # overrided in Oracle adapter
414
+ def returning_clause(serial)
415
+ " RETURNING #{quote_name(serial.field)}"
380
416
  end
381
417
 
382
- def property_to_column_name(repository, property, qualify)
383
- table_name = property.model.storage_name(repository.name) if property && property.respond_to?(:model)
418
+ # Constructs UPDATE statement for given query,
419
+ #
420
+ # @return [String] UPDATE statement as a string
421
+ #
422
+ # @api private
423
+ def update_statement(properties, query)
424
+ conditions_statement, bind_values = conditions_statement(query.conditions)
384
425
 
385
- if table_name && qualify
386
- "#{quote_table_name(table_name)}.#{quote_column_name(property.field(repository.name))}"
387
- else
388
- quote_column_name(property.field(repository.name))
389
- end
390
- end
426
+ statement = "UPDATE #{quote_name(query.model.storage_name(name))}"
427
+ statement << " SET #{properties.map { |property| "#{quote_name(property.field)} = ?" }.join(', ')}"
428
+ statement << " WHERE #{conditions_statement}" unless conditions_statement.blank?
391
429
 
392
- # TODO: once the driver's quoting methods become public, have
393
- # this method delegate to them instead
394
- def quote_table_name(table_name)
395
- table_name.gsub('"', '""').split('.').map { |part| "\"#{part}\"" } * '.'
430
+ return statement, bind_values
396
431
  end
397
432
 
398
- # TODO: once the driver's quoting methods become public, have
399
- # this method delegate to them instead
400
- def quote_column_name(column_name)
401
- "\"#{column_name.gsub('"', '""')}\""
402
- end
433
+ # Constructs DELETE statement for given query,
434
+ #
435
+ # @return [String] DELETE statement as a string
436
+ #
437
+ # @api private
438
+ def delete_statement(query)
439
+ conditions_statement, bind_values = conditions_statement(query.conditions)
403
440
 
404
- # TODO: once the driver's quoting methods become public, have
405
- # this method delegate to them instead
406
- def quote_column_value(column_value)
407
- return 'NULL' if column_value.nil?
441
+ statement = "DELETE FROM #{quote_name(query.model.storage_name(name))}"
442
+ statement << " WHERE #{conditions_statement}" unless conditions_statement.blank?
408
443
 
409
- case column_value
410
- when String
411
- if (integer = column_value.to_i).to_s == column_value
412
- quote_column_value(integer)
413
- elsif (float = column_value.to_f).to_s == column_value
414
- quote_column_value(integer)
415
- else
416
- "'#{column_value.gsub("'", "''")}'"
417
- end
418
- when DateTime
419
- quote_column_value(column_value.strftime('%Y-%m-%d %H:%M:%S'))
420
- when Date
421
- quote_column_value(column_value.strftime('%Y-%m-%d'))
422
- when Time
423
- quote_column_value(column_value.strftime('%Y-%m-%d %H:%M:%S') + ((column_value.usec > 0 ? ".#{column_value.usec.to_s.rjust(6, '0')}" : '')))
424
- when Integer, Float
425
- column_value.to_s
426
- when BigDecimal
427
- column_value.to_s('F')
428
- else
429
- column_value.to_s
430
- end
444
+ return statement, bind_values
431
445
  end
432
- end #module SQL
433
446
 
434
- include SQL
435
-
436
- # TODO: move to dm-more/dm-migrations
437
- module Migration
438
- # TODO: move to dm-more/dm-migrations
439
- def upgrade_model_storage(repository, model)
440
- table_name = model.storage_name(repository.name)
441
-
442
- if success = create_model_storage(repository, model)
443
- return model.properties(repository.name)
444
- end
447
+ # Constructs comma separated list of fields
448
+ #
449
+ # @return [String]
450
+ # list of fields as a string
451
+ #
452
+ # @api private
453
+ def columns_statement(properties, qualify)
454
+ properties.map { |property| property_to_column_name(property, qualify) }.join(', ')
455
+ end
445
456
 
446
- properties = []
457
+ # Constructs joins clause
458
+ #
459
+ # @return [String]
460
+ # joins clause
461
+ #
462
+ # @api private
463
+ def join_statement(query, qualify)
464
+ statement = ''
447
465
 
448
- model.properties(repository.name).each do |property|
449
- schema_hash = property_schema_hash(repository, property)
450
- next if field_exists?(table_name, schema_hash[:name])
451
- statement = alter_table_add_column_statement(table_name, schema_hash)
452
- execute(statement)
453
- properties << property
466
+ query.links.reverse_each do |relationship|
467
+ statement << " INNER JOIN #{quote_name(relationship.source_model.storage_name(name))} ON "
468
+ statement << relationship.target_key.zip(relationship.source_key).map do |target_property, source_property|
469
+ "#{property_to_column_name(target_property, qualify)} = #{property_to_column_name(source_property, qualify)}"
470
+ end.join(' AND ')
454
471
  end
455
472
 
456
- properties
473
+ statement
457
474
  end
458
475
 
459
- # TODO: move to dm-more/dm-migrations
460
- def create_model_storage(repository, model)
461
- return false if storage_exists?(model.storage_name(repository.name))
476
+ # Constructs where clause
477
+ #
478
+ # @return [String]
479
+ # where clause
480
+ #
481
+ # @api private
482
+ def conditions_statement(conditions, qualify = false)
483
+ case conditions
484
+ when Query::Conditions::NotOperation
485
+ negate_operation(conditions, qualify)
486
+
487
+ when Query::Conditions::AbstractOperation
488
+ # TODO: remove this once conditions can be compressed
489
+ if conditions.operands.size == 1
490
+ # factor out operations with a single operand
491
+ conditions_statement(conditions.operands.first, qualify)
492
+ else
493
+ operation_statement(conditions, qualify)
494
+ end
462
495
 
463
- execute(create_table_statement(repository, model))
496
+ when Query::Conditions::AbstractComparison
497
+ comparison_statement(conditions, qualify)
464
498
 
465
- (create_index_statements(repository, model) + create_unique_index_statements(repository, model)).each do |sql|
466
- execute(sql)
499
+ when Array
500
+ statement, bind_values = conditions # handle raw conditions
501
+ [ "(#{statement})", bind_values ]
467
502
  end
468
-
469
- true
470
503
  end
471
504
 
472
- # TODO: move to dm-more/dm-migrations
473
- def destroy_model_storage(repository, model)
474
- execute(drop_table_statement(repository, model))
475
- true
476
- end
505
+ # Constructs order clause
506
+ #
507
+ # @return [String]
508
+ # order clause
509
+ #
510
+ # @api private
511
+ def order_statement(order, qualify)
512
+ statements = order.map do |direction|
513
+ statement = property_to_column_name(direction.target, qualify)
514
+ statement << ' DESC' if direction.operator == :desc
515
+ statement
516
+ end
477
517
 
478
- # TODO: move to dm-more/dm-transactions
479
- def transaction_primitive
480
- DataObjects::Transaction.create_for_uri(@uri)
518
+ statements.join(', ')
481
519
  end
482
520
 
483
- module SQL
484
- private
485
-
486
- # Adapters that support AUTO INCREMENT fields for CREATE TABLE
487
- # statements should overwrite this to return true
488
- #
489
- # TODO: move to dm-more/dm-migrations
490
- def supports_serial?
491
- false
492
- end
493
-
494
- # TODO: move to dm-more/dm-migrations
495
- def alter_table_add_column_statement(table_name, schema_hash)
496
- "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{property_schema_statement(schema_hash)}"
521
+ # TODO: document
522
+ # @api private
523
+ def negate_operation(operation, qualify)
524
+ @negated = !@negated
525
+ begin
526
+ conditions_statement(operation.operands.first, qualify)
527
+ ensure
528
+ @negated = !@negated
497
529
  end
530
+ end
498
531
 
499
- # TODO: move to dm-more/dm-migrations
500
- def create_table_statement(repository, model)
501
- repository_name = repository.name
532
+ # TODO: document
533
+ # @api private
534
+ def operation_statement(operation, qualify)
535
+ statements = []
536
+ bind_values = []
502
537
 
503
- statement = <<-EOS.compress_lines
504
- CREATE TABLE #{quote_table_name(model.storage_name(repository_name))}
505
- (#{model.properties_with_subclasses(repository_name).map { |p| property_schema_statement(property_schema_hash(repository, p)) } * ', '}
506
- EOS
538
+ operation.each do |operand|
539
+ statement, values = conditions_statement(operand, qualify)
507
540
 
508
- if (key = model.key(repository_name)).any?
509
- statement << ", PRIMARY KEY(#{ key.map { |p| quote_column_name(p.field(repository_name)) } * ', '})"
541
+ if operand.respond_to?(:operands) && operand.operands.size > 1
542
+ statement = "(#{statement})"
510
543
  end
511
544
 
512
- statement << ')'
513
- statement
545
+ statements << statement
546
+ bind_values.concat(values)
514
547
  end
515
548
 
516
- # TODO: move to dm-more/dm-migrations
517
- def drop_table_statement(repository, model)
518
- "DROP TABLE IF EXISTS #{quote_table_name(model.storage_name(repository.name))}"
519
- end
549
+ join_with = operation.kind_of?(@negated ? Query::Conditions::OrOperation : Query::Conditions::AndOperation) ? 'AND' : 'OR'
550
+ statement = statements.join(" #{join_with} ")
520
551
 
521
- # TODO: move to dm-more/dm-migrations
522
- def create_index_statements(repository, model)
523
- table_name = model.storage_name(repository.name)
524
- model.properties(repository.name).indexes.map do |index_name, fields|
525
- <<-EOS.compress_lines
526
- CREATE INDEX #{quote_column_name("index_#{table_name}_#{index_name}")} ON
527
- #{quote_table_name(table_name)} (#{fields.map { |f| quote_column_name(f) } * ', '})
528
- EOS
529
- end
530
- end
552
+ return statement, bind_values
553
+ end
531
554
 
532
- # TODO: move to dm-more/dm-migrations
533
- def create_unique_index_statements(repository, model)
534
- table_name = model.storage_name(repository.name)
535
- model.properties(repository.name).unique_indexes.map do |index_name, fields|
536
- <<-EOS.compress_lines
537
- CREATE UNIQUE INDEX #{quote_column_name("unique_index_#{table_name}_#{index_name}")} ON
538
- #{quote_table_name(table_name)} (#{fields.map { |f| quote_column_name(f) } * ', '})
539
- EOS
540
- end
541
- end
555
+ # Constructs comparison clause
556
+ #
557
+ # @return [String]
558
+ # comparison clause
559
+ #
560
+ # @api private
561
+ def comparison_statement(comparison, qualify)
562
+ value = comparison.value
542
563
 
543
- # TODO: move to dm-more/dm-migrations
544
- def property_schema_hash(repository, property)
545
- schema = self.class.type_map[property.type].merge(:name => property.field(repository.name))
546
- # TODO: figure out a way to specify the size not be included, even if
547
- # a default is defined in the typemap
548
- # - use this to make it so all TEXT primitive fields do not have size
549
- if property.primitive == String && schema[:primitive] != 'TEXT'
550
- schema[:size] = property.length
551
- elsif property.primitive == BigDecimal || property.primitive == Float
552
- schema[:precision] = property.precision
553
- schema[:scale] = property.scale
554
- end
564
+ # TODO: move exclusive Range handling into another method, and
565
+ # update conditions_statement to use it
555
566
 
556
- schema[:nullable?] = property.nullable?
557
- schema[:serial?] = property.serial?
567
+ # break exclusive Range queries up into two comparisons ANDed together
568
+ if value.kind_of?(Range) && value.exclude_end?
569
+ operation = Query::Conditions::Operation.new(:and,
570
+ Query::Conditions::Comparison.new(:gte, comparison.subject, value.first),
571
+ Query::Conditions::Comparison.new(:lt, comparison.subject, value.last)
572
+ )
558
573
 
559
- if property.default.nil? || property.default.respond_to?(:call)
560
- # remove the default if the property is not nullable
561
- schema.delete(:default) unless property.nullable?
562
- else
563
- if property.type.respond_to?(:dump)
564
- schema[:default] = property.type.dump(property.default, property)
565
- else
566
- schema[:default] = property.default
567
- end
568
- end
574
+ statement, bind_values = conditions_statement(operation, qualify)
569
575
 
570
- schema
576
+ return "(#{statement})", bind_values
577
+ elsif comparison.relationship?
578
+ return conditions_statement(comparison.foreign_key_mapping, qualify)
571
579
  end
572
580
 
573
- # TODO: move to dm-more/dm-migrations
574
- def property_schema_statement(schema)
575
- statement = quote_column_name(schema[:name])
576
- statement << " #{schema[:primitive]}"
577
-
578
- if schema[:precision] && schema[:scale]
579
- statement << "(#{[ :precision, :scale ].map { |k| quote_column_value(schema[k]) } * ','})"
580
- elsif schema[:size]
581
- statement << "(#{quote_column_value(schema[:size])})"
582
- end
581
+ operator = case comparison
582
+ when Query::Conditions::EqualToComparison then @negated ? inequality_operator(comparison.subject, value) : equality_operator(comparison.subject, value)
583
+ when Query::Conditions::InclusionComparison then @negated ? exclude_operator(comparison.subject, value) : include_operator(comparison.subject, value)
584
+ when Query::Conditions::RegexpComparison then @negated ? not_regexp_operator(value) : regexp_operator(value)
585
+ when Query::Conditions::LikeComparison then @negated ? unlike_operator(value) : like_operator(value)
586
+ when Query::Conditions::GreaterThanComparison then @negated ? '<=' : '>'
587
+ when Query::Conditions::LessThanComparison then @negated ? '>=' : '<'
588
+ when Query::Conditions::GreaterThanOrEqualToComparison then @negated ? '<' : '>='
589
+ when Query::Conditions::LessThanOrEqualToComparison then @negated ? '>' : '<='
590
+ end
583
591
 
584
- statement << ' NOT NULL' unless schema[:nullable?]
585
- statement << " DEFAULT #{quote_column_value(schema[:default])}" if schema.has_key?(:default)
586
- statement
592
+ # if operator return value contains ? then it means that it is function call
593
+ # and it contains placeholder (%s) for property name as well (used in Oracle adapter for regexp operator)
594
+ if operator.include?('?')
595
+ return operator % property_to_column_name(comparison.subject, qualify), [ value ]
596
+ else
597
+ return "#{property_to_column_name(comparison.subject, qualify)} #{operator} #{value.nil? ? 'NULL' : '?'}", [ value ].compact
587
598
  end
599
+ end
588
600
 
589
- # TODO: move to dm-more/dm-migrations
590
- def relationship_schema_hash(relationship)
591
- identifier, relationship = relationship
601
+ # TODO: document
602
+ # @api private
603
+ def equality_operator(property, operand)
604
+ operand.nil? ? 'IS' : '='
605
+ end
592
606
 
593
- self.class.type_map[Integer].merge(:name => "#{identifier}_id") if identifier == relationship.name
594
- end
607
+ # TODO: document
608
+ # @api private
609
+ def inequality_operator(property, operand)
610
+ operand.nil? ? 'IS NOT' : '<>'
611
+ end
595
612
 
596
- # TODO: move to dm-more/dm-migrations
597
- def relationship_schema_statement(hash)
598
- property_schema_statement(hash) unless hash.nil?
599
- end
600
- end # module SQL
601
-
602
- include SQL
603
-
604
- module ClassMethods
605
- # Default TypeMap for all data object based adapters.
606
- #
607
- # @return <DataMapper::TypeMap> default TypeMap for data objects adapters.
608
- #
609
- # TODO: move to dm-more/dm-migrations
610
- def type_map
611
- @type_map ||= TypeMap.new(super) do |tm|
612
- tm.map(Integer).to('INT')
613
- tm.map(String).to('VARCHAR').with(:size => Property::DEFAULT_LENGTH)
614
- tm.map(Class).to('VARCHAR').with(:size => Property::DEFAULT_LENGTH)
615
- tm.map(DM::Discriminator).to('VARCHAR').with(:size => Property::DEFAULT_LENGTH)
616
- tm.map(BigDecimal).to('DECIMAL').with(:precision => Property::DEFAULT_PRECISION, :scale => Property::DEFAULT_SCALE_BIGDECIMAL)
617
- tm.map(Float).to('FLOAT').with(:precision => Property::DEFAULT_PRECISION)
618
- tm.map(DateTime).to('DATETIME')
619
- tm.map(Date).to('DATE')
620
- tm.map(Time).to('TIMESTAMP')
621
- tm.map(TrueClass).to('BOOLEAN')
622
- tm.map(DM::Object).to('TEXT')
623
- tm.map(DM::Text).to('TEXT')
624
- end
613
+ # TODO: document
614
+ # @api private
615
+ def include_operator(property, operand)
616
+ case operand
617
+ when Array then 'IN'
618
+ when Range then 'BETWEEN'
625
619
  end
626
- end # module ClassMethods
627
- end # module Migration
628
-
629
- include Migration
630
- extend Migration::ClassMethods
631
- end # class DataObjectsAdapter
632
- end # module Adapters
620
+ end
633
621
 
634
- # TODO: move to dm-ar-finders
635
- module Model
636
- #
637
- # Find instances by manually providing SQL
638
- #
639
- # @param sql<String> an SQL query to execute
640
- # @param <Array> an Array containing a String (being the SQL query to
641
- # execute) and the parameters to the query.
642
- # example: ["SELECT name FROM users WHERE id = ?", id]
643
- # @param query<DataMapper::Query> a prepared Query to execute.
644
- # @param opts<Hash> an options hash.
645
- # :repository<Symbol> the name of the repository to execute the query
646
- # in. Defaults to self.default_repository_name.
647
- # :reload<Boolean> whether to reload any instances found that already
648
- # exist in the identity map. Defaults to false.
649
- # :properties<Array> the Properties of the instance that the query
650
- # loads. Must contain DataMapper::Properties.
651
- # Defaults to self.properties.
652
- #
653
- # @note
654
- # A String, Array or Query is required.
655
- # @return <Collection> the instance matched by the query.
656
- #
657
- # @example
658
- # MyClass.find_by_sql(["SELECT id FROM my_classes WHERE county = ?",
659
- # selected_county], :properties => MyClass.property[:id],
660
- # :repository => :county_repo)
661
- #
662
- # -
663
- # @api public
664
- def find_by_sql(*args)
665
- sql = nil
666
- query = nil
667
- bind_values = []
668
- properties = nil
669
- do_reload = false
670
- repository_name = default_repository_name
671
- args.each do |arg|
672
- if arg.is_a?(String)
673
- sql = arg
674
- elsif arg.is_a?(Array)
675
- sql = arg.first
676
- bind_values = arg[1..-1]
677
- elsif arg.is_a?(DataMapper::Query)
678
- query = arg
679
- elsif arg.is_a?(Hash)
680
- repository_name = arg.delete(:repository) if arg.include?(:repository)
681
- properties = Array(arg.delete(:properties)) if arg.include?(:properties)
682
- do_reload = arg.delete(:reload) if arg.include?(:reload)
683
- raise "unknown options to #find_by_sql: #{arg.inspect}" unless arg.empty?
622
+ # TODO: document
623
+ # @api private
624
+ def exclude_operator(property, operand)
625
+ "NOT #{include_operator(property, operand)}"
684
626
  end
685
- end
686
627
 
687
- repository = repository(repository_name)
688
- raise "#find_by_sql only available for Repositories served by a DataObjectsAdapter" unless repository.adapter.is_a?(DataMapper::Adapters::DataObjectsAdapter)
628
+ # TODO: document
629
+ # @api private
630
+ def regexp_operator(operand)
631
+ '~'
632
+ end
689
633
 
690
- if query
691
- sql = repository.adapter.send(:read_statement, query)
692
- bind_values = query.bind_values
693
- end
634
+ # TODO: document
635
+ # @api private
636
+ def not_regexp_operator(operand)
637
+ '!~'
638
+ end
694
639
 
695
- raise "#find_by_sql requires a query of some kind to work" unless sql
640
+ # TODO: document
641
+ # @api private
642
+ def like_operator(operand)
643
+ 'LIKE'
644
+ end
696
645
 
697
- properties ||= self.properties(repository.name)
646
+ # TODO: document
647
+ # @api private
648
+ def unlike_operator(operand)
649
+ 'NOT LIKE'
650
+ end
698
651
 
699
- Collection.new(Query.new(repository, self)) do |collection|
700
- repository.adapter.send(:with_connection) do |connection|
701
- command = connection.create_command(sql)
652
+ # TODO: document
653
+ # @api private
654
+ def quote_name(name)
655
+ "\"#{name[0, self.class::IDENTIFIER_MAX_LENGTH].gsub('"', '""')}\""
656
+ end
657
+ end #module SQL
702
658
 
703
- begin
704
- reader = command.execute_reader(*bind_values)
659
+ include SQL
660
+ end # class DataObjectsAdapter
705
661
 
706
- while(reader.next!)
707
- collection.load(reader.values)
708
- end
709
- ensure
710
- reader.close if reader
711
- end
712
- end
713
- end
714
- end
715
- end # module Model
662
+ const_added(:DataObjectsAdapter)
663
+ end # module Adapters
716
664
  end # module DataMapper