sam-dm-core 0.9.6

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