datamapper-dm-core 0.9.11 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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