sam-dm-core 0.9.6

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