dm-core 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/CHANGELOG +144 -0
  2. data/FAQ +74 -0
  3. data/MIT-LICENSE +22 -0
  4. data/QUICKLINKS +12 -0
  5. data/README +143 -0
  6. data/lib/dm-core.rb +213 -0
  7. data/lib/dm-core/adapters.rb +4 -0
  8. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +701 -0
  10. data/lib/dm-core/adapters/mysql_adapter.rb +132 -0
  11. data/lib/dm-core/adapters/postgres_adapter.rb +179 -0
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  13. data/lib/dm-core/associations.rb +172 -0
  14. data/lib/dm-core/associations/many_to_many.rb +138 -0
  15. data/lib/dm-core/associations/many_to_one.rb +101 -0
  16. data/lib/dm-core/associations/one_to_many.rb +275 -0
  17. data/lib/dm-core/associations/one_to_one.rb +61 -0
  18. data/lib/dm-core/associations/relationship.rb +116 -0
  19. data/lib/dm-core/associations/relationship_chain.rb +74 -0
  20. data/lib/dm-core/auto_migrations.rb +64 -0
  21. data/lib/dm-core/collection.rb +604 -0
  22. data/lib/dm-core/hook.rb +11 -0
  23. data/lib/dm-core/identity_map.rb +45 -0
  24. data/lib/dm-core/is.rb +16 -0
  25. data/lib/dm-core/logger.rb +233 -0
  26. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  27. data/lib/dm-core/migrator.rb +29 -0
  28. data/lib/dm-core/model.rb +399 -0
  29. data/lib/dm-core/naming_conventions.rb +52 -0
  30. data/lib/dm-core/property.rb +611 -0
  31. data/lib/dm-core/property_set.rb +158 -0
  32. data/lib/dm-core/query.rb +590 -0
  33. data/lib/dm-core/repository.rb +159 -0
  34. data/lib/dm-core/resource.rb +618 -0
  35. data/lib/dm-core/scope.rb +35 -0
  36. data/lib/dm-core/support.rb +7 -0
  37. data/lib/dm-core/support/array.rb +13 -0
  38. data/lib/dm-core/support/assertions.rb +8 -0
  39. data/lib/dm-core/support/errors.rb +23 -0
  40. data/lib/dm-core/support/kernel.rb +7 -0
  41. data/lib/dm-core/support/symbol.rb +41 -0
  42. data/lib/dm-core/transaction.rb +267 -0
  43. data/lib/dm-core/type.rb +160 -0
  44. data/lib/dm-core/type_map.rb +80 -0
  45. data/lib/dm-core/types.rb +19 -0
  46. data/lib/dm-core/types/boolean.rb +7 -0
  47. data/lib/dm-core/types/discriminator.rb +32 -0
  48. data/lib/dm-core/types/object.rb +20 -0
  49. data/lib/dm-core/types/paranoid_boolean.rb +23 -0
  50. data/lib/dm-core/types/paranoid_datetime.rb +22 -0
  51. data/lib/dm-core/types/serial.rb +9 -0
  52. data/lib/dm-core/types/text.rb +10 -0
  53. data/spec/integration/association_spec.rb +1215 -0
  54. data/spec/integration/association_through_spec.rb +150 -0
  55. data/spec/integration/associations/many_to_many_spec.rb +171 -0
  56. data/spec/integration/associations/many_to_one_spec.rb +123 -0
  57. data/spec/integration/associations/one_to_many_spec.rb +66 -0
  58. data/spec/integration/auto_migrations_spec.rb +398 -0
  59. data/spec/integration/collection_spec.rb +1015 -0
  60. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  61. data/spec/integration/model_spec.rb +68 -0
  62. data/spec/integration/mysql_adapter_spec.rb +85 -0
  63. data/spec/integration/postgres_adapter_spec.rb +732 -0
  64. data/spec/integration/property_spec.rb +224 -0
  65. data/spec/integration/query_spec.rb +376 -0
  66. data/spec/integration/repository_spec.rb +57 -0
  67. data/spec/integration/resource_spec.rb +324 -0
  68. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  69. data/spec/integration/sti_spec.rb +185 -0
  70. data/spec/integration/transaction_spec.rb +75 -0
  71. data/spec/integration/type_spec.rb +149 -0
  72. data/spec/lib/mock_adapter.rb +27 -0
  73. data/spec/spec_helper.rb +112 -0
  74. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  75. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  76. data/spec/unit/adapters/data_objects_adapter_spec.rb +627 -0
  77. data/spec/unit/adapters/postgres_adapter_spec.rb +125 -0
  78. data/spec/unit/associations/many_to_many_spec.rb +14 -0
  79. data/spec/unit/associations/many_to_one_spec.rb +138 -0
  80. data/spec/unit/associations/one_to_many_spec.rb +385 -0
  81. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  82. data/spec/unit/associations/relationship_spec.rb +67 -0
  83. data/spec/unit/associations_spec.rb +205 -0
  84. data/spec/unit/auto_migrations_spec.rb +110 -0
  85. data/spec/unit/collection_spec.rb +174 -0
  86. data/spec/unit/data_mapper_spec.rb +21 -0
  87. data/spec/unit/identity_map_spec.rb +126 -0
  88. data/spec/unit/is_spec.rb +80 -0
  89. data/spec/unit/migrator_spec.rb +33 -0
  90. data/spec/unit/model_spec.rb +339 -0
  91. data/spec/unit/naming_conventions_spec.rb +28 -0
  92. data/spec/unit/property_set_spec.rb +96 -0
  93. data/spec/unit/property_spec.rb +447 -0
  94. data/spec/unit/query_spec.rb +485 -0
  95. data/spec/unit/repository_spec.rb +93 -0
  96. data/spec/unit/resource_spec.rb +557 -0
  97. data/spec/unit/scope_spec.rb +131 -0
  98. data/spec/unit/transaction_spec.rb +493 -0
  99. data/spec/unit/type_map_spec.rb +114 -0
  100. data/spec/unit/type_spec.rb +119 -0
  101. metadata +187 -0
