rpbertp13-dm-core 0.9.11.1

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 (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 +52 -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 +32 -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 +138 -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 +221 -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 +252 -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 +60 -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 +469 -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