dm-core 0.9.11 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (194) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -50
  5. data/Manifest.txt +66 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +6 -7
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/deps.rip +2 -0
  12. data/dm-core.gemspec +11 -15
  13. data/lib/dm-core.rb +105 -110
  14. data/lib/dm-core/adapters.rb +135 -16
  15. data/lib/dm-core/adapters/abstract_adapter.rb +251 -181
  16. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  17. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  18. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  19. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  22. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  23. data/lib/dm-core/associations/many_to_many.rb +372 -90
  24. data/lib/dm-core/associations/many_to_one.rb +220 -73
  25. data/lib/dm-core/associations/one_to_many.rb +319 -255
  26. data/lib/dm-core/associations/one_to_one.rb +66 -53
  27. data/lib/dm-core/associations/relationship.rb +561 -156
  28. data/lib/dm-core/collection.rb +1101 -379
  29. data/lib/dm-core/core_ext/kernel.rb +12 -0
  30. data/lib/dm-core/core_ext/symbol.rb +10 -0
  31. data/lib/dm-core/identity_map.rb +4 -34
  32. data/lib/dm-core/migrations.rb +1283 -0
  33. data/lib/dm-core/model.rb +570 -369
  34. data/lib/dm-core/model/descendant_set.rb +81 -0
  35. data/lib/dm-core/model/hook.rb +45 -0
  36. data/lib/dm-core/model/is.rb +32 -0
  37. data/lib/dm-core/model/property.rb +247 -0
  38. data/lib/dm-core/model/relationship.rb +335 -0
  39. data/lib/dm-core/model/scope.rb +90 -0
  40. data/lib/dm-core/property.rb +808 -273
  41. data/lib/dm-core/property_set.rb +141 -98
  42. data/lib/dm-core/query.rb +1037 -483
  43. data/lib/dm-core/query/conditions/comparison.rb +872 -0
  44. data/lib/dm-core/query/conditions/operation.rb +221 -0
  45. data/lib/dm-core/query/direction.rb +43 -0
  46. data/lib/dm-core/query/operator.rb +84 -0
  47. data/lib/dm-core/query/path.rb +138 -0
  48. data/lib/dm-core/query/sort.rb +45 -0
  49. data/lib/dm-core/repository.rb +210 -94
  50. data/lib/dm-core/resource.rb +641 -421
  51. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  52. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  53. data/lib/dm-core/support/chainable.rb +22 -0
  54. data/lib/dm-core/support/deprecate.rb +12 -0
  55. data/lib/dm-core/support/logger.rb +13 -0
  56. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  57. data/lib/dm-core/transaction.rb +333 -92
  58. data/lib/dm-core/type.rb +98 -60
  59. data/lib/dm-core/types/boolean.rb +1 -1
  60. data/lib/dm-core/types/discriminator.rb +34 -20
  61. data/lib/dm-core/types/object.rb +7 -4
  62. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  63. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  64. data/lib/dm-core/types/serial.rb +3 -3
  65. data/lib/dm-core/types/text.rb +3 -4
  66. data/lib/dm-core/version.rb +1 -1
  67. data/script/performance.rb +102 -109
  68. data/script/profile.rb +169 -38
  69. data/spec/lib/adapter_helpers.rb +105 -0
  70. data/spec/lib/collection_helpers.rb +18 -0
  71. data/spec/lib/counter_adapter.rb +34 -0
  72. data/spec/lib/pending_helpers.rb +27 -0
  73. data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
  74. data/spec/public/associations/many_to_many_spec.rb +193 -0
  75. data/spec/public/associations/many_to_one_spec.rb +73 -0
  76. data/spec/public/associations/one_to_many_spec.rb +77 -0
  77. data/spec/public/associations/one_to_one_spec.rb +156 -0
  78. data/spec/public/collection_spec.rb +65 -0
  79. data/spec/public/migrations_spec.rb +359 -0
  80. data/spec/public/model/relationship_spec.rb +924 -0
  81. data/spec/public/model_spec.rb +159 -0
  82. data/spec/public/property_spec.rb +829 -0
  83. data/spec/public/resource_spec.rb +71 -0
  84. data/spec/public/sel_spec.rb +44 -0
  85. data/spec/public/setup_spec.rb +145 -0
  86. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  87. data/spec/public/shared/collection_shared_spec.rb +1670 -0
  88. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  89. data/spec/public/shared/resource_shared_spec.rb +924 -0
  90. data/spec/public/shared/sel_shared_spec.rb +112 -0
  91. data/spec/public/transaction_spec.rb +129 -0
  92. data/spec/public/types/discriminator_spec.rb +130 -0
  93. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  94. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  95. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  96. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  97. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  99. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  100. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  101. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  102. data/spec/semipublic/associations_spec.rb +177 -0
  103. data/spec/semipublic/collection_spec.rb +142 -0
  104. data/spec/semipublic/property_spec.rb +61 -0
  105. data/spec/semipublic/query/conditions_spec.rb +528 -0
  106. data/spec/semipublic/query/path_spec.rb +443 -0
  107. data/spec/semipublic/query_spec.rb +2626 -0
  108. data/spec/semipublic/resource_spec.rb +47 -0
  109. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  110. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  111. data/spec/spec.opts +3 -1
  112. data/spec/spec_helper.rb +80 -57
  113. data/tasks/ci.rb +19 -31
  114. data/tasks/dm.rb +43 -48
  115. data/tasks/doc.rb +8 -11
  116. data/tasks/gemspec.rb +5 -5
  117. data/tasks/hoe.rb +15 -16
  118. data/tasks/install.rb +8 -10
  119. metadata +74 -111
  120. data/lib/dm-core/associations.rb +0 -207
  121. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  122. data/lib/dm-core/auto_migrations.rb +0 -105
  123. data/lib/dm-core/dependency_queue.rb +0 -32
  124. data/lib/dm-core/hook.rb +0 -11
  125. data/lib/dm-core/is.rb +0 -16
  126. data/lib/dm-core/logger.rb +0 -232
  127. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  128. data/lib/dm-core/migrator.rb +0 -29
  129. data/lib/dm-core/scope.rb +0 -58
  130. data/lib/dm-core/support.rb +0 -7
  131. data/lib/dm-core/support/array.rb +0 -13
  132. data/lib/dm-core/support/assertions.rb +0 -8
  133. data/lib/dm-core/support/errors.rb +0 -23
  134. data/lib/dm-core/support/kernel.rb +0 -11
  135. data/lib/dm-core/support/symbol.rb +0 -41
  136. data/lib/dm-core/type_map.rb +0 -80
  137. data/lib/dm-core/types.rb +0 -19
  138. data/script/all +0 -4
  139. data/spec/integration/association_spec.rb +0 -1382
  140. data/spec/integration/association_through_spec.rb +0 -203
  141. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  142. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  143. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  144. data/spec/integration/auto_migrations_spec.rb +0 -413
  145. data/spec/integration/collection_spec.rb +0 -1073
  146. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  147. data/spec/integration/dependency_queue_spec.rb +0 -46
  148. data/spec/integration/model_spec.rb +0 -197
  149. data/spec/integration/mysql_adapter_spec.rb +0 -85
  150. data/spec/integration/postgres_adapter_spec.rb +0 -731
  151. data/spec/integration/property_spec.rb +0 -253
  152. data/spec/integration/query_spec.rb +0 -514
  153. data/spec/integration/repository_spec.rb +0 -61
  154. data/spec/integration/resource_spec.rb +0 -513
  155. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  156. data/spec/integration/sti_spec.rb +0 -273
  157. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  158. data/spec/integration/transaction_spec.rb +0 -75
  159. data/spec/integration/type_spec.rb +0 -275
  160. data/spec/lib/logging_helper.rb +0 -18
  161. data/spec/lib/mock_adapter.rb +0 -27
  162. data/spec/lib/model_loader.rb +0 -100
  163. data/spec/lib/publicize_methods.rb +0 -28
  164. data/spec/models/content.rb +0 -16
  165. data/spec/models/vehicles.rb +0 -34
  166. data/spec/models/zoo.rb +0 -48
  167. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  168. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  169. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  170. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  171. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  172. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  173. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  174. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  175. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  176. data/spec/unit/associations/relationship_spec.rb +0 -71
  177. data/spec/unit/associations_spec.rb +0 -242
  178. data/spec/unit/auto_migrations_spec.rb +0 -111
  179. data/spec/unit/collection_spec.rb +0 -182
  180. data/spec/unit/data_mapper_spec.rb +0 -35
  181. data/spec/unit/identity_map_spec.rb +0 -126
  182. data/spec/unit/is_spec.rb +0 -80
  183. data/spec/unit/migrator_spec.rb +0 -33
  184. data/spec/unit/model_spec.rb +0 -321
  185. data/spec/unit/naming_conventions_spec.rb +0 -36
  186. data/spec/unit/property_set_spec.rb +0 -90
  187. data/spec/unit/property_spec.rb +0 -753
  188. data/spec/unit/query_spec.rb +0 -571
  189. data/spec/unit/repository_spec.rb +0 -93
  190. data/spec/unit/resource_spec.rb +0 -649
  191. data/spec/unit/scope_spec.rb +0 -142
  192. data/spec/unit/transaction_spec.rb +0 -493
  193. data/spec/unit/type_map_spec.rb +0 -114
  194. data/spec/unit/type_spec.rb +0 -119
