datamapper-dm-core 0.9.11

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