@@ -0,0 +1,158 @@
1
+ module DataMapper
2
+ class PropertySet
3
+ include Assertions
4
+ include Enumerable
5
+
6
+ def [](name)
7
+ @property_for[name]
8
+ end
9
+
10
+ alias has_property? []
11
+
12
+ def slice(*names)
13
+ @property_for.values_at(*names)
14
+ end
15
+
16
+ def add(*properties)
17
+ @entries.push(*properties)
18
+ properties.each { |property| property.hash }
19
+ self
20
+ end
21
+
22
+ alias << add
23
+
24
+ def length
25
+ @entries.length
26
+ end
27
+
28
+ def empty?
29
+ @entries.empty?
30
+ end
31
+
32
+ def each
33
+ @entries.each { |property| yield property }
34
+ self
35
+ end
36
+
37
+ def defaults
38
+ reject { |property| property.lazy? }
39
+ end
40
+
41
+ def key
42
+ select { |property| property.key? }
43
+ end
44
+
45
+ def indexes
46
+ index_hash = {}
47
+ each { |property| parse_index(property.index, property.field.to_s, index_hash) }
48
+ index_hash
49
+ end
50
+
51
+ def unique_indexes
52
+ index_hash = {}
53
+ each { |property| parse_index(property.unique_index, property.field.to_s, index_hash) }
54
+ index_hash
55
+ end
56
+
57
+ def inheritance_property
58
+ detect { |property| property.type == DataMapper::Types::Discriminator }
59
+ end
60
+
61
+ def get(resource)
62
+ map { |property| property.get(resource) }
63
+ end
64
+
65
+ def set(resource, values)
66
+ if values.kind_of?(Array) && values.length != length
67
+ raise ArgumentError, "+values+ must have a length of #{length}, but has #{values.length}", caller
68
+ end
69
+
70
+ each_with_index { |property,i| property.set(resource, values.nil? ? nil : values[i]) }
71
+ end
72
+
73
+ def property_contexts(name)
74
+ contexts = []
75
+ lazy_contexts.each do |context,property_names|
76
+ contexts << context if property_names.include?(name)
77
+ end
78
+ contexts
79
+ end
80
+
81
+ def lazy_context(name)
82
+ lazy_contexts[name]
83
+ end
84
+
85
+ def lazy_load_context(names)
86
+ if names.kind_of?(Array) && names.empty?
87
+ raise ArgumentError, '+names+ cannot be empty', caller
88
+ end
89
+
90
+ result = []
91
+
92
+ Array(names).each do |name|
93
+ contexts = property_contexts(name)
94
+ if contexts.empty?
95
+ result << name # not lazy
96
+ else
97
+ result |= lazy_contexts.values_at(*contexts).flatten.uniq
98
+ end
99
+ end
100
+ result
101
+ end
102
+
103
+ def to_query(bind_values)
104
+ Hash[ *zip(bind_values).flatten ]
105
+ end
106
+
107
+ def inspect
108
+ '#<PropertySet:{' + map { |property| property.inspect }.join(',') + '}>'
109
+ end
110
+
111
+ def dup(target = nil)
112
+ return super() unless target
113
+
114
+ properties = map do |property|
115
+ Property.new(target || property.model, property.name, property.type, property.options.dup)
116
+ end
117
+
118
+ self.class.new(properties)
119
+ end
120
+
121
+ private
122
+
123
+ def initialize(properties = [])
124
+ assert_kind_of 'properties', properties, Enumerable
125
+
126
+ @entries = properties
127
+ @property_for = hash_for_property_for
128
+ end
129
+
130
+ def initialize_copy(orig)
131
+ @entries = orig.entries.dup
132
+ @property_for = hash_for_property_for
133
+ end
134
+
135
+ def hash_for_property_for
136
+ Hash.new do |h,k|
137
+ ksym = k.to_sym
138
+ if property = detect { |property| property.name == ksym }
139
+ h[ksym] = h[k.to_s] = property
140
+ end
141
+ end
142
+ end
143
+
144
+ def lazy_contexts
145
+ @lazy_contexts ||= Hash.new { |h,context| h[context] = [] }
146
+ end
147
+
148
+ def parse_index(index, property, index_hash)
149
+ case index
150
+ when true then index_hash[property] = [property]
151
+ when Symbol
152
+ index_hash[index.to_s] ||= []
153
+ index_hash[index.to_s] << property
154
+ when Enumerable then index.each { |idx| parse_index(idx, property, index_hash) }
155
+ end
156
+ end
157
+ end # class PropertySet
158
+ end # module DataMapper
@@ -0,0 +1,590 @@
1
+ module DataMapper
2
+ class Query
3
+ include Assertions
4
+
5
+ OPTIONS = [
6
+ :reload, :offset, :limit, :order, :add_reversed, :fields, :links, :includes, :conditions
7
+ ]
8
+
9
+ attr_reader :repository, :model, *OPTIONS - [ :reload ]
10
+ attr_writer :add_reversed
11
+ alias add_reversed? add_reversed
12
+
13
+ def reload?
14
+ @reload
15
+ end
16
+
17
+ def reverse
18
+ dup.reverse!
19
+ end
20
+
21
+ def reverse!
22
+ # reverse the sort order
23
+ update(:order => self.order.map { |o| o.reverse })
24
+
25
+ self
26
+ end
27
+
28
+ def update(other)
29
+ assert_kind_of 'other', other, self.class, Hash
30
+
31
+ assert_valid_other(other)
32
+
33
+ if other.kind_of?(Hash)
34
+ return self if other.empty?
35
+ other = self.class.new(@repository, model, other)
36
+ end
37
+
38
+ return self if self == other
39
+
40
+ # TODO: update this so if "other" had a value explicitly set
41
+ # overwrite the attributes in self
42
+
43
+ # only overwrite the attributes with non-default values
44
+ @reload = other.reload? unless other.reload? == false
45
+ @offset = other.offset unless other.offset == 0
46
+ @limit = other.limit unless other.limit == nil
47
+ @order = other.order unless other.order == model.default_order
48
+ @add_reversed = other.add_reversed? unless other.add_reversed? == false
49
+ @fields = other.fields unless other.fields == @properties.defaults
50
+ @links = other.links unless other.links == []
51
+ @includes = other.includes unless other.includes == []
52
+
53
+ update_conditions(other)
54
+
55
+ self
56
+ end
57
+
58
+ def merge(other)
59
+ dup.update(other)
60
+ end
61
+
62
+ def ==(other)
63
+ return true if super
64
+ return false unless other.kind_of?(self.class)
65
+
66
+ # TODO: add a #hash method, and then use it in the comparison, eg:
67
+ # return hash == other.hash
68
+ @model == other.model &&
69
+ @reload == other.reload? &&
70
+ @offset == other.offset &&
71
+ @limit == other.limit &&
72
+ @order == other.order && # order is significant, so do not sort this
73
+ @add_reversed == other.add_reversed? &&
74
+ @fields == other.fields && # TODO: sort this so even if the order is different, it is equal
75
+ @links == other.links && # TODO: sort this so even if the order is different, it is equal
76
+ @includes == other.includes && # TODO: sort this so even if the order is different, it is equal
77
+ @conditions.sort_by { |c| c.at(0).hash + c.at(1).hash + c.at(2).hash } == other.conditions.sort_by { |c| c.at(0).hash + c.at(1).hash + c.at(2).hash }
78
+ end
79
+
80
+ alias eql? ==
81
+
82
+ def bind_values
83
+ bind_values = []
84
+ conditions.each do |tuple|
85
+ next if tuple.size == 2
86
+ operator, property, bind_value = *tuple
87
+ if :raw == operator
88
+ bind_values.push(*bind_value)
89
+ else
90
+ bind_values << bind_value
91
+ end
92
+ end
93
+ bind_values
94
+ end
95
+
96
+ # TODO: spec this
97
+ def inheritance_property_index(repository)
98
+ fields.index(model.inheritance_property(repository.name))
99
+ end
100
+
101
+ # TODO: spec this
102
+ def key_property_indexes(repository)
103
+ if (key_property_indexes = model.key(repository.name).map { |property| fields.index(property) }).all?
104
+ key_property_indexes
105
+ end
106
+ end
107
+
108
+ # find the point in self.conditions where the sub select tuple is
109
+ # located. Delete the tuple and add value.conditions. value must be a
110
+ # <DM::Query>
111
+ #
112
+ def merge_subquery(operator, property, value)
113
+ assert_kind_of 'value', value, self.class
114
+
115
+ new_conditions = []
116
+ conditions.each do |tuple|
117
+ if tuple.at(0).to_s == operator.to_s && tuple.at(1) == property && tuple.at(2) == value
118
+ value.conditions.each do |subquery_tuple|
119
+ new_conditions << subquery_tuple
120
+ end
121
+ else
122
+ new_conditions << tuple
123
+ end
124
+ end
125
+ @conditions = new_conditions
126
+ end
127
+
128
+ def inspect
129
+ attrs = [
130
+ [ :repository, repository.name ],
131
+ [ :model, model ],
132
+ [ :fields, fields ],
133
+ [ :links, links ],
134
+ [ :conditions, conditions ],
135
+ [ :order, order ],
136
+ [ :limit, limit ],
137
+ [ :offset, offset ],
138
+ [ :reload, reload? ],
139
+ ]
140
+
141
+ "#<#{self.class.name} #{attrs.map { |(k,v)| "@#{k}=#{v.inspect}" } * ' '}>"
142
+ end
143
+
144
+ private
145
+
146
+ def initialize(repository, model, options = {})
147
+ assert_kind_of 'repository', repository, Repository
148
+ assert_kind_of 'model', model, Model
149
+ assert_kind_of 'options', options, Hash
150
+
151
+ options.each_pair { |k,v| options[k] = v.call if v.is_a? Proc } if options.is_a? Hash
152
+
153
+ assert_valid_options(options)
154
+
155
+ @repository = repository
156
+ @properties = model.properties(@repository.name)
157
+
158
+ @model = model # must be Class that includes DM::Resource
159
+ @reload = options.fetch :reload, false # must be true or false
160
+ @offset = options.fetch :offset, 0 # must be an Integer greater than or equal to 0
161
+ @limit = options.fetch :limit, nil # must be an Integer greater than or equal to 1
162
+ @order = options.fetch :order, model.default_order # must be an Array of Symbol, DM::Query::Direction or DM::Property
163
+ @add_reversed = options.fetch :add_reversed, false # must be true or false
164
+ @fields = options.fetch :fields, @properties.defaults # must be an Array of Symbol, String or DM::Property
165
+ @links = options.fetch :links, [] # must be an Array of Tuples - Tuple [DM::Query,DM::Assoc::Relationship]
166
+ @includes = options.fetch :includes, [] # must be an Array of DM::Query::Path
167
+ @conditions = [] # must be an Array of triplets (or pairs when passing in raw String queries)
168
+
169
+ # normalize order and fields
170
+ @order = normalize_order(@order)
171
+ @fields = normalize_fields(@fields)
172
+
173
+ # XXX: should I validate that each property in @order corresponds
174
+ # to something in @fields? Many DB engines require they match,
175
+ # and I can think of no valid queries where a field would be so
176
+ # important that you sort on it, but not important enough to
177
+ # return.
178
+
179
+ # normalize links and includes.
180
+ # NOTE: this must be done after order and fields
181
+ @links = normalize_links(@links)
182
+ @includes = normalize_includes(@includes)
183
+
184
+ translate_custom_types(@properties, options)
185
+
186
+ # treat all non-options as conditions
187
+ (options.keys - OPTIONS - OPTIONS.map { |option| option.to_s }).each do |k|
188
+ append_condition(k, options[k])
189
+ end
190
+
191
+ # parse raw options[:conditions] differently
192
+ if conditions = options[:conditions]
193
+ if conditions.kind_of?(Hash)
194
+ conditions.each do |k,v|
195
+ append_condition(k, v)
196
+ end
197
+ elsif conditions.kind_of?(Array)
198
+ raw_query, *bind_values = conditions
199
+ @conditions << if bind_values.empty?
200
+ [ :raw, raw_query ]
201
+ else
202
+ [ :raw, raw_query, bind_values ]
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ def initialize_copy(original)
209
+ # deep-copy the condition tuples when copying the object
210
+ @conditions = original.conditions.map { |tuple| tuple.dup }
211
+ end
212
+
213
+ def translate_custom_types(properties, options)
214
+ options.each do |key, value|
215
+ case key
216
+ when DataMapper::Query::Operator
217
+ options[key] = properties[key.target].type.dump(value, properties[key.target]) if !properties[key.target].nil? && properties[key.target].custom?
218
+ when Symbol, String
219
+ options[key] = properties[key].type.dump(value, properties[key]) if !properties[key].nil? && properties[key].custom?
220
+ end
221
+ end
222
+ end
223
+
224
+ # validate the options
225
+ def assert_valid_options(options)
226
+ # validate the reload option
227
+ if options.has_key?(:reload) && options[:reload] != true && options[:reload] != false
228
+ raise ArgumentError, "+options[:reload]+ must be true or false, but was #{options[:reload].inspect}", caller(2)
229
+ end
230
+
231
+ # validate the offset and limit options
232
+ ([ :offset, :limit ] & options.keys).each do |attribute|
233
+ value = options[attribute]
234
+ assert_kind_of "options[:#{attribute}]", value, Integer
235
+ end
236
+
237
+ if options.has_key?(:offset) && options[:offset] < 0
238
+ raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{options[:offset].inspect}", caller(2)
239
+ end
240
+
241
+ if options.has_key?(:limit) && options[:limit] < 1
242
+ raise ArgumentError, "+options[:limit]+ must be greater than or equal to 1, but was #{options[:limit].inspect}", caller(2)
243
+ end
244
+
245
+ # validate the order, fields, links, includes and conditions options
246
+ ([ :order, :fields, :links, :includes ] & options.keys).each do |attribute|
247
+ value = options[attribute]
248
+ assert_kind_of "options[:#{attribute}]", value, Array
249
+
250
+ if value.empty?
251
+ raise ArgumentError, "+options[:#{attribute}]+ cannot be empty", caller(2)
252
+ end
253
+ end
254
+
255
+ if options.has_key?(:conditions)
256
+ value = options[:conditions]
257
+ assert_kind_of 'options[:conditions]', value, Hash, Array
258
+
259
+ if value.empty?
260
+ raise ArgumentError, '+options[:conditions]+ cannot be empty', caller(2)
261
+ end
262
+ end
263
+ end
264
+
265
+ # validate other DM::Query or Hash object
266
+ def assert_valid_other(other)
267
+ return unless other.kind_of?(self.class)
268
+
269
+ unless other.repository == repository
270
+ raise ArgumentError, "+other+ #{self.class} must be for the #{repository.name} repository, not #{other.repository.name}", caller(2)
271
+ end
272
+
273
+ unless other.model == model
274
+ raise ArgumentError, "+other+ #{self.class} must be for the #{model.name} model, not #{other.model.name}", caller(2)
275
+ end
276
+ end
277
+
278
+ # normalize order elements to DM::Query::Direction
279
+ def normalize_order(order)
280
+ order.map do |order_by|
281
+ case order_by
282
+ when Direction
283
+ # NOTE: The property is available via order_by.property
284
+ # TODO: if the Property's model doesn't match
285
+ # self.model, append the property's model to @links
286
+ # eg:
287
+ #if property.model != self.model
288
+ # @links << discover_path_for_property(property)
289
+ #end
290
+
291
+ order_by
292
+ when Property
293
+ # TODO: if the Property's model doesn't match
294
+ # self.model, append the property's model to @links
295
+ # eg:
296
+ #if property.model != self.model
297
+ # @links << discover_path_for_property(property)
298
+ #end
299
+
300
+ Direction.new(order_by)
301
+ when Operator
302
+ property = @properties[order_by.target]
303
+ Direction.new(property, order_by.operator)
304
+ when Symbol, String
305
+ property = @properties[order_by]
306
+
307
+ if property.nil?
308
+ raise ArgumentError, "+options[:order]+ entry #{order_by} does not map to a DataMapper::Property", caller(2)
309
+ end
310
+
311
+ Direction.new(property)
312
+ else
313
+ raise ArgumentError, "+options[:order]+ entry #{order_by.inspect} not supported", caller(2)
314
+ end
315
+ end
316
+ end
317
+
318
+ # normalize fields to DM::Property
319
+ def normalize_fields(fields)
320
+ # TODO: return a PropertySet
321
+ fields.map do |field|
322
+ case field
323
+ when Property
324
+ # TODO: if the Property's model doesn't match
325
+ # self.model, append the property's model to @links
326
+ # eg:
327
+ #if property.model != self.model
328
+ # @links << discover_path_for_property(property)
329
+ #end
330
+ field
331
+ when Symbol, String
332
+ property = @properties[field]
333
+
334
+ if property.nil?
335
+ raise ArgumentError, "+options[:fields]+ entry #{field} does not map to a DataMapper::Property", caller(2)
336
+ end
337
+
338
+ property
339
+ else
340
+ raise ArgumentError, "+options[:fields]+ entry #{field.inspect} not supported", caller(2)
341
+ end
342
+ end
343
+ end
344
+
345
+ # normalize links to DM::Query::Path
346
+ def normalize_links(links)
347
+ # XXX: this should normalize to DM::Query::Path, not DM::Association::Relationship
348
+ # because a link may be more than one-hop-away from the source. A DM::Query::Path
349
+ # should include an Array of Relationship objects that trace the "path" between
350
+ # the source and the target.
351
+ links.map do |link|
352
+ case link
353
+ when Associations::Relationship
354
+ link
355
+ when Symbol, String
356
+ link = link.to_sym if link.kind_of?(String)
357
+
358
+ unless model.relationships(@repository.name).has_key?(link)
359
+ raise ArgumentError, "+options[:links]+ entry #{link} does not map to a DataMapper::Associations::Relationship", caller(2)
360
+ end
361
+
362
+ model.relationships(@repository.name)[link]
363
+ else
364
+ raise ArgumentError, "+options[:links]+ entry #{link.inspect} not supported", caller(2)
365
+ end
366
+ end
367
+ end
368
+
369
+ # normalize includes to DM::Query::Path
370
+ def normalize_includes(includes)
371
+ # TODO: normalize Array of Symbol, String, DM::Property 1-jump-away or DM::Query::Path
372
+ # NOTE: :includes can only be and array of DM::Query::Path objects now. This method
373
+ # can go away after review of what has been done.
374
+ includes
375
+ end
376
+
377
+ # validate that all the links or includes are present for the given DM::Query::Path
378
+ #
379
+ def validate_query_path_links(path)
380
+ path.relationships.map do |relationship|
381
+ @links << relationship unless (@links.include?(relationship) || @includes.include?(relationship))
382
+ end
383
+ end
384
+
385
+ def append_condition(clause, bind_value)
386
+ operator = :eql
387
+ bind_value = bind_value.call if bind_value.is_a?(Proc)
388
+
389
+ property = case clause
390
+ when Property
391
+ clause
392
+ when Query::Path
393
+ validate_query_path_links(clause)
394
+ clause
395
+ when Operator
396
+ operator = clause.operator
397
+ if clause.target.is_a?(Symbol)
398
+ @properties[clause.target]
399
+ elsif clause.target.is_a?(Query::Path)
400
+ validate_query_path_links(clause.target)
401
+ clause.target
402
+ end
403
+ when Symbol
404
+ @properties[clause]
405
+ when String
406
+ if clause =~ /\w\.\w/
407
+ query_path = @model
408
+ clause.split(".").each { |piece| query_path = query_path.send(piece) }
409
+ append_condition(query_path, bind_value)
410
+ return
411
+ else
412
+ @properties[clause]
413
+ end
414
+ else
415
+ raise ArgumentError, "Condition type #{clause.inspect} not supported", caller(2)
416
+ end
417
+
418
+ if property.nil?
419
+ raise ArgumentError, "Clause #{clause.inspect} does not map to a DataMapper::Property", caller(2)
420
+ end
421
+
422
+ @conditions << [ operator, property, bind_value ]
423
+ end
424
+
425
+ # TODO: check for other mutually exclusive operator + property
426
+ # combinations. For example if self's conditions were
427
+ # [ :gt, :amount, 5 ] and the other's condition is [ :lt, :amount, 2 ]
428
+ # there is a conflict. When in conflict the other's conditions
429
+ # overwrites self's conditions.
430
+
431
+ # TODO: Another condition is when the other condition operator is
432
+ # eql, this should over-write all the like,range and list operators
433
+ # for the same property, since we are now looking for an exact match.
434
+ # Vice versa, passing in eql should overwrite all of those operators.
435
+
436
+ def update_conditions(other)
437
+ @conditions = @conditions.dup
438
+
439
+ # build an index of conditions by the property and operator to
440
+ # avoid nested looping
441
+ conditions_index = Hash.new { |h,k| h[k] = {} }
442
+ @conditions.each do |condition|
443
+ operator, property = *condition
444
+ next if :raw == operator
445
+ conditions_index[property][operator] = condition
446
+ end
447
+
448
+ # loop over each of the other's conditions, and overwrite the
449
+ # conditions when in conflict
450
+ other.conditions.each do |other_condition|
451
+ other_operator, other_property, other_bind_value = *other_condition
452
+
453
+ unless :raw == other_operator
454
+ if condition = conditions_index[other_property][other_operator]
455
+ operator, property, bind_value = *condition
456
+
457
+ next if bind_value == other_bind_value
458
+
459
+ # overwrite the bind value in the existing condition
460
+ condition[2] = case operator
461
+ when :eql, :like then other_bind_value
462
+ when :gt, :gte then [ bind_value, other_bind_value ].min
463
+ when :lt, :lte then [ bind_value, other_bind_value ].max
464
+ when :not, :in
465
+ if bind_value.kind_of?(Array)
466
+ bind_value |= other_bind_value
467
+ elsif other_bind_value.kind_of?(Array)
468
+ other_bind_value |= bind_value
469
+ else
470
+ other_bind_value
471
+ end
472
+ end
473
+
474
+ next # process the next other condition
475
+ end
476
+ end
477
+
478
+ # otherwise append the other condition
479
+ @conditions << other_condition.dup
480
+ end
481
+
482
+ @conditions
483
+ end
484
+
485
+ class Direction
486
+ include Assertions
487
+
488
+ attr_reader :property, :direction
489
+
490
+ def ==(other)
491
+ return true if super
492
+ hash == other.hash
493
+ end
494
+
495
+ alias eql? ==
496
+
497
+ def hash
498
+ @property.hash + @direction.hash
499
+ end
500
+
501
+ def reverse
502
+ self.class.new(@property, @direction == :asc ? :desc : :asc)
503
+ end
504
+
505
+ def inspect
506
+ "#<#{self.class.name} #{@property.inspect} #{@direction}>"
507
+ end
508
+
509
+ private
510
+
511
+ def initialize(property, direction = :asc)
512
+ assert_kind_of 'property', property, Property
513
+ assert_kind_of 'direction', direction, Symbol
514
+
515
+ @property = property
516
+ @direction = direction
517
+ end
518
+ end # class Direction
519
+
520
+ class Operator
521
+ include Assertions
522
+
523
+ attr_reader :target, :operator
524
+
525
+ def to_sym
526
+ @property_name
527
+ end
528
+
529
+ def ==(other)
530
+ @operator == other.operator && @target == other.target
531
+ end
532
+
533
+ private
534
+
535
+ def initialize(target, operator)
536
+ assert_kind_of 'operator', operator, Symbol
537
+
538
+ @target = target
539
+ @operator = operator
540
+ end
541
+ end # class Operator
542
+
543
+ class Path
544
+ include Assertions
545
+
546
+ attr_reader :relationships, :model, :property, :operator
547
+
548
+ [ :gt, :gte, :lt, :lte, :not, :eql, :like, :in ].each do |sym|
549
+ class_eval <<-EOS, __FILE__, __LINE__
550
+ def #{sym}
551
+ Operator.new(self, :#{sym})
552
+ end
553
+ EOS
554
+ end
555
+
556
+ # duck type the DM::Query::Path to act like a DM::Property
557
+ def field(*args)
558
+ @property ? @property.field(*args) : nil
559
+ end
560
+
561
+ private
562
+
563
+ def initialize(repository, relationships, model, property_name = nil)
564
+ assert_kind_of 'repository', repository, Repository
565
+ assert_kind_of 'relationships', relationships, Array
566
+ assert_kind_of 'model', model, Model
567
+ assert_kind_of 'property_name', property_name, Symbol unless property_name.nil?
568
+
569
+ @repository = repository
570
+ @relationships = relationships
571
+ @model = model
572
+ @property = @model.properties(@repository.name)[property_name] if property_name
573
+ end
574
+
575
+ def method_missing(method, *args)
576
+ if relationship = @model.relationships(@repository.name)[method]
577
+ klass = klass = model == relationship.child_model ? relationship.parent_model : relationship.child_model
578
+ return Query::Path.new(@repository, @relationships + [ relationship ], klass)
579
+ end
580
+
581
+ if @model.properties(@repository.name)[method]
582
+ @property = @model.properties(@repository.name)[method]
583
+ return self
584
+ end
585
+
586
+ raise NoMethodError, "undefined property or association `#{method}' on #{@model}"
587
+ end
588
+ end # class Path
589
+ end # class Query
590
+ end # module DataMapper