@@ -1,169 +1,212 @@
1
1
  module DataMapper
2
- class PropertySet
3
- include Assertions
4
- include Enumerable
5
-
2
+ # Set of Property objects, used to associate
3
+ # queries with set of fields it performed over,
4
+ # to represent composite keys (esp. for associations)
5
+ # and so on.
6
+ class PropertySet < Array
7
+ extend Deprecate
8
+
9
+ deprecate :has_property?, :named?
10
+ deprecate :slice, :values_at
11
+ deprecate :add, :<<
12
+
13
+ # TODO: document
14
+ # @api semipublic
6
15
  def [](name)
7
- property_for(name) || raise(ArgumentError, "Unknown property '#{name}'", caller)
16
+ @properties[name]
8
17
  end
9
18
 
19
+ alias super_slice []=
20
+
21
+ # TODO: document
22
+ # @api semipublic
10
23
  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
24
+ if named?(name)
25
+ add_property(property)
26
+ super_slice(index(property), property)
15
27
  else
16
- add(property)
28
+ self << property
17
29
  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
30
  end
36
31
 
37
- def add(*properties)
38
- @key, @defaults = nil
39
- @entries.push(*properties)
40
- properties.each { |property| property.hash }
41
- self
32
+ # TODO: document
33
+ # @api semipublic
34
+ def named?(name)
35
+ @properties.key?(name)
42
36
  end
43
37
 
44
- alias << add
45
-
46
- def length
47
- @entries.length
38
+ # TODO: document
39
+ # @api semipublic
40
+ def values_at(*names)
41
+ @properties.values_at(*names)
48
42
  end
49
43
 
50
- def empty?
51
- @entries.empty?
44
+ # TODO: document
45
+ # @api semipublic
46
+ def <<(property)
47
+ if named?(property.name)
48
+ add_property(property)
49
+ super_slice(index(property), property)
50
+ else
51
+ add_property(property)
52
+ super
53
+ end
52
54
  end
53
55
 
54
- def each
55
- @entries.each { |property| yield property }
56
- self
56
+ # TODO: document
57
+ # @api semipublic
58
+ def include?(property)
59
+ named?(property.name)
57
60
  end
58
61
 
62
+ # TODO: make PropertySet#reject return a PropertySet instance
63
+ # TODO: document
64
+ # @api semipublic
59
65
  def defaults
60
- @defaults ||= reject { |property| property.lazy? }
66
+ @defaults ||= self.class.new(key | [ discriminator ].compact | reject { |property| property.lazy? }).freeze
61
67
  end
62
68
 
69
+ # TODO: document
70
+ # @api semipublic
63
71
  def key
64
- @key ||= select { |property| property.key? }
72
+ @key ||= self.class.new(select { |property| property.key? }).freeze
73
+ end
74
+
75
+ # TODO: document
76
+ # @api semipublic
77
+ def discriminator
78
+ @discriminator ||= detect { |property| property.type == Types::Discriminator }
65
79
  end
66
80
 
81
+ # TODO: document
82
+ # @api semipublic
67
83
  def indexes
68
84
  index_hash = {}
69
- repository_name = repository.name
70
- each { |property| parse_index(property.index, property.field(repository_name), index_hash) }
85
+ each { |property| parse_index(property.index, property.field, index_hash) }
71
86
  index_hash
72
87
  end
73
88
 
89
+ # TODO: document
90
+ # @api semipublic
74
91
  def unique_indexes
75
92
  index_hash = {}
76
- repository_name = repository.name
77
- each { |property| parse_index(property.unique_index, property.field(repository_name), index_hash) }
93
+ each { |property| parse_index(property.unique_index, property.field, index_hash) }
78
94
  index_hash
79
95
  end
80
96
 
97
+ # TODO: document
98
+ # @api semipublic
81
99
  def get(resource)
82
100
  map { |property| property.get(resource) }
83
101
  end
84
102
 
103
+ # TODO: document
104
+ # @api semipublic
105
+ def get!(resource)
106
+ map { |property| property.get!(resource) }
107
+ end
108
+
109
+ # TODO: document
110
+ # @api semipublic
85
111
  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
112
+ zip(values) { |property, value| property.set(resource, value) }
113
+ end
89
114
 
90
- each_with_index { |property,i| property.set(resource, values.nil? ? nil : values[i]) }
115
+ # TODO: document
116
+ # @api semipublic
117
+ def set!(resource, values)
118
+ zip(values) { |property, value| property.set!(resource, value) }
91
119
  end
92
120
 
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
121
+ # TODO: document
122
+ # @api semipublic
123
+ def loaded?(resource)
124
+ all? { |property| property.loaded?(resource) }
99
125
  end
100
126
 
101
- def lazy_context(name)
102
- lazy_contexts[name] ||= []
127
+ # TODO: document
128
+ # @api semipublic
129
+ def typecast(values)
130
+ zip(values.nil? ? [] : values).map { |property, value| property.typecast(value) }
103
131
  end
104
132
 
105
- def lazy_load_context(names)
106
- if names.kind_of?(Array) && names.empty?
107
- raise ArgumentError, '+names+ cannot be empty', caller
133
+ # TODO: document
134
+ # @api private
135
+ def property_contexts(property_name)
136
+ contexts = []
137
+ lazy_contexts.each do |context, property_names|
138
+ contexts << context if property_names.include?(property_name)
108
139
  end
140
+ contexts
141
+ end
109
142
 
110
- result = []
143
+ # TODO: document
144
+ # @api private
145
+ def lazy_context(context)
146
+ lazy_contexts[context] ||= []
147
+ end
111
148
 
112
- Array(names).each do |name|
113
- contexts = property_contexts(name)
114
- if contexts.empty?
115
- result << name # not lazy
149
+ # TODO: document
150
+ # @api private
151
+ def in_context(property_names)
152
+ property_names_in_context = property_names.map do |property_name|
153
+ if (contexts = property_contexts(property_name)).any?
154
+ lazy_contexts.values_at(*contexts)
116
155
  else
117
- result |= lazy_contexts.values_at(*contexts).flatten.uniq
156
+ property_name # not lazy
118
157
  end
119
158
  end
120
- result
121
- end
122
159
 
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(',') + '}>'
160
+ values_at(*property_names_in_context.flatten.uniq)
129
161
  end
130
162
 
131
163
  private
132
164
 
133
- def initialize(properties = [])
134
- assert_kind_of 'properties', properties, Enumerable
165
+ # TODO: document
166
+ # @api semipublic
167
+ def initialize(*)
168
+ super
169
+ @properties = map { |property| [ property.name, property ] }.to_mash
170
+ end
171
+
172
+ # TODO: document
173
+ # @api private
174
+ def initialize_copy(*)
175
+ super
176
+ @properties = @properties.dup
177
+ end
135
178
 
