dm-core 0.9.2

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