dm-core 0.9.11 → 0.10.0

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