136
- @entries = properties
137
- @property_for = {}
179
+ # TODO: document
180
+ # @api private
181
+ def add_property(property)
182
+ clear_cache
183
+ @properties[property.name] = property
138
184
  end
139
185
 
140
- def initialize_copy(orig)
141
- @key, @defaults = nil
142
- @entries = orig.entries.dup
143
- @property_for = {}
186
+ # TODO: document
187
+ # @api private
188
+ def clear_cache
189
+ @defaults, @key, @discriminator = nil
144
190
  end
145
191
 
192
+ # TODO: document
193
+ # @api private
146
194
  def lazy_contexts
147
195
  @lazy_contexts ||= {}
148
196
  end
149
197
 
198
+ # TODO: document
199
+ # @api private
150
200
  def parse_index(index, property, index_hash)
151
201
  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) }
202
+ when true
203
+ index_hash[property] = [ property ]
204
+ when Symbol
205
+ index_hash[index] ||= []
206
+ index_hash[index] << property
207
+ when Array
208
+ index.each { |idx| parse_index(idx, property, index_hash) }
157
209
  end
158
210
  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
211
  end # class PropertySet
169
212
  end # module DataMapper
@@ -1,139 +1,563 @@
1
+ # TODO: break this up into classes for each primary option, eg:
2
+ #
3
+ # - DataMapper::Query::Fields
4
+ # - DataMapper::Query::Links
5
+ # - DataMapper::Query::Conditions
6
+ # - DataMapper::Query::Offset
7
+ # - DataMapper::Query::Limit
8
+ # - DataMapper::Query::Order
9
+ #
10
+ # TODO: move assertions, validations, transformations, and equality
11
+ # checking into each class and clean up Query
12
+ #
13
+ # TODO: add a way to "register" these classes with the Query object
14
+ # so that new reserved options can be added in the future. Each
15
+ # class will need to implement a "slug" method or something similar
16
+ # so that their option namespace can be reserved.
17
+
18
+ # TODO: move condition transformations into a Query::Conditions
19
+ # helper class that knows how to transform the primitives, and
20
+ # calls #comparison_for(repository, model) on objects (or some
21
+ # other convention that we establish)
22
+
1
23
  module DataMapper
24
+
25
+ # Query class represents a query which will be run against the data-store.
26
+ # Generally Query objects can be found inside Collection objects.
27
+ #
2
28
  class Query
3
- include Assertions
29
+ include Extlib::Assertions
30
+
31
+ OPTIONS = [ :fields, :links, :conditions, :offset, :limit, :order, :unique, :add_reversed, :reload ].to_set.freeze
32
+
33
+ # Extract conditions to match a Resource or Collection
34
+ #
35
+ # @param [Array, Collection, Resource] source
36
+ # the source to extract the values from
37
+ # @param [ProperySet] source_key
38
+ # the key to extract the value from the resource
39
+ # @param [ProperySet] target_key
40
+ # the key to match the resource with
41
+ #
42
+ # @return [AbstractComparison, AbstractOperation]
43
+ # the conditions to match the resources with
44
+ #
45
+ # @api private
46
+ def self.target_conditions(source, source_key, target_key)
47
+ source_values = []
48
+
49
+ if source.nil?
50
+ source_values << [ nil ] * target_key.size
51
+ else
52
+ Array(source).each do |resource|
53
+ next unless source_key.loaded?(resource)
54
+ source_values << source_key.get!(resource)
55
+ end
56
+ end
57
+
58
+ source_values.uniq!
59
+
60
+ if target_key.size == 1
61
+ target_key = target_key.first
62
+ source_values.flatten!
63
+
64
+ if source_values.size == 1
65
+ Conditions::EqualToComparison.new(target_key, source_values.first)
66
+ else
67
+ Conditions::InclusionComparison.new(target_key, source_values)
68
+ end
69
+ else
70
+ or_operation = Conditions::OrOperation.new
71
+
72
+ source_values.each do |source_value|
73
+ and_operation = Conditions::AndOperation.new
74
+
75
+ target_key.zip(source_value) do |property, value|
76
+ and_operation << Conditions::EqualToComparison.new(property, value)
77
+ end
78
+
79
+ or_operation << and_operation
80
+ end
81
+
82
+ or_operation
83
+ end
84
+ end
85
+
86
+ # Returns the repository query should be
87
+ # executed in
88
+ #
89
+ # Set in cases like the following:
90
+ #
91
+ # @example
92
+ #
93
+ # Document.all(:repository => :medline)
94
+ #
95
+ #
96
+ # @return [Repository]
97
+ # the Repository to retrieve results from
98
+ #
99
+ # @api semipublic
100
+ attr_reader :repository
101
+
102
+ # Returns model (class) that is used
103
+ # to instantiate objects from query result
104
+ # returned by adapter
105
+ #
106
+ # @return [Model]
107
+ # the Model to retrieve results from
108
+ #
109
+ # @api semipublic
110
+ attr_reader :model
111
+
112
+ # Returns the fields
113
+ #
114
+ # Set in cases like the following:
115
+ #
116
+ # @example
117
+ #
118
+ # Document.all(:fields => [:title, :vernacular_title, :abstract])
119
+ #
120
+ # @return [PropertySet]
121
+ # the properties in the Model that will be retrieved
122
+ #
123
+ # @api semipublic
124
+ attr_reader :fields
125
+
126
+ # Returns the links (associations) query fetches
127
+ #
128
+ # @return [Array<DataMapper::Associations::Relationship>]
129
+ # the relationships that will be used to scope the results
130
+ #
131
+ # @api private
132
+ attr_reader :links
133
+
134
+ # Returns the conditions of the query
135
+ #
136
+ # In the following example:
137
+ #
138
+ # @example
139
+ #
140
+ # Team.all(:wins.gt => 30, :conference => 'East')
141
+ #
142
+ # Conditions are "greater than" operator for "wins"
143
+ # field and exact match operator for "conference".
144
+ #
145
+ # @return [Array]
146
+ # the conditions that will be used to scope the results
147
+ #
148
+ # @api semipublic
149
+ attr_reader :conditions
150
+
151
+ # Returns the offset query uses
152
+ #
153
+ # Set in cases like the following:
154
+ #
155
+ # @example
156
+ #
157
+ # Document.all(:offset => page.offset)
158
+ #
159
+ # @return [Integer]
160
+ # the offset of the results
161
+ #
162
+ # @api semipublic
163
+ attr_reader :offset
164
+
165
+ # Returns the limit query uses
166
+ #
167
+ # Set in cases like the following:
168
+ #
169
+ # @example
170
+ #
171
+ # Document.all(:limit => 10)
172
+ #
173
+ # @return [Integer, NilClass]
174
+ # the maximum number of results
175
+ #
176
+ # @api semipublic
177
+ attr_reader :limit
4
178
 
5
- OPTIONS = [
6
- :reload, :offset, :limit, :order, :add_reversed, :fields, :links, :includes, :conditions, :unique
7
- ]
179
+ # Returns the order
180
+ #
181
+ # Set in cases like the following:
182
+ #
183
+ # @example
184
+ #
185
+ # Document.all(:order => [:created_at.desc, :length.desc])
186
+ #
187
+ # query order is a set of two ordering rules, descending on
188
+ # "created_at" field and descending again on "length" field
189
+ #
190
+ # @return [Array]
191
+ # the order of results
192
+ #
193
+ # @api semipublic
194
+ attr_reader :order
8
195
 
9
- attr_reader :repository, :model, *OPTIONS - [ :reload, :unique ]
10
- attr_writer :add_reversed
11
- alias add_reversed? add_reversed
196
+ # Returns the original options
197
+ #
198
+ # @return [Hash]
199
+ # the original options
200
+ #
201
+ # @api private
202
+ attr_reader :options
203
+
204
+ # Indicates if each result should be returned in reverse order
205
+ #
206
+ # Set in cases like the following:
207
+ #
208
+ # @example
209
+ #
210
+ # Document.all(:limit => 5).reverse
211
+ #
212
+ # Note that :add_reversed option may be used in conditions directly,
213
+ # but this is rarely the case
214
+ #
215
+ # @return [Boolean]
216
+ # true if the results should be reversed, false if not
217
+ #
218
+ # @api private
219
+ def add_reversed?
220
+ @add_reversed
221
+ end
12
222
 
223
+ # Indicates if the Query results should replace the results in the Identity Map
224
+ #
225
+ # TODO: needs example
226
+ #
227
+ # @return [Boolean]
228
+ # true if the results should be reloaded, false if not
229
+ #
230
+ # @api semipublic
13
231
  def reload?
14
232
  @reload
15
233
  end
16
234
 
235
+ # Indicates if the Query results should be unique
236
+ #
237
+ # TODO: needs example
238
+ #
239
+ # @return [Boolean]
240
+ # true if the results should be unique, false if not
241
+ #
242
+ # @api semipublic
17
243
  def unique?
18
244
  @unique
19
245
  end
20
246
 
247
+ # Indicates if the Query has raw conditions
248
+ #
249
+ # @return [Boolean]
250
+ # true if the query has raw conditions, false if not
251
+ #
252
+ # @api semipublic
253
+ def raw?
254
+ @raw
255
+ end
256
+
257
+ # Indicates if the Query is valid
258
+ #
259
+ # @return [Boolean]
260
+ # true if the query is valid
261
+ #
262
+ # @api semipublic
263
+ def valid?
264
+ conditions.valid?
265
+ end
266
+
267
+ # Returns a new Query with a reversed order
268
+ #
269
+ # @example
270
+ #
271
+ # Document.all(:limit => 5).reverse
272
+ #
273
+ # Will execute a single query with correct order
274
+ #
275
+ # @return [Query]
276
+ # new Query with reversed order
277
+ #
278
+ # @api semipublic
21
279
  def reverse
22
280
  dup.reverse!
23
281
  end
24
282
 
283
+ # Reverses the sort order of the Query
284
+ #
285
+ # @example
286
+ #
287
+ # Document.all(:limit => 5).reverse
288
+ #
289
+ # Will execute a single query with original order
290
+ # and then reverse collection in the Ruby space
291
+ #
292
+ # @return [Query]
293
+ # self
294
+ #
295
+ # @api semipublic
25
296
  def reverse!
26
297
  # reverse the sort order
27
- update(:order => self.order.map { |o| o.reverse })
298
+ @order.map! { |direction| direction.reverse! }
299
+
300
+ # copy the order to the options
301
+ @options = @options.merge(:order => @order.map { |direction| direction.dup }).freeze
28
302
 
29
303
  self
30
304
  end
31
305
 
306
+ # Updates the Query with another Query or conditions
307
+ #
308
+ # Pretty unrealistic example:
309
+ #
310
+ # @example
311
+ #
312
+ # Journal.all(:limit => 2).query.limit # => 2
313
+ # Journal.all(:limit => 2).query.update(:limit => 3).limit # => 3
314
+ #
315
+ # @param [Query, Hash] other
316
+ # other Query or conditions
317
+ #
318
+ # @return [Query]
319
+ # self
320
+ #
321
+ # @api semipublic
32
322
  def update(other)
33
323
  assert_kind_of 'other', other, self.class, Hash
34
324
 
35
- assert_valid_other(other)
325
+ other_options = if other.kind_of? self.class
326
+ if self.eql?(other)
327
+ return self
328
+ end
329
+ assert_valid_other(other)
330
+ other.options
331
+ else
332
+ other
333
+ end
36
334
 
37
- if other.kind_of?(Hash)
38
- return self if other.empty?
39
- other = self.class.new(@repository, model, other)
335
+ unless other_options.empty?
336
+ options = @options.merge(other_options)
337
+ if @options[:conditions] and other_options[:conditions]
338
+ options[:conditions] = @options[:conditions].dup << other_options[:conditions]
339
+ end
340
+ initialize(repository, model, options)
40
341
  end
41
342
 
42
- return self if self == other
343
+ self
344
+ end
345
+
346
+ # Similar to Query#update, but acts on a duplicate.
347
+ #
348
+ # @param [Query, Hash] other
349
+ # other query to merge with
350
+ #
351
+ # @return [Query]
352
+ # updated duplicate of original query
353
+ #
354
+ # @api semipublic
355
+ def merge(other)
356
+ dup.update(other)
357
+ end
358
+
359
+ # Builds and returns new query that merges
360
+ # original with one given, and slices the result
361
+ # with respect to :limit and :offset options
362
+ #
363
+ # This method is used by Collection to
364
+ # concatenate options from multiple chained
365
+ # calls in cases like the following:
366
+ #
367
+ # @example
368
+ #
369
+ # author.books.all(:year => 2009).all(:published => false)
370
+ #
371
+ # @api semipublic
372
+ def relative(options)
373
+ assert_kind_of 'options', options, Hash
374
+
375
+ options = options.dup
43
376
 
44
- # TODO: update this so if "other" had a value explicitly set
45
- # overwrite the attributes in self
377
+ repository = options.delete(:repository) || self.repository
46
378
 
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 == []
379
+ if repository.kind_of?(Symbol)
380
+ repository = DataMapper.repository(repository)
381
+ end
57
382
 
58
- update_conditions(other)
383
+ if options.key?(:offset) && (options.key?(:limit) || self.limit)
384
+ offset = options.delete(:offset)
385
+ limit = options.delete(:limit) || self.limit - offset
59
386
 
60
- self
387
+ self.class.new(repository, model, @options.merge(options)).slice!(offset, limit)
388
+ else
389
+ self.class.new(repository, model, @options.merge(options))
390
+ end
61
391
  end
62
392
 
63
- def merge(other)
64
- dup.update(other)
393
+ # Takes an Enumerable of records, and destructively filters it.
394
+ # First finds all matching conditions, then sorts it,
395
+ # then does offset & limit
396
+ #
397
+ # @param [Enumerable] records
398
+ # The set of records to be filtered
399
+ #
400
+ # @return [Enumerable]
401
+ # Whats left of the given array after the filtering
402
+ #
403
+ # @api semipublic
404
+ def filter_records(records)
405
+ records = records.uniq if unique?
406
+ records = match_records(records)
407
+ records = sort_records(records)
408
+ records = limit_records(records)
409
+ records
65
410
  end
66
411
 
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
412
+ # Filter a set of records by the conditions
413
+ #
414
+ # @param [Enumerable] records
415
+ # The set of records to be filtered
416
+ #
417
+ # @return [Enumerable]
418
+ # Whats left of the given array after the matching
419
+ #
420
+ # @api semipublic
421
+ def match_records(records)
422
+ records.select do |record|
423
+ conditions.matches?(record)
424
+ end
425
+ end
426
+
427
+ # Sorts a list of Records by the order
428
+ #
429
+ # @param [Enumerable] records
430
+ # A list of Resources to sort
431
+ #
432
+ # @return [Enumerable]
433
+ # The sorted records
434
+ #
435
+ # @api semipublic
436
+ def sort_records(records)
437
+ sort_order = order.map { |direction| [ direction.target, direction.operator == :asc ] }
438
+
439
+ records.sort_by do |record|
440
+ sort_order.map do |(property, ascending)|
441
+ Sort.new(record_value(record, property), ascending)
97
442
  end
98
443
  end
99
- bind_values
100
444
  end
101
445
 
102
- def inheritance_property
103
- fields.detect { |property| property.type == DataMapper::Types::Discriminator }
446
+ # Limits a set of records by the offset and/or limit
447
+ #
448
+ # @param [Enumerable] records
449
+ # A list of Recrods to sort
450
+ #
451
+ # @return [Enumerable]
452
+ # The offset & limited records
453
+ #
454
+ # @api semipublic
455
+ def limit_records(records)
456
+ size = records.size
457
+
458
+ if offset > size - 1
459
+ []
460
+ elsif (limit && limit != size) || offset > 0
461
+ records[offset, limit || size] || []
462
+ else
463
+ records.dup
464
+ end
104
465
  end
105
466
 
106
- def inheritance_property_index
107
- fields.index(inheritance_property)
467
+ # Compares another Query for equivalency
468
+ #
469
+ # @param [Query] other
470
+ # the other Query to compare with
471
+ #
472
+ # @return [Boolean]
473
+ # true if they are equivalent, false if not
474
+ #
475
+ # @api semipublic
476
+ def ==(other)
477
+ if equal?(other)
478
+ return true
479
+ end
480
+
481
+ unless [ :repository, :model, :fields, :links, :conditions, :order, :offset, :limit, :reload?, :unique?, :add_reversed? ].all? { |method| other.respond_to?(method) }
482
+ return false
483
+ end
484
+
485
+ cmp?(other, :==)
108
486
  end
109
487
 
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
488
+ # Compares another Query for equality
489
+ #
490
+ # @param [Query] other
491
+ # the other Query to compare with
492
+ #
493
+ # @return [Boolean]
494
+ # true if they are equal, false if not
495
+ #
496
+ # @api semipublic
497
+ def eql?(other)
498
+ if equal?(other)
499
+ return true
500
+ end
501
+
502
+ unless instance_of?(other.class)
503
+ return false
114
504
  end
505
+
506
+ cmp?(other, :eql?)
115
507
  end
116
508
 
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>
509
+ # Slices collection by adding limit and offset to the
510
+ # query, so a single query is executed
511
+ #
512
+ # @example
513
+ #
514
+ # Journal.all(:limit => 10).slice(3, 5)
120
515
  #
121
- def merge_subquery(operator, property, value)
122
- assert_kind_of 'value', value, self.class
516
+ # will execute query with the following limit and offset
517
+ # (when repository uses DataObjects adapter, and thus
518
+ # queries use SQL):
519
+ #
520
+ # LIMIT 5 OFFSET 3
521
+ #
522
+ # @api semipublic
523
+ def slice(*args)
524
+ dup.slice!(*args)
525
+ end
123
526
 
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
527
+ alias [] slice
528
+
529
+ # Slices collection by adding limit and offset to the
530
+ # query, so a single query is executed
531
+ #
532
+ # @example
533
+ #
534
+ # Journal.all(:limit => 10).slice!(3, 5)
535
+ #
536
+ # will execute query with the following limit
537
+ # (when repository uses DataObjects adapter, and thus
538
+ # queries use SQL):
539
+ #
540
+ # LIMIT 10
541
+ #
542
+ # and then takes a slice of collection in the Ruby space
543
+ #
544
+ # @api semipublic
545
+ def slice!(*args)
546
+ offset, limit = extract_slice_arguments(*args)
547
+
548
+ if self.limit || self.offset > 0
549
+ offset, limit = get_relative_position(offset, limit)
133
550
  end
134
- @conditions = new_conditions
551
+
552
+ update(:offset => offset, :limit => limit)
135
553
  end
136
554
 
555
+ # Returns detailed human readable
556
+ # string representation of the query
557
+ #
558
+ # @return [String] detailed string representation of the query
559
+ #
560
+ # @api semipublic
137
561
  def inspect
138
562
  attrs = [
139
563
  [ :repository, repository.name ],
@@ -148,529 +572,659 @@ module DataMapper
148
572
  [ :unique, unique? ],
149
573
  ]
150
574
 
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)
575
+ "#<#{self.class.name} #{attrs.map { |key, value| "@#{key}=#{value.inspect}" }.join(' ')}>"
189
576
  end
190
577
 
191
- # TODO: add docs
578
+ # Get the properties used in the conditions
579
+ #
580
+ # @return [Set<Property>]
581
+ # Set of properties used in the conditions
582
+ #
192
583
  # @api private
193
- def _dump(*)
194
- Marshal.dump([ repository, model, to_hash ])
195
- end
584
+ def condition_properties
585
+ properties = Set.new
196
586
 
197
- # TODO: add docs
198
- # @api private
199
- def self._load(marshalled)
200
- new(*Marshal.load(marshalled))
587
+ each_comparison do |comparison|
588
+ properties << comparison.subject if comparison.subject.kind_of?(Property)
589
+ end
590
+
591
+ properties
201
592
  end
202
593
 
203
594
  private
204
595
 
596
+ # Initializes a Query instance
597
+ #
598
+ # @example
599
+ #
600
+ # JournalIssue.all(:repository => :medline, :created_on.gte => Date.today - 7)
601
+ #
602
+ # initialized a query with repository defined with name :medline,
603
+ # model JournalIssue and options { :created_on.gte => Date.today - 7 }
604
+ #
605
+ # @param [Repository] repository
606
+ # the Repository to retrieve results from
607
+ # @param [Model] model
608
+ # the Model to retrieve results from
609
+ # @param [Hash] options
610
+ # the conditions and scope
611
+ #
612
+ # @api semipublic
205
613
  def initialize(repository, model, options = {})
206
614
  assert_kind_of 'repository', repository, Repository
207
615
  assert_kind_of 'model', model, Model
208
- assert_kind_of 'options', options, Hash
209
616
 
210
- options.each_pair { |k,v| options[k] = v.call if v.is_a? Proc } if options.is_a? Hash
617
+ @repository = repository
618
+ @model = model
619
+ @options = options.dup.freeze
211
620
 
212
- assert_valid_options(options)
621
+ repository_name = repository.name
213
622
 
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)
623
+ @properties = @model.properties(repository_name)
624
+ @relationships = @model.relationships(repository_name)
625
+
626
+ assert_valid_options(@options)
627
+
628
+ @fields = @options.fetch :fields, @properties.defaults
629
+ @links = @options.fetch :links, []
630
+ @conditions = Conditions::Operation.new(:and) # AND all the conditions together
631
+ @offset = @options.fetch :offset, 0
632
+ @limit = @options.fetch :limit, nil
633
+ @order = @options.fetch :order, @model.default_order(repository_name)
634
+ @unique = @options.fetch :unique, false
635
+ @add_reversed = @options.fetch :add_reversed, false
636
+ @reload = @options.fetch :reload, false
637
+ @raw = false
638
+
639
+ @links = @links.dup
243
640
 
244
641
  # treat all non-options as conditions
245
- (options.keys - OPTIONS).each do |k|
246
- append_condition(k, options[k])
247
- end
642
+ @options.except(*OPTIONS).each { |kv| append_condition(*kv) }
248
643
 
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
644
+ # parse @options[:conditions] differently
645
+ case conditions = @options[:conditions]
646
+ when Conditions::AbstractOperation, Conditions::AbstractComparison
647
+ @conditions << conditions
648
+
649
+ when Hash
650
+ conditions.each { |kv| append_condition(*kv) }
651
+
652
+ when Array
653
+ statement, *bind_values = *conditions
654
+ @conditions << [ statement, bind_values ]
655
+ @raw = true
263
656
  end
657
+
658
+ normalize_order
659
+ normalize_fields
660
+ normalize_links
264
661
  end
265
662
 
663
+ # Copying contructor, called for Query#dup
664
+ #
665
+ # @api semipublic
266
666
  def initialize_copy(original)
267
- # deep-copy the condition tuples when copying the object
268
- @conditions = original.conditions.map { |tuple| tuple.dup }
667
+ initialize(original.repository, original.model, original.options)
269
668
  end
270
669
 
271
- # validate the options
670
+ # Validate the options
671
+ #
672
+ # @param [#each] options
673
+ # the options to validate
674
+ #
675
+ # @raise [ArgumentError]
676
+ # if any pairs in +options+ are invalid options
677
+ #
678
+ # @api private
272
679
  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
680
+ assert_kind_of 'options', options, Hash
681
+
682
+ options.each do |attribute, value|
683
+ case attribute
684
+ when :fields then assert_valid_fields(value, options[:unique])
685
+ when :links then assert_valid_links(value)
686
+ when :conditions then assert_valid_conditions(value)
687
+ when :offset then assert_valid_offset(value, options[:limit])
688
+ when :limit then assert_valid_limit(value)
689
+ when :order then assert_valid_order(value, options[:fields])
690
+ when :unique, :add_reversed, :reload then assert_valid_boolean("options[:#{attribute}]", value)
691
+ else
692
+ assert_valid_conditions(attribute => value)
693
+ end
694
+ end
695
+ end
282
696
 
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
697
+ # Verifies that value of :fields option
698
+ # refers to existing properties
699
+ #
700
+ # @api private
701
+ def assert_valid_fields(fields, unique)
702
+ assert_kind_of 'options[:fields]', fields, Array
703
+
704
+ if fields.empty? && unique == false
705
+ raise ArgumentError, '+options[:fields]+ should not be empty if +options[:unique]+ is false'
706
+ end
291
707
 
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)
708
+ fields.each do |field|
709
+ case field
710
+ when Symbol, String
711
+ unless @properties.named?(field)
712
+ raise ArgumentError, "+options[:fields]+ entry #{field.inspect} does not map to a property in #{model}"
307
713
  end
308
- end
309
714
 
310
- # validates the :conditions option
311
- elsif :conditions == attribute
312
- assert_kind_of 'options[:conditions]', value, Hash, Array
715
+ when Property
716
+ unless @properties.include?(field)
717
+ raise ArgumentError, "+options[:field]+ entry #{field.name.inspect} does not map to a property in #{model}"
718
+ end
313
719
 
314
- if value.empty?
315
- raise ArgumentError, '+options[:conditions]+ cannot be empty', caller(2)
316
- end
720
+ else
721
+ raise ArgumentError, "+options[:fields]+ entry #{field.inspect} of an unsupported object #{field.class}"
317
722
  end
318
723
  end
319
724
  end
320
725
 
321
- # validate other DM::Query or Hash object
322
- def assert_valid_other(other)
323
- return unless other.kind_of?(self.class)
726
+ # Verifies that value of :links option
727
+ # refers to existing associations
728
+ #
729
+ # @api private
730
+ def assert_valid_links(links)
731
+ assert_kind_of 'options[:links]', links, Array
324
732
 
325
- unless other.repository == repository
326
- raise ArgumentError, "+other+ #{self.class} must be for the #{repository.name} repository, not #{other.repository.name}", caller(2)
733
+ if links.empty?
734
+ raise ArgumentError, '+options[:links]+ should not be empty'
327
735
  end
328
736
 
329
- unless other.model == model
330
- raise ArgumentError, "+other+ #{self.class} must be for the #{model.name} model, not #{other.model.name}", caller(2)
737
+ links.each do |link|
738
+ case link
739
+ when Symbol, String
740
+ unless @relationships.key?(link.to_sym)
741
+ raise ArgumentError, "+options[:links]+ entry #{link.inspect} does not map to a relationship in #{model}"
742
+ end
743
+
744
+ when Associations::Relationship
745
+ # TODO: figure out how to validate links from other models
746
+ #unless @relationships.value?(link)
747
+ # raise ArgumentError, "+options[:links]+ entry #{link.name.inspect} does not map to a relationship in #{model}"
748
+ #end
749
+
750
+ else
751
+ raise ArgumentError, "+options[:links]+ entry #{link.inspect} of an unsupported object #{link.class}"
752
+ end
331
753
  end
332
754
  end
333
755
 
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
756
+ # Verifies that value of :conditions option
757
+ # refers to existing properties
758
+ #
759
+ # @api private
760
+ def assert_valid_conditions(conditions)
761
+ assert_kind_of 'options[:conditions]', conditions, Conditions::AbstractOperation, Conditions::AbstractComparison, Hash, Array
762
+
763
+ case conditions
764
+ when Hash
765
+ conditions.each do |subject, bind_value|
766
+ case subject
767
+ when Symbol, String
768
+ unless subject.to_s.include?('.') || @properties.named?(subject) || @relationships.key?(subject)
769
+ raise ArgumentError, "condition #{subject.inspect} does not map to a property or relationship in #{model}"
770
+ end
346
771
 
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
772
+ when Operator
773
+ unless (Conditions::Comparison.slugs | [ :not ]).include?(subject.operator)
774
+ raise ArgumentError, "condition #{subject.inspect} used an invalid operator #{subject.operator}"
775
+ end
355
776
 
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]
777
+ assert_valid_conditions(subject.target => bind_value)
778
+
779
+ if subject.operator == :not && bind_value.kind_of?(Array) && bind_value.empty?
780
+ raise ArgumentError, "Cannot use 'not' operator with a bind value that is an empty Array for #{subject.inspect}"
781
+ end
362
782
 
363
- if property.nil?
364
- raise ArgumentError, "+options[:order]+ entry #{order_by} does not map to a DataMapper::Property", caller(2)
783
+ when Path
784
+ assert_valid_links(subject.relationships)
785
+
786
+ when Associations::Relationship, Property
787
+ # TODO: validate that it belongs to the current model, or to any
788
+ # model in the links
789
+ #unless @properties.include?(subject)
790
+ # raise ArgumentError, "condition #{subject.name.inspect} does not map to a property in #{model}"
791
+ #end
792
+
793
+ else
794
+ raise ArgumentError, "condition #{subject.inspect} of an unsupported object #{subject.class}"
365
795
  end
796
+ end
366
797
 
367
- Direction.new(property)
368
- else
369
- raise ArgumentError, "+options[:order]+ entry #{order_by.inspect} not supported", caller(2)
370
- end
798
+ when Array
799
+ if conditions.empty?
800
+ raise ArgumentError, '+options[:conditions]+ should not be empty'
801
+ end
802
+
803
+ unless conditions.first.kind_of?(String) && !conditions.first.blank?
804
+ raise ArgumentError, '+options[:conditions]+ should have a statement for the first entry'
805
+ end
371
806
  end
372
807
  end
373
808
 
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]
809
+ # Verifies that query offset is non-negative and only used together with limit
810
+ # @api private
811
+ def assert_valid_offset(offset, limit)
812
+ assert_kind_of 'options[:offset]', offset, Integer
390
813
 
391
- if property.nil?
392
- raise ArgumentError, "+options[:fields]+ entry #{field} does not map to a DataMapper::Property", caller(2)
393
- end
814
+ unless offset >= 0
815
+ raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{offset.inspect}"
816
+ end
394
817
 
395
- property
396
- else
397
- raise ArgumentError, "+options[:fields]+ entry #{field.inspect} not supported", caller(2)
398
- end
818
+ if offset > 0 && limit.nil?
819
+ raise ArgumentError, '+options[:offset]+ cannot be greater than 0 if limit is not specified'
399
820
  end
400
821
  end
401
822
 
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
823
+ # Verifies the limit is equal to or greater than 0
824
+ #
825
+ # @raise [ArgumentError]
826
+ # raised if the limit is not an Integer or less than 0
827
+ #
828
+ # @api private
829
+ def assert_valid_limit(limit)
830
+ assert_kind_of 'options[:limit]', limit, Integer
831
+
832
+ unless limit >= 0
833
+ raise ArgumentError, "+options[:limit]+ must be greater than or equal to 0, but was #{limit.inspect}"
834
+ end
835
+ end
836
+
837
+ # Verifies that :order option uses proper operator and refers
838
+ # to existing property
839
+ #
840
+ # @api private
841
+ def assert_valid_order(order, fields)
842
+ return if order.nil?
843
+
844
+ assert_kind_of 'options[:order]', order, Array
845
+
846
+ if order.empty? && fields && fields.any? { |property| !property.kind_of?(Operator) }
847
+ raise ArgumentError, '+options[:order]+ should not be empty if +options[:fields] contains a non-operator'
848
+ end
849
+
850
+ order.each do |order_entry|
851
+ case order_entry
412
852
  when Symbol, String
413
- link = link.to_sym if link.kind_of?(String)
853
+ unless @properties.named?(order_entry)
854
+ raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} does not map to a property in #{model}"
855
+ end
414
856
 
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)
857
+ when Property
858
+ unless @properties.include?(order_entry)
859
+ raise ArgumentError, "+options[:order]+ entry #{order_entry.name.inspect} does not map to a property in #{model}"
417
860
  end
418
861
 
419
- model.relationships(@repository.name)[link]
862
+ when Operator, Direction
863
+ unless order_entry.operator == :asc || order_entry.operator == :desc
864
+ raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} used an invalid operator #{order_entry.operator}"
865
+ end
866
+
867
+ assert_valid_order([ order_entry.target ], fields)
868
+
420
869
  else
421
- raise ArgumentError, "+options[:links]+ entry #{link.inspect} not supported", caller(2)
870
+ raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} of an unsupported object #{order_entry.class}"
422
871
  end
423
872
  end
424
873
  end
425
874
 
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
875
+ # Used to verify value of boolean properties in conditions
876
+ # @api private
877
+ def assert_valid_boolean(name, value)
878
+ if value != true && value != false
879
+ raise ArgumentError, "+#{name}+ should be true or false, but was #{value.inspect}"
880
+ end
432
881
  end
433
882
 
434
- # validate that all the links or includes are present for the given DM::Query::Path
883
+ # Verifies that associations given in conditions belong
884
+ # to the same repository as query's model
435
885
  #
436
- def validate_query_path_links(path)
437
- path.relationships.map do |relationship|
438
- @links << relationship unless (@links.include?(relationship) || @includes.include?(relationship))
886
+ # @api private
887
+ def assert_valid_other(other)
888
+ unless other.repository == repository
889
+ raise ArgumentError, "+other+ #{other.class} must be for the #{repository.name} repository, not #{other.repository.name}"
890
+ end
891
+
892
+ unless other.model >= model
893
+ raise ArgumentError, "+other+ #{other.class} must be for the #{model.name} model, not #{other.model.name}"
439
894
  end
440
895
  end
441
896
 
442
- def append_condition(clause, bind_value)
443
- operator = :eql
444
- bind_value = bind_value.call if bind_value.is_a?(Proc)
897
+ # Normalize order elements to Query::Direction instances
898
+ #
899
+ # TODO: needs example
900
+ #
901
+ # @api private
902
+ def normalize_order
903
+ return if @order.nil?
445
904
 
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
905
+ # TODO: should Query::Path objects be permitted? If so, then it
906
+ # should probably be normalized to a Direction object
907
+ @order = @order.map do |order|
908
+ case order
909
+ when Operator
910
+ target = order.target
911
+ property = target.kind_of?(Property) ? target : @properties[target]
912
+
913
+ Direction.new(property, order.operator)
475
914
 
476
- if property.nil?
477
- raise ArgumentError, "Clause #{clause.inspect} does not map to a DataMapper::Property", caller(2)
915
+ when Symbol, String
916
+ Direction.new(@properties[order])
917
+
918
+ when Property
919
+ Direction.new(order)
920
+
921
+ when Direction
922
+ order.dup
923
+ end
478
924
  end
925
+ end
479
926
 
480
- bind_value = dump_custom_value(property, bind_value)
927
+ # Normalize fields to Property instances
928
+ #
929
+ # TODO: needs example
930
+ #
931
+ # @api private
932
+ def normalize_fields
933
+ @fields = @fields.map do |field|
934
+ case field
935
+ when Symbol, String
936
+ @properties[field]
481
937
 
482
- @conditions << [ operator, property, bind_value ]
938
+ when Property, Operator
939
+ field
940
+ end
941
+ end
483
942
  end
484
943
 
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
944
+ # Normalize links to Query::Path
945
+ #
946
+ # Normalization means links given as symbols are replaced with
947
+ # relationships they refer to, intermediate links are "followed"
948
+ # and duplicates are removed
949
+ #
950
+ # @api private
951
+ def normalize_links
952
+ links = @links.dup
953
+
954
+ @links.clear
955
+
956
+ while link = links.shift
957
+ relationship = case link
958
+ when Symbol, String then @relationships[link]
959
+ when Associations::Relationship then link
494
960
  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
961
+
962
+ next if @links.include?(relationship)
963
+
964
+ if relationship.respond_to?(:links)
965
+ links.concat(relationship.links)
966
+ else
967
+ repository_name = relationship.relative_target_repository_name
968
+ model = relationship.target_model
969
+
970
+ # TODO: see if this can handle extracting the :order option and sort the
971
+ # resulting collection using the order specified by through relationships
972
+
973
+ model.current_scope.merge(relationship.query).each do |subject, value|
974
+ # TODO: figure out how to merge Query options from links
975
+ if OPTIONS.include?(subject)
976
+ next # skip for now
549
977
  end
550
978
 
551
- next # process the next other condition
979
+ # set @repository when appending conditions
980
+ original, @repository = @repository, DataMapper.repository(repository_name)
981
+
982
+ begin
983
+ append_condition(subject, value, model)
984
+ ensure
985
+ @repository = original
986
+ end
552
987
  end
553
- end
554
988
 
555
- # otherwise append the other condition
556
- @conditions << other_condition.dup
989
+ @links << relationship
990
+ end
557
991
  end
992
+ end
558
993
 
559
- @conditions
994
+ # Append conditions to this Query
995
+ #
996
+ # TODO: needs example
997
+ #
998
+ # @param [Property, Symbol, String, Operator, Associations::Relationship, Path] subject
999
+ # the subject to match
1000
+ # @param [Object] bind_value
1001
+ # the value to match on
1002
+ # @param [Symbol] operator
1003
+ # the operator to match with
1004
+ #
1005
+ # @return [Query::Conditions::AbstractOperation]
1006
+ # the Query conditions
1007
+ #
1008
+ # @api private
1009
+ def append_condition(subject, bind_value, model = self.model, operator = :eql)
1010
+ case subject
1011
+ when Property, Associations::Relationship then append_property_condition(subject, bind_value, operator)
1012
+ when Symbol then append_symbol_condition(subject, bind_value, model, operator)
1013
+ when String then append_string_condition(subject, bind_value, model, operator)
1014
+ when Operator then append_operator_conditions(subject, bind_value, model)
1015
+ when Path then append_path(subject, bind_value, model, operator)
1016
+ else
1017
+ raise ArgumentError, "#{subject} is an invalid instance: #{subject.class}"
1018
+ end
560
1019
  end
561
1020
 
562
- class Direction
563
- include Assertions
1021
+ # TODO: document
1022
+ # @api private
1023
+ def append_property_condition(property, bind_value, operator)
1024
+ bind_value = normalize_bind_value(property, bind_value)
1025
+ negated = operator == :not
1026
+
1027
+ if operator == :eql || negated
1028
+ operator = case bind_value
1029
+ when Array, Range, Set, Collection then :in
1030
+ when Regexp then :regexp
1031
+ else :eql
1032
+ end
1033
+ end
564
1034
 
565
- attr_reader :property, :direction
1035
+ condition = Conditions::Comparison.new(operator, property, bind_value)
566
1036
 
567
- def ==(other)
568
- return true if super
569
- hash == other.hash
1037
+ if negated
1038
+ condition = Conditions::Operation.new(:not, condition)
570
1039
  end
571
1040
 
572
- alias eql? ==
1041
+ @conditions << condition
1042
+ end
1043
+
1044
+ # TODO: document
1045
+ # @api private
1046
+ def append_symbol_condition(symbol, bind_value, model, operator)
1047
+ append_condition(symbol.to_s, bind_value, model, operator)
1048
+ end
1049
+
1050
+ # TODO: document
1051
+ # @api private
1052
+ def append_string_condition(string, bind_value, model, operator)
1053
+ if string.include?('.')
1054
+ query_path = model
1055
+ string.split('.').each { |method| query_path = query_path.send(method) }
1056
+
1057
+ append_condition(query_path, bind_value, model, operator)
1058
+ else
1059
+ repository_name = repository.name
1060
+ subject = model.properties(repository_name)[string] ||
1061
+ model.relationships(repository_name)[string]
573
1062
 
574
- def hash
575
- @property.hash + @direction.hash
1063
+ append_condition(subject, bind_value, model, operator)
576
1064
  end
1065
+ end
1066
+
1067
+ # TODO: document
1068
+ # @api private
1069
+ def append_operator_conditions(operator, bind_value, model)
1070
+ append_condition(operator.target, bind_value, model, operator.operator)
1071
+ end
1072
+
1073
+ # TODO: document
1074
+ # @api private
1075
+ def append_path(path, bind_value, model, operator)
1076
+ @links.unshift(*path.relationships.reverse.map { |relationship| relationship.inverse })
1077
+ append_condition(path.property, bind_value, path.model, operator)
1078
+ end
577
1079
 
578
- def reverse
579
- self.class.new(@property, @direction == :asc ? :desc : :asc)
1080
+ # TODO: make this typecast all bind values that do not match the
1081
+ # property primitive
1082
+
1083
+ # TODO: document
1084
+ # @api private
1085
+ def normalize_bind_value(property_or_path, bind_value)
1086
+ # TODO: defer this inside the comparison
1087
+ if bind_value.respond_to?(:call)
1088
+ bind_value = bind_value.call
580
1089
  end
581
1090
 
582
- def inspect
583
- "#<#{self.class.name} #{@property.inspect} #{@direction}>"
1091
+ # TODO: bypass this for Collection, once subqueries can be handled by adapters
1092
+ if bind_value.respond_to?(:to_ary)
1093
+ bind_value = bind_value.to_ary
1094
+ bind_value.uniq!
584
1095
  end
585
1096
 
586
- private
1097
+ # FIXME: causes m:m specs to fail with in-memory adapter
1098
+ # if bind_value.instance_of?(Array) && bind_value.size == 1
1099
+ # bind_value = bind_value.first
1100
+ # end
587
1101
 
588
- def initialize(property, direction = :asc)
589
- assert_kind_of 'property', property, Property
590
- assert_kind_of 'direction', direction, Symbol
1102
+ bind_value
1103
+ end
591
1104
 
592
- @property = property
593
- @direction = direction
1105
+ # Extract arguments for #slice and #slice! then return offset and limit
1106
+ #
1107
+ # @param [Integer, Array(Integer), Range] *args the offset,
1108
+ # offset and limit, or range indicating first and last position
1109
+ #
1110
+ # @return [Integer] the offset
1111
+ # @return [Integer, NilClass] the limit, if any
1112
+ #
1113
+ # @api private
1114
+ def extract_slice_arguments(*args)
1115
+ first_arg, second_arg = args
1116
+
1117
+ if args.size == 2 && first_arg.kind_of?(Integer) && second_arg.kind_of?(Integer)
1118
+ return first_arg, second_arg
1119
+ elsif args.size == 1
1120
+ if first_arg.kind_of?(Integer)
1121
+ return first_arg, 1
1122
+ elsif first_arg.kind_of?(Range)
1123
+ offset = first_arg.first
1124
+ limit = first_arg.last - offset
1125
+ limit += 1 unless first_arg.exclude_end?
1126
+ return offset, limit
1127
+ end
594
1128
  end
595
- end # class Direction
596
1129
 
597
- class Operator
598
- include Assertions
1130
+ raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}"
1131
+ end
599
1132
 
600
- attr_reader :target, :operator
1133
+ # TODO: document
1134
+ # @api private
1135
+ def get_relative_position(offset, limit)
1136
+ new_offset = self.offset + offset
601
1137
 
602
- def to_sym
603
- @property_name
1138
+ if limit <= 0 || (self.limit && new_offset + limit > self.offset + self.limit)
1139
+ raise RangeError, "offset #{offset} and limit #{limit} are outside allowed range"
604
1140
  end
605
1141
 
606
- def ==(other)
607
- return true if super
608
- return false unless other.kind_of?(self.class)
609
- @operator == other.operator && @target == other.target
1142
+ return new_offset, limit
1143
+ end
1144
+
1145
+ # Return true if +other+'s is equivalent or equal to +self+'s
1146
+ #
1147
+ # @param [Query] other
1148
+ # The Resource whose attributes are to be compared with +self+'s
1149
+ # @param [Symbol] operator
1150
+ # The comparison operator to use to compare the attributes
1151
+ #
1152
+ # @return [Boolean]
1153
+ # The result of the comparison of +other+'s attributes with +self+'s
1154
+ #
1155
+ # @api private
1156
+ def cmp?(other, operator)
1157
+ # check the attributes that are most likely to differ first
1158
+ unless repository.send(operator, other.repository)
1159
+ return false
610
1160
  end
611
1161
 
612
- private
1162
+ unless model.send(operator, other.model)
1163
+ return false
1164
+ end
613
1165
 
614
- def initialize(target, operator)
615
- assert_kind_of 'operator', operator, Symbol
1166
+ unless conditions.send(operator, other.conditions)
1167
+ return false
1168
+ end
616
1169
 
617
- @target = target
618
- @operator = operator
1170
+ unless offset.send(operator, other.offset)
1171
+ return false
619
1172
  end
620
- end # class Operator
621
1173
 
622
- class Path
623
- include Assertions
1174
+ unless limit.send(operator, other.limit)
1175
+ return false
1176
+ end
624
1177
 
625
- instance_methods.each { |m| undef_method m if %w[ id type ].include?(m.to_s) }
1178
+ unless order.send(operator, other.order)
1179
+ return false
1180
+ end
626
1181
 
627
- attr_reader :relationships, :model, :property, :operator
1182
+ unless fields.sort_by { |property| property.hash }.send(operator, other.fields.sort_by { |property| property.hash })
1183
+ return false
1184
+ end
628
1185
 
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
1186
+ unless links.send(operator, other.links)
1187
+ return false
635
1188
  end
636
1189
 
637
- # duck type the DM::Query::Path to act like a DM::Property
638
- def field(*args)
639
- @property ? @property.field(*args) : nil
1190
+ unless reload?.send(operator, other.reload?)
1191
+ return false
640
1192
  end
641
1193
 
642
- # more duck typing
643
- def to_sym
644
- @property ? @property.name.to_sym : @model.storage_name(@repository).to_sym
1194
+ unless unique?.send(operator, other.unique?)
1195
+ return false
645
1196
  end
646
1197
 
647
- private
1198
+ unless add_reversed?.send(operator, other.add_reversed?)
1199
+ return false
1200
+ end
648
1201
 
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?
1202
+ true
1203
+ end
654
1204
 
655
- @repository = repository
656
- @relationships = relationships
657
- @model = model
658
- @property = @model.properties(@repository.name)[property_name] if property_name
1205
+ # TODO: DRY this up with conditions
1206
+ # @api private
1207
+ def record_value(record, property)
1208
+ case record
1209
+ when Hash
1210
+ record.fetch(property, record[property.field])
1211
+ when Resource
1212
+ property.get!(record)
659
1213
  end
1214
+ end
660
1215
 
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
1216
+ # TODO: document
1217
+ # @api private
1218
+ def each_comparison
1219
+ operands = conditions.operands.dup
666
1220
 
667
- if @model.properties(@repository.name)[method]
668
- @property = @model.properties(@repository.name)[method] unless @property
669
- return self
1221
+ while operand = operands.shift
1222
+ if operand.respond_to?(:operands)
1223
+ operands.concat(operand.operands)
1224
+ else
1225
+ yield operand
670
1226
  end
671
-
672
- raise NoMethodError, "undefined property or association `#{method}' on #{@model}"
673
1227
  end
674
- end # class Path
1228
+ end
675
1229
  end # class Query
676
1230
  end # module DataMapper