ghost_dm-core 1.3.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (254) hide show
  1. data/.autotest +29 -0
  2. data/.document +5 -0
  3. data/.gitignore +35 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +65 -0
  6. data/LICENSE +20 -0
  7. data/README.md +269 -0
  8. data/Rakefile +4 -0
  9. data/dm-core.gemspec +24 -0
  10. data/lib/dm-core.rb +292 -0
  11. data/lib/dm-core/adapters.rb +222 -0
  12. data/lib/dm-core/adapters/abstract_adapter.rb +237 -0
  13. data/lib/dm-core/adapters/in_memory_adapter.rb +113 -0
  14. data/lib/dm-core/associations/many_to_many.rb +499 -0
  15. data/lib/dm-core/associations/many_to_one.rb +290 -0
  16. data/lib/dm-core/associations/one_to_many.rb +348 -0
  17. data/lib/dm-core/associations/one_to_one.rb +86 -0
  18. data/lib/dm-core/associations/relationship.rb +663 -0
  19. data/lib/dm-core/backwards.rb +13 -0
  20. data/lib/dm-core/collection.rb +1515 -0
  21. data/lib/dm-core/core_ext/kernel.rb +23 -0
  22. data/lib/dm-core/core_ext/pathname.rb +6 -0
  23. data/lib/dm-core/core_ext/symbol.rb +10 -0
  24. data/lib/dm-core/identity_map.rb +7 -0
  25. data/lib/dm-core/model.rb +874 -0
  26. data/lib/dm-core/model/hook.rb +103 -0
  27. data/lib/dm-core/model/is.rb +32 -0
  28. data/lib/dm-core/model/property.rb +249 -0
  29. data/lib/dm-core/model/relationship.rb +378 -0
  30. data/lib/dm-core/model/scope.rb +89 -0
  31. data/lib/dm-core/property.rb +866 -0
  32. data/lib/dm-core/property/binary.rb +21 -0
  33. data/lib/dm-core/property/boolean.rb +20 -0
  34. data/lib/dm-core/property/class.rb +17 -0
  35. data/lib/dm-core/property/date.rb +10 -0
  36. data/lib/dm-core/property/date_time.rb +10 -0
  37. data/lib/dm-core/property/decimal.rb +36 -0
  38. data/lib/dm-core/property/discriminator.rb +44 -0
  39. data/lib/dm-core/property/float.rb +16 -0
  40. data/lib/dm-core/property/integer.rb +22 -0
  41. data/lib/dm-core/property/invalid_value_error.rb +22 -0
  42. data/lib/dm-core/property/lookup.rb +27 -0
  43. data/lib/dm-core/property/numeric.rb +38 -0
  44. data/lib/dm-core/property/object.rb +34 -0
  45. data/lib/dm-core/property/serial.rb +14 -0
  46. data/lib/dm-core/property/string.rb +38 -0
  47. data/lib/dm-core/property/text.rb +9 -0
  48. data/lib/dm-core/property/time.rb +10 -0
  49. data/lib/dm-core/property_set.rb +177 -0
  50. data/lib/dm-core/query.rb +1366 -0
  51. data/lib/dm-core/query/conditions/comparison.rb +911 -0
  52. data/lib/dm-core/query/conditions/operation.rb +721 -0
  53. data/lib/dm-core/query/direction.rb +36 -0
  54. data/lib/dm-core/query/operator.rb +35 -0
  55. data/lib/dm-core/query/path.rb +114 -0
  56. data/lib/dm-core/query/sort.rb +39 -0
  57. data/lib/dm-core/relationship_set.rb +72 -0
  58. data/lib/dm-core/repository.rb +226 -0
  59. data/lib/dm-core/resource.rb +1214 -0
  60. data/lib/dm-core/resource/persistence_state.rb +75 -0
  61. data/lib/dm-core/resource/persistence_state/clean.rb +40 -0
  62. data/lib/dm-core/resource/persistence_state/deleted.rb +30 -0
  63. data/lib/dm-core/resource/persistence_state/dirty.rb +96 -0
  64. data/lib/dm-core/resource/persistence_state/immutable.rb +34 -0
  65. data/lib/dm-core/resource/persistence_state/persisted.rb +29 -0
  66. data/lib/dm-core/resource/persistence_state/transient.rb +80 -0
  67. data/lib/dm-core/spec/lib/adapter_helpers.rb +64 -0
  68. data/lib/dm-core/spec/lib/collection_helpers.rb +21 -0
  69. data/lib/dm-core/spec/lib/counter_adapter.rb +38 -0
  70. data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
  71. data/lib/dm-core/spec/lib/spec_helper.rb +74 -0
  72. data/lib/dm-core/spec/setup.rb +174 -0
  73. data/lib/dm-core/spec/shared/adapter_spec.rb +341 -0
  74. data/lib/dm-core/spec/shared/public/property_spec.rb +229 -0
  75. data/lib/dm-core/spec/shared/resource_spec.rb +1232 -0
  76. data/lib/dm-core/spec/shared/sel_spec.rb +111 -0
  77. data/lib/dm-core/spec/shared/semipublic/property_spec.rb +176 -0
  78. data/lib/dm-core/spec/shared/semipublic/query/conditions/abstract_comparison_spec.rb +261 -0
  79. data/lib/dm-core/support/assertions.rb +8 -0
  80. data/lib/dm-core/support/chainable.rb +18 -0
  81. data/lib/dm-core/support/deprecate.rb +12 -0
  82. data/lib/dm-core/support/descendant_set.rb +89 -0
  83. data/lib/dm-core/support/equalizer.rb +48 -0
  84. data/lib/dm-core/support/ext/array.rb +22 -0
  85. data/lib/dm-core/support/ext/blank.rb +25 -0
  86. data/lib/dm-core/support/ext/hash.rb +67 -0
  87. data/lib/dm-core/support/ext/module.rb +47 -0
  88. data/lib/dm-core/support/ext/object.rb +57 -0
  89. data/lib/dm-core/support/ext/string.rb +24 -0
  90. data/lib/dm-core/support/ext/try_dup.rb +12 -0
  91. data/lib/dm-core/support/hook.rb +405 -0
  92. data/lib/dm-core/support/inflections.rb +60 -0
  93. data/lib/dm-core/support/inflector/inflections.rb +211 -0
  94. data/lib/dm-core/support/inflector/methods.rb +151 -0
  95. data/lib/dm-core/support/lazy_array.rb +451 -0
  96. data/lib/dm-core/support/local_object_space.rb +13 -0
  97. data/lib/dm-core/support/logger.rb +201 -0
  98. data/lib/dm-core/support/mash.rb +176 -0
  99. data/lib/dm-core/support/naming_conventions.rb +90 -0
  100. data/lib/dm-core/support/ordered_set.rb +380 -0
  101. data/lib/dm-core/support/subject.rb +33 -0
  102. data/lib/dm-core/support/subject_set.rb +250 -0
  103. data/lib/dm-core/version.rb +3 -0
  104. data/script/performance.rb +275 -0
  105. data/script/profile.rb +218 -0
  106. data/spec/lib/rspec_immediate_feedback_formatter.rb +54 -0
  107. data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +68 -0
  108. data/spec/public/associations/many_to_many_spec.rb +197 -0
  109. data/spec/public/associations/many_to_one_spec.rb +83 -0
  110. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +40 -0
  111. data/spec/public/associations/many_to_one_with_custom_fk_spec.rb +49 -0
  112. data/spec/public/associations/one_to_many_spec.rb +81 -0
  113. data/spec/public/associations/one_to_one_spec.rb +176 -0
  114. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +46 -0
  115. data/spec/public/collection_spec.rb +69 -0
  116. data/spec/public/finalize_spec.rb +76 -0
  117. data/spec/public/model/hook_spec.rb +246 -0
  118. data/spec/public/model/property_spec.rb +88 -0
  119. data/spec/public/model/relationship_spec.rb +1040 -0
  120. data/spec/public/model_spec.rb +462 -0
  121. data/spec/public/property/binary_spec.rb +41 -0
  122. data/spec/public/property/boolean_spec.rb +22 -0
  123. data/spec/public/property/class_spec.rb +28 -0
  124. data/spec/public/property/date_spec.rb +22 -0
  125. data/spec/public/property/date_time_spec.rb +22 -0
  126. data/spec/public/property/decimal_spec.rb +23 -0
  127. data/spec/public/property/discriminator_spec.rb +135 -0
  128. data/spec/public/property/float_spec.rb +22 -0
  129. data/spec/public/property/integer_spec.rb +22 -0
  130. data/spec/public/property/object_spec.rb +107 -0
  131. data/spec/public/property/serial_spec.rb +22 -0
  132. data/spec/public/property/string_spec.rb +22 -0
  133. data/spec/public/property/text_spec.rb +63 -0
  134. data/spec/public/property/time_spec.rb +22 -0
  135. data/spec/public/property_spec.rb +341 -0
  136. data/spec/public/resource_spec.rb +288 -0
  137. data/spec/public/sel_spec.rb +53 -0
  138. data/spec/public/setup_spec.rb +145 -0
  139. data/spec/public/shared/association_collection_shared_spec.rb +309 -0
  140. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  141. data/spec/public/shared/collection_shared_spec.rb +1667 -0
  142. data/spec/public/shared/finder_shared_spec.rb +1629 -0
  143. data/spec/rcov.opts +6 -0
  144. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  145. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +13 -0
  146. data/spec/semipublic/associations/many_to_many_spec.rb +94 -0
  147. data/spec/semipublic/associations/many_to_one_spec.rb +63 -0
  148. data/spec/semipublic/associations/one_to_many_spec.rb +55 -0
  149. data/spec/semipublic/associations/one_to_one_spec.rb +53 -0
  150. data/spec/semipublic/associations/relationship_spec.rb +200 -0
  151. data/spec/semipublic/associations_spec.rb +177 -0
  152. data/spec/semipublic/collection_spec.rb +110 -0
  153. data/spec/semipublic/model_spec.rb +96 -0
  154. data/spec/semipublic/property/binary_spec.rb +13 -0
  155. data/spec/semipublic/property/boolean_spec.rb +47 -0
  156. data/spec/semipublic/property/class_spec.rb +33 -0
  157. data/spec/semipublic/property/date_spec.rb +43 -0
  158. data/spec/semipublic/property/date_time_spec.rb +46 -0
  159. data/spec/semipublic/property/decimal_spec.rb +83 -0
  160. data/spec/semipublic/property/discriminator_spec.rb +19 -0
  161. data/spec/semipublic/property/float_spec.rb +82 -0
  162. data/spec/semipublic/property/integer_spec.rb +82 -0
  163. data/spec/semipublic/property/lookup_spec.rb +29 -0
  164. data/spec/semipublic/property/serial_spec.rb +13 -0
  165. data/spec/semipublic/property/string_spec.rb +13 -0
  166. data/spec/semipublic/property/text_spec.rb +31 -0
  167. data/spec/semipublic/property/time_spec.rb +50 -0
  168. data/spec/semipublic/property_spec.rb +114 -0
  169. data/spec/semipublic/query/conditions/comparison_spec.rb +1501 -0
  170. data/spec/semipublic/query/conditions/operation_spec.rb +1294 -0
  171. data/spec/semipublic/query/path_spec.rb +471 -0
  172. data/spec/semipublic/query_spec.rb +3682 -0
  173. data/spec/semipublic/resource/state/clean_spec.rb +88 -0
  174. data/spec/semipublic/resource/state/deleted_spec.rb +78 -0
  175. data/spec/semipublic/resource/state/dirty_spec.rb +162 -0
  176. data/spec/semipublic/resource/state/immutable_spec.rb +105 -0
  177. data/spec/semipublic/resource/state/transient_spec.rb +162 -0
  178. data/spec/semipublic/resource/state_spec.rb +230 -0
  179. data/spec/semipublic/resource_spec.rb +23 -0
  180. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  181. data/spec/semipublic/shared/resource_shared_spec.rb +199 -0
  182. data/spec/semipublic/shared/resource_state_shared_spec.rb +79 -0
  183. data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
  184. data/spec/spec.opts +5 -0
  185. data/spec/spec_helper.rb +38 -0
  186. data/spec/support/core_ext/hash.rb +10 -0
  187. data/spec/support/core_ext/inheritable_attributes.rb +46 -0
  188. data/spec/support/properties/huge_integer.rb +17 -0
  189. data/spec/unit/array_spec.rb +23 -0
  190. data/spec/unit/blank_spec.rb +73 -0
  191. data/spec/unit/data_mapper/ordered_set/append_spec.rb +26 -0
  192. data/spec/unit/data_mapper/ordered_set/clear_spec.rb +24 -0
  193. data/spec/unit/data_mapper/ordered_set/delete_spec.rb +28 -0
  194. data/spec/unit/data_mapper/ordered_set/each_spec.rb +19 -0
  195. data/spec/unit/data_mapper/ordered_set/empty_spec.rb +20 -0
  196. data/spec/unit/data_mapper/ordered_set/entries_spec.rb +22 -0
  197. data/spec/unit/data_mapper/ordered_set/eql_spec.rb +51 -0
  198. data/spec/unit/data_mapper/ordered_set/equal_value_spec.rb +84 -0
  199. data/spec/unit/data_mapper/ordered_set/hash_spec.rb +12 -0
  200. data/spec/unit/data_mapper/ordered_set/include_spec.rb +23 -0
  201. data/spec/unit/data_mapper/ordered_set/index_spec.rb +28 -0
  202. data/spec/unit/data_mapper/ordered_set/initialize_spec.rb +32 -0
  203. data/spec/unit/data_mapper/ordered_set/merge_spec.rb +36 -0
  204. data/spec/unit/data_mapper/ordered_set/shared/append_spec.rb +24 -0
  205. data/spec/unit/data_mapper/ordered_set/shared/clear_spec.rb +9 -0
  206. data/spec/unit/data_mapper/ordered_set/shared/delete_spec.rb +25 -0
  207. data/spec/unit/data_mapper/ordered_set/shared/each_spec.rb +17 -0
  208. data/spec/unit/data_mapper/ordered_set/shared/empty_spec.rb +9 -0
  209. data/spec/unit/data_mapper/ordered_set/shared/entries_spec.rb +9 -0
  210. data/spec/unit/data_mapper/ordered_set/shared/include_spec.rb +9 -0
  211. data/spec/unit/data_mapper/ordered_set/shared/index_spec.rb +13 -0
  212. data/spec/unit/data_mapper/ordered_set/shared/initialize_spec.rb +28 -0
  213. data/spec/unit/data_mapper/ordered_set/shared/merge_spec.rb +28 -0
  214. data/spec/unit/data_mapper/ordered_set/shared/size_spec.rb +13 -0
  215. data/spec/unit/data_mapper/ordered_set/shared/to_ary_spec.rb +11 -0
  216. data/spec/unit/data_mapper/ordered_set/size_spec.rb +27 -0
  217. data/spec/unit/data_mapper/ordered_set/to_ary_spec.rb +23 -0
  218. data/spec/unit/data_mapper/subject_set/append_spec.rb +47 -0
  219. data/spec/unit/data_mapper/subject_set/clear_spec.rb +34 -0
  220. data/spec/unit/data_mapper/subject_set/delete_spec.rb +40 -0
  221. data/spec/unit/data_mapper/subject_set/each_spec.rb +30 -0
  222. data/spec/unit/data_mapper/subject_set/empty_spec.rb +31 -0
  223. data/spec/unit/data_mapper/subject_set/entries_spec.rb +31 -0
  224. data/spec/unit/data_mapper/subject_set/get_spec.rb +34 -0
  225. data/spec/unit/data_mapper/subject_set/include_spec.rb +32 -0
  226. data/spec/unit/data_mapper/subject_set/named_spec.rb +33 -0
  227. data/spec/unit/data_mapper/subject_set/shared/append_spec.rb +18 -0
  228. data/spec/unit/data_mapper/subject_set/shared/clear_spec.rb +9 -0
  229. data/spec/unit/data_mapper/subject_set/shared/delete_spec.rb +9 -0
  230. data/spec/unit/data_mapper/subject_set/shared/each_spec.rb +9 -0
  231. data/spec/unit/data_mapper/subject_set/shared/empty_spec.rb +9 -0
  232. data/spec/unit/data_mapper/subject_set/shared/entries_spec.rb +9 -0
  233. data/spec/unit/data_mapper/subject_set/shared/get_spec.rb +9 -0
  234. data/spec/unit/data_mapper/subject_set/shared/include_spec.rb +9 -0
  235. data/spec/unit/data_mapper/subject_set/shared/named_spec.rb +9 -0
  236. data/spec/unit/data_mapper/subject_set/shared/size_spec.rb +13 -0
  237. data/spec/unit/data_mapper/subject_set/shared/to_ary_spec.rb +9 -0
  238. data/spec/unit/data_mapper/subject_set/shared/values_at_spec.rb +44 -0
  239. data/spec/unit/data_mapper/subject_set/size_spec.rb +42 -0
  240. data/spec/unit/data_mapper/subject_set/to_ary_spec.rb +34 -0
  241. data/spec/unit/data_mapper/subject_set/values_at_spec.rb +57 -0
  242. data/spec/unit/hash_spec.rb +28 -0
  243. data/spec/unit/hook_spec.rb +1235 -0
  244. data/spec/unit/inflections_spec.rb +16 -0
  245. data/spec/unit/lazy_array_spec.rb +1949 -0
  246. data/spec/unit/mash_spec.rb +312 -0
  247. data/spec/unit/module_spec.rb +71 -0
  248. data/spec/unit/object_spec.rb +38 -0
  249. data/spec/unit/try_dup_spec.rb +46 -0
  250. data/tasks/ci.rake +1 -0
  251. data/tasks/spec.rake +38 -0
  252. data/tasks/yard.rake +9 -0
  253. data/tasks/yardstick.rake +19 -0
  254. metadata +365 -0
@@ -0,0 +1,1366 @@
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
+
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
+ #
28
+ class Query
29
+ include DataMapper::Assertions
30
+ extend Equalizer
31
+
32
+ OPTIONS = [ :fields, :links, :conditions, :offset, :limit, :order, :unique, :add_reversed, :reload ].to_set.freeze
33
+
34
+ equalize :repository, :model, :sorted_fields, :links, :conditions, :order, :offset, :limit, :reload?, :unique?, :add_reversed?
35
+
36
+ # Extract conditions to match a Resource or Collection
37
+ #
38
+ # @param [Array, Collection, Resource] source
39
+ # the source to extract the values from
40
+ # @param [ProperySet] source_key
41
+ # the key to extract the value from the resource
42
+ # @param [ProperySet] target_key
43
+ # the key to match the resource with
44
+ #
45
+ # @return [AbstractComparison, AbstractOperation]
46
+ # the conditions to match the resources with
47
+ #
48
+ # @api private
49
+ def self.target_conditions(source, source_key, target_key)
50
+ target_key_size = target_key.size
51
+ source_values = []
52
+
53
+ if source.nil?
54
+ source_values << [ nil ] * target_key_size
55
+ else
56
+ Array(source).each do |resource|
57
+ next unless source_key.loaded?(resource)
58
+ source_value = source_key.get!(resource)
59
+ next unless target_key.valid?(source_value)
60
+ source_values << source_value
61
+ end
62
+ end
63
+
64
+ source_values.uniq!
65
+
66
+ if target_key_size == 1
67
+ target_key = target_key.first
68
+ source_values.flatten!
69
+
70
+ if source_values.size == 1
71
+ Conditions::EqualToComparison.new(target_key, source_values.first)
72
+ else
73
+ Conditions::InclusionComparison.new(target_key, source_values)
74
+ end
75
+ else
76
+ or_operation = Conditions::OrOperation.new
77
+
78
+ source_values.each do |source_value|
79
+ and_operation = Conditions::AndOperation.new
80
+
81
+ target_key.zip(source_value) do |property, value|
82
+ and_operation << Conditions::EqualToComparison.new(property, value)
83
+ end
84
+
85
+ or_operation << and_operation
86
+ end
87
+
88
+ or_operation
89
+ end
90
+ end
91
+
92
+ # @param [Repository] repository
93
+ # the default repository to scope the query within
94
+ # @param [Model] model
95
+ # the default model for the query
96
+ # @param [#query, Enumerable] source
97
+ # the source to generate the query with
98
+ #
99
+ # @return [Query]
100
+ # the query to match the resources with
101
+ #
102
+ # @api private
103
+ def self.target_query(repository, model, source)
104
+ if source.respond_to?(:query)
105
+ source.query
106
+ elsif source.kind_of?(Enumerable)
107
+ key = model.key(repository.name)
108
+ conditions = Query.target_conditions(source, key, key)
109
+ repository.new_query(model, :conditions => conditions)
110
+ else
111
+ raise ArgumentError, "+source+ must respond to #query or be an Enumerable, but was #{source.class}"
112
+ end
113
+ end
114
+
115
+ # Returns the repository query should be
116
+ # executed in
117
+ #
118
+ # Set in cases like the following:
119
+ #
120
+ # @example
121
+ #
122
+ # Document.all(:repository => :medline)
123
+ #
124
+ #
125
+ # @return [Repository]
126
+ # the Repository to retrieve results from
127
+ #
128
+ # @api semipublic
129
+ attr_reader :repository
130
+
131
+ # Returns model (class) that is used
132
+ # to instantiate objects from query result
133
+ # returned by adapter
134
+ #
135
+ # @return [Model]
136
+ # the Model to retrieve results from
137
+ #
138
+ # @api semipublic
139
+ attr_reader :model
140
+
141
+ # Returns the fields
142
+ #
143
+ # Set in cases like the following:
144
+ #
145
+ # @example
146
+ #
147
+ # Document.all(:fields => [:title, :vernacular_title, :abstract])
148
+ #
149
+ # @return [PropertySet]
150
+ # the properties in the Model that will be retrieved
151
+ #
152
+ # @api semipublic
153
+ attr_reader :fields
154
+
155
+ # Returns the links (associations) query fetches
156
+ #
157
+ # @return [Array<DataMapper::Associations::Relationship>]
158
+ # the relationships that will be used to scope the results
159
+ #
160
+ # @api private
161
+ attr_reader :links
162
+
163
+ # Returns the conditions of the query
164
+ #
165
+ # In the following example:
166
+ #
167
+ # @example
168
+ #
169
+ # Team.all(:wins.gt => 30, :conference => 'East')
170
+ #
171
+ # Conditions are "greater than" operator for "wins"
172
+ # field and exact match operator for "conference".
173
+ #
174
+ # @return [Array]
175
+ # the conditions that will be used to scope the results
176
+ #
177
+ # @api semipublic
178
+ attr_reader :conditions
179
+
180
+ # Returns the offset query uses
181
+ #
182
+ # Set in cases like the following:
183
+ #
184
+ # @example
185
+ #
186
+ # Document.all(:offset => page.offset)
187
+ #
188
+ # @return [Integer]
189
+ # the offset of the results
190
+ #
191
+ # @api semipublic
192
+ attr_reader :offset
193
+
194
+ # Returns the limit query uses
195
+ #
196
+ # Set in cases like the following:
197
+ #
198
+ # @example
199
+ #
200
+ # Document.all(:limit => 10)
201
+ #
202
+ # @return [Integer, nil]
203
+ # the maximum number of results
204
+ #
205
+ # @api semipublic
206
+ attr_reader :limit
207
+
208
+ # Returns the order
209
+ #
210
+ # Set in cases like the following:
211
+ #
212
+ # @example
213
+ #
214
+ # Document.all(:order => [:created_at.desc, :length.desc])
215
+ #
216
+ # query order is a set of two ordering rules, descending on
217
+ # "created_at" field and descending again on "length" field
218
+ #
219
+ # @return [Array]
220
+ # the order of results
221
+ #
222
+ # @api semipublic
223
+ attr_reader :order
224
+
225
+ # Returns the original options
226
+ #
227
+ # @return [Hash]
228
+ # the original options
229
+ #
230
+ # @api private
231
+ attr_reader :options
232
+
233
+ # Indicates if each result should be returned in reverse order
234
+ #
235
+ # Set in cases like the following:
236
+ #
237
+ # @example
238
+ #
239
+ # Document.all(:limit => 5).reverse
240
+ #
241
+ # Note that :add_reversed option may be used in conditions directly,
242
+ # but this is rarely the case
243
+ #
244
+ # @return [Boolean]
245
+ # true if the results should be reversed, false if not
246
+ #
247
+ # @api private
248
+ def add_reversed?
249
+ @add_reversed
250
+ end
251
+
252
+ # Indicates if the Query results should replace the results in the Identity Map
253
+ #
254
+ # TODO: needs example
255
+ #
256
+ # @return [Boolean]
257
+ # true if the results should be reloaded, false if not
258
+ #
259
+ # @api semipublic
260
+ def reload?
261
+ @reload
262
+ end
263
+
264
+ # Indicates if the Query results should be unique
265
+ #
266
+ # TODO: needs example
267
+ #
268
+ # @return [Boolean]
269
+ # true if the results should be unique, false if not
270
+ #
271
+ # @api semipublic
272
+ def unique?
273
+ @unique
274
+ end
275
+
276
+ # Indicates if the Query has raw conditions
277
+ #
278
+ # @return [Boolean]
279
+ # true if the query has raw conditions, false if not
280
+ #
281
+ # @api semipublic
282
+ def raw?
283
+ @raw
284
+ end
285
+
286
+ # Indicates if the Query is valid
287
+ #
288
+ # @return [Boolean]
289
+ # true if the query is valid
290
+ #
291
+ # @api semipublic
292
+ def valid?
293
+ conditions.valid?
294
+ end
295
+
296
+ # Returns a new Query with a reversed order
297
+ #
298
+ # @example
299
+ #
300
+ # Document.all(:limit => 5).reverse
301
+ #
302
+ # Will execute a single query with correct order
303
+ #
304
+ # @return [Query]
305
+ # new Query with reversed order
306
+ #
307
+ # @api semipublic
308
+ def reverse
309
+ dup.reverse!
310
+ end
311
+
312
+ # Reverses the sort order of the Query
313
+ #
314
+ # @example
315
+ #
316
+ # Document.all(:limit => 5).reverse
317
+ #
318
+ # Will execute a single query with original order
319
+ # and then reverse collection in the Ruby space
320
+ #
321
+ # @return [Query]
322
+ # self
323
+ #
324
+ # @api semipublic
325
+ def reverse!
326
+ # reverse the sort order
327
+ @order.map! { |direction| direction.dup.reverse! }
328
+
329
+ # copy the order to the options
330
+ @options = @options.merge(:order => @order).freeze
331
+
332
+ self
333
+ end
334
+
335
+ # Updates the Query with another Query or conditions
336
+ #
337
+ # Pretty unrealistic example:
338
+ #
339
+ # @example
340
+ #
341
+ # Journal.all(:limit => 2).query.limit # => 2
342
+ # Journal.all(:limit => 2).query.update(:limit => 3).limit # => 3
343
+ #
344
+ # @param [Query, Hash] other
345
+ # other Query or conditions
346
+ #
347
+ # @return [Query]
348
+ # self
349
+ #
350
+ # @api semipublic
351
+ def update(other)
352
+ other_options = if kind_of?(other.class)
353
+ return self if self.eql?(other)
354
+ assert_valid_other(other)
355
+ other.options
356
+ else
357
+ other = other.to_hash
358
+ return self if other.empty?
359
+ other
360
+ end
361
+
362
+ @options = @options.merge(other_options).freeze
363
+ assert_valid_options(@options)
364
+
365
+ normalize = DataMapper::Ext::Hash.only(other_options, *OPTIONS - [ :conditions ]).map do |attribute, value|
366
+ instance_variable_set("@#{attribute}", DataMapper::Ext.try_dup(value))
367
+ attribute
368
+ end
369
+
370
+ merge_conditions([ DataMapper::Ext::Hash.except(other_options, *OPTIONS), other_options[:conditions] ])
371
+ normalize_options(normalize | [ :links, :unique ])
372
+
373
+ self
374
+ end
375
+
376
+ # Similar to Query#update, but acts on a duplicate.
377
+ #
378
+ # @param [Query, Hash] other
379
+ # other query to merge with
380
+ #
381
+ # @return [Query]
382
+ # updated duplicate of original query
383
+ #
384
+ # @api semipublic
385
+ def merge(other)
386
+ dup.update(other)
387
+ end
388
+
389
+ # Builds and returns new query that merges
390
+ # original with one given, and slices the result
391
+ # with respect to :limit and :offset options
392
+ #
393
+ # This method is used by Collection to
394
+ # concatenate options from multiple chained
395
+ # calls in cases like the following:
396
+ #
397
+ # @example
398
+ #
399
+ # author.books.all(:year => 2009).all(:published => false)
400
+ #
401
+ # @api semipublic
402
+ def relative(options)
403
+ options = options.to_hash
404
+
405
+ offset = nil
406
+ limit = self.limit
407
+
408
+ if options.key?(:offset) && (options.key?(:limit) || limit)
409
+ options = options.dup
410
+ offset = options.delete(:offset)
411
+ limit = options.delete(:limit) || limit - offset
412
+ end
413
+
414
+ query = merge(options)
415
+ query = query.slice!(offset, limit) if offset
416
+ query
417
+ end
418
+
419
+ # Return the union with another query
420
+ #
421
+ # @param [Query] other
422
+ # the other query
423
+ #
424
+ # @return [Query]
425
+ # the union of the query and other
426
+ #
427
+ # @api semipublic
428
+ def union(other)
429
+ return dup if self == other
430
+ set_operation(:union, other)
431
+ end
432
+
433
+ alias_method :|, :union
434
+ alias_method :+, :union
435
+
436
+ # Return the intersection with another query
437
+ #
438
+ # @param [Query] other
439
+ # the other query
440
+ #
441
+ # @return [Query]
442
+ # the intersection of the query and other
443
+ #
444
+ # @api semipublic
445
+ def intersection(other)
446
+ return dup if self == other
447
+ set_operation(:intersection, other)
448
+ end
449
+
450
+ alias_method :&, :intersection
451
+
452
+ # Return the difference with another query
453
+ #
454
+ # @param [Query] other
455
+ # the other query
456
+ #
457
+ # @return [Query]
458
+ # the difference of the query and other
459
+ #
460
+ # @api semipublic
461
+ def difference(other)
462
+ set_operation(:difference, other)
463
+ end
464
+
465
+ alias_method :-, :difference
466
+
467
+ # Clear conditions
468
+ #
469
+ # @return [self]
470
+ #
471
+ # @api semipublic
472
+ def clear
473
+ @conditions = Conditions::Operation.new(:null)
474
+ self
475
+ end
476
+
477
+ # Takes an Enumerable of records, and destructively filters it.
478
+ # First finds all matching conditions, then sorts it,
479
+ # then does offset & limit
480
+ #
481
+ # @param [Enumerable] records
482
+ # The set of records to be filtered
483
+ #
484
+ # @return [Enumerable]
485
+ # Whats left of the given array after the filtering
486
+ #
487
+ # @api semipublic
488
+ def filter_records(records)
489
+ records = records.uniq if unique?
490
+ records = match_records(records) if conditions
491
+ records = sort_records(records) if order
492
+ records = limit_records(records) if limit || offset > 0
493
+ records
494
+ end
495
+
496
+ # Filter a set of records by the conditions
497
+ #
498
+ # @param [Enumerable] records
499
+ # The set of records to be filtered
500
+ #
501
+ # @return [Enumerable]
502
+ # Whats left of the given array after the matching
503
+ #
504
+ # @api semipublic
505
+ def match_records(records)
506
+ conditions = self.conditions
507
+ records.select { |record| conditions.matches?(record) }
508
+ end
509
+
510
+ # Sorts a list of Records by the order
511
+ #
512
+ # @param [Enumerable] records
513
+ # A list of Resources to sort
514
+ #
515
+ # @return [Enumerable]
516
+ # The sorted records
517
+ #
518
+ # @api semipublic
519
+ def sort_records(records)
520
+ sort_order = order.map { |direction| [ direction.target, direction.operator == :asc ] }
521
+
522
+ records.sort_by do |record|
523
+ sort_order.map do |(property, ascending)|
524
+ Sort.new(record_value(record, property), ascending)
525
+ end
526
+ end
527
+ end
528
+
529
+ # Limits a set of records by the offset and/or limit
530
+ #
531
+ # @param [Enumerable] records
532
+ # A list of records to sort
533
+ #
534
+ # @return [Enumerable]
535
+ # The offset & limited records
536
+ #
537
+ # @api semipublic
538
+ def limit_records(records)
539
+ offset = self.offset
540
+ limit = self.limit
541
+ size = records.size
542
+
543
+ if offset > size - 1
544
+ []
545
+ elsif (limit && limit != size) || offset > 0
546
+ records[offset, limit || size] || []
547
+ else
548
+ records.dup
549
+ end
550
+ end
551
+
552
+ # Slices collection by adding limit and offset to the
553
+ # query, so a single query is executed
554
+ #
555
+ # @example
556
+ #
557
+ # Journal.all(:limit => 10).slice(3, 5)
558
+ #
559
+ # will execute query with the following limit and offset
560
+ # (when repository uses DataObjects adapter, and thus
561
+ # queries use SQL):
562
+ #
563
+ # LIMIT 5 OFFSET 3
564
+ #
565
+ # @api semipublic
566
+ def slice(*args)
567
+ dup.slice!(*args)
568
+ end
569
+
570
+ alias_method :[], :slice
571
+
572
+ # Slices collection by adding limit and offset to the
573
+ # query, so a single query is executed
574
+ #
575
+ # @example
576
+ #
577
+ # Journal.all(:limit => 10).slice!(3, 5)
578
+ #
579
+ # will execute query with the following limit
580
+ # (when repository uses DataObjects adapter, and thus
581
+ # queries use SQL):
582
+ #
583
+ # LIMIT 10
584
+ #
585
+ # and then takes a slice of collection in the Ruby space
586
+ #
587
+ # @api semipublic
588
+ def slice!(*args)
589
+ offset, limit = extract_slice_arguments(*args)
590
+
591
+ if self.limit || self.offset > 0
592
+ offset, limit = get_relative_position(offset, limit)
593
+ end
594
+
595
+ update(:offset => offset, :limit => limit)
596
+ end
597
+
598
+ # Returns detailed human readable
599
+ # string representation of the query
600
+ #
601
+ # @return [String] detailed string representation of the query
602
+ #
603
+ # @api semipublic
604
+ def inspect
605
+ attrs = [
606
+ [ :repository, repository.name ],
607
+ [ :model, model ],
608
+ [ :fields, fields ],
609
+ [ :links, links ],
610
+ [ :conditions, conditions ],
611
+ [ :order, order ],
612
+ [ :limit, limit ],
613
+ [ :offset, offset ],
614
+ [ :reload, reload? ],
615
+ [ :unique, unique? ],
616
+ ]
617
+
618
+ "#<#{self.class.name} #{attrs.map { |key, value| "@#{key}=#{value.inspect}" }.join(' ')}>"
619
+ end
620
+
621
+ # Get the properties used in the conditions
622
+ #
623
+ # @return [Set<Property>]
624
+ # Set of properties used in the conditions
625
+ #
626
+ # @api private
627
+ def condition_properties
628
+ properties = Set.new
629
+
630
+ each_comparison do |comparison|
631
+ next unless comparison.respond_to?(:subject)
632
+ subject = comparison.subject
633
+ properties << subject if subject.kind_of?(Property)
634
+ end
635
+
636
+ properties
637
+ end
638
+
639
+ # Return a list of fields in predictable order
640
+ #
641
+ # @return [Array<Property>]
642
+ # list of fields sorted in deterministic order
643
+ #
644
+ # @api private
645
+ def sorted_fields
646
+ fields.sort_by { |property| property.hash }
647
+ end
648
+
649
+ # Transform Query into subquery conditions
650
+ #
651
+ # @return [AndOperation]
652
+ # a subquery for the Query
653
+ #
654
+ # @api private
655
+ def to_subquery
656
+ collection = model.all(merge(:fields => model_key))
657
+ Conditions::Operation.new(:and, Conditions::Comparison.new(:in, self_relationship, collection))
658
+ end
659
+
660
+ # Hash representation of a Query
661
+ #
662
+ # @return [Hash]
663
+ # Hash representation of a Query
664
+ #
665
+ # @api private
666
+ def to_hash
667
+ {
668
+ :repository => repository.name,
669
+ :model => model.name,
670
+ :fields => fields,
671
+ :links => links,
672
+ :conditions => conditions,
673
+ :offset => offset,
674
+ :limit => limit,
675
+ :order => order,
676
+ :unique => unique?,
677
+ :add_reversed => add_reversed?,
678
+ :reload => reload?,
679
+ }
680
+ end
681
+
682
+ # Extract options from a Query
683
+ #
684
+ # @param [Query] query
685
+ # the query to extract options from
686
+ #
687
+ # @return [Hash]
688
+ # the options to use to initialize the new query
689
+ #
690
+ # @api private
691
+ def to_relative_hash
692
+ DataMapper::Ext::Hash.only(to_hash, :fields, :order, :unique, :add_reversed, :reload)
693
+ end
694
+
695
+ private
696
+
697
+ # Initializes a Query instance
698
+ #
699
+ # @example
700
+ #
701
+ # JournalIssue.all(:repository => :medline, :created_on.gte => Date.today - 7)
702
+ #
703
+ # initialized a query with repository defined with name :medline,
704
+ # model JournalIssue and options { :created_on.gte => Date.today - 7 }
705
+ #
706
+ # @param [Repository] repository
707
+ # the Repository to retrieve results from
708
+ # @param [Model] model
709
+ # the Model to retrieve results from
710
+ # @param [Hash] options
711
+ # the conditions and scope
712
+ #
713
+ # @api semipublic
714
+ def initialize(repository, model, options = {})
715
+ assert_kind_of 'repository', repository, Repository
716
+ assert_kind_of 'model', model, Model
717
+
718
+ @repository = repository
719
+ @model = model
720
+ @options = options.dup.freeze
721
+
722
+ repository_name = repository.name
723
+
724
+ @properties = @model.properties(repository_name)
725
+ @relationships = @model.relationships(repository_name)
726
+
727
+ assert_valid_options(@options)
728
+
729
+ @fields = @options.fetch :fields, @properties.defaults
730
+ @links = @options.key?(:links) ? @options[:links].dup : []
731
+ @conditions = Conditions::Operation.new(:null)
732
+ @offset = @options.fetch :offset, 0
733
+ @limit = @options.fetch :limit, nil
734
+ @order = @options.fetch :order, @model.default_order(repository_name)
735
+ @unique = @options.fetch :unique, true
736
+ @add_reversed = @options.fetch :add_reversed, false
737
+ @reload = @options.fetch :reload, false
738
+ @raw = false
739
+
740
+ merge_conditions([ DataMapper::Ext::Hash.except(@options, *OPTIONS), @options[:conditions] ])
741
+ normalize_options
742
+ end
743
+
744
+ # Copying contructor, called for Query#dup
745
+ #
746
+ # @api semipublic
747
+ def initialize_copy(*)
748
+ @fields = @fields.dup
749
+ @links = @links.dup
750
+ @conditions = @conditions.dup
751
+ @order = DataMapper::Ext.try_dup(@order)
752
+ end
753
+
754
+ # Validate the options
755
+ #
756
+ # @param [#each] options
757
+ # the options to validate
758
+ #
759
+ # @raise [ArgumentError]
760
+ # if any pairs in +options+ are invalid options
761
+ #
762
+ # @api private
763
+ def assert_valid_options(options)
764
+ options = options.to_hash
765
+
766
+ options.each do |attribute, value|
767
+ case attribute
768
+ when :fields then assert_valid_fields(value, options[:unique])
769
+ when :links then assert_valid_links(value)
770
+ when :conditions then assert_valid_conditions(value)
771
+ when :offset then assert_valid_offset(value, options[:limit])
772
+ when :limit then assert_valid_limit(value)
773
+ when :order then assert_valid_order(value, options[:fields])
774
+ when :unique, :add_reversed, :reload then assert_valid_boolean("options[:#{attribute}]", value)
775
+ else
776
+ assert_valid_conditions(attribute => value)
777
+ end
778
+ end
779
+ end
780
+
781
+ # Verifies that value of :fields option
782
+ # refers to existing properties
783
+ #
784
+ # @api private
785
+ def assert_valid_fields(fields, unique)
786
+ valid_properties = model.properties
787
+
788
+ model.descendants.each do |descendant|
789
+ valid_properties += descendant.properties
790
+ end
791
+
792
+ fields.each do |field|
793
+ case field
794
+ when Symbol, String
795
+ unless valid_properties.named?(field)
796
+ raise ArgumentError, "+options[:fields]+ entry #{field.inspect} does not map to a property in #{model}"
797
+ end
798
+ end
799
+ end
800
+ end
801
+
802
+ # Verifies that value of :links option
803
+ # refers to existing associations
804
+ #
805
+ # @api private
806
+ def assert_valid_links(links)
807
+ if links.empty?
808
+ raise ArgumentError, '+options[:links]+ should not be empty'
809
+ end
810
+
811
+ links.each do |link|
812
+ case link
813
+ when Symbol, String
814
+ unless @relationships.named?(link.to_sym)
815
+ raise ArgumentError, "+options[:links]+ entry #{link.inspect} does not map to a relationship in #{model}"
816
+ end
817
+ end
818
+ end
819
+ end
820
+
821
+ # Verifies that value of :conditions option
822
+ # refers to existing properties
823
+ #
824
+ # @api private
825
+ def assert_valid_conditions(conditions)
826
+ case conditions
827
+ when Hash
828
+ conditions.each do |subject, bind_value|
829
+ case subject
830
+ when Symbol, String
831
+ original = subject
832
+ subject = subject.to_s
833
+ name = subject[0, subject.index('.') || subject.length]
834
+
835
+ unless @properties.named?(name) || @relationships.named?(name)
836
+ raise ArgumentError, "condition #{original.inspect} does not map to a property or relationship in #{model}"
837
+ end
838
+ end
839
+ end
840
+
841
+ when Array
842
+ if conditions.empty?
843
+ raise ArgumentError, '+options[:conditions]+ should not be empty'
844
+ end
845
+
846
+ first_condition = conditions.first
847
+
848
+ unless first_condition.kind_of?(String) && !DataMapper::Ext.blank?(first_condition)
849
+ raise ArgumentError, '+options[:conditions]+ should have a statement for the first entry'
850
+ end
851
+ end
852
+ end
853
+
854
+ # Verifies that query offset is non-negative and only used together with limit
855
+ # @api private
856
+ def assert_valid_offset(offset, limit)
857
+ unless offset >= 0
858
+ raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{offset.inspect}"
859
+ end
860
+
861
+ if offset > 0 && limit.nil?
862
+ raise ArgumentError, '+options[:offset]+ cannot be greater than 0 if limit is not specified'
863
+ end
864
+ end
865
+
866
+ # Verifies the limit is equal to or greater than 0
867
+ #
868
+ # @raise [ArgumentError]
869
+ # raised if the limit is not an Integer or less than 0
870
+ #
871
+ # @api private
872
+ def assert_valid_limit(limit)
873
+ unless limit >= 0
874
+ raise ArgumentError, "+options[:limit]+ must be greater than or equal to 0, but was #{limit.inspect}"
875
+ end
876
+ end
877
+
878
+ # Verifies that :order option uses proper operator and refers
879
+ # to existing property
880
+ #
881
+ # @api private
882
+ def assert_valid_order(order, fields)
883
+ Array(order).each do |order_entry|
884
+ case order_entry
885
+ when Symbol, String
886
+ unless @properties.named?(order_entry)
887
+ raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} does not map to a property in #{model}"
888
+ end
889
+ end
890
+ end
891
+ end
892
+
893
+ # Used to verify value of boolean properties in conditions
894
+ # @api private
895
+ def assert_valid_boolean(name, value)
896
+ if value != true && value != false
897
+ raise ArgumentError, "+#{name}+ should be true or false, but was #{value.inspect}"
898
+ end
899
+ end
900
+
901
+ # Verifies that associations given in conditions belong
902
+ # to the same repository as query's model
903
+ #
904
+ # @api private
905
+ def assert_valid_other(other)
906
+ other_repository = other.repository
907
+ repository = self.repository
908
+ other_class = other.class
909
+
910
+ unless other_repository == repository
911
+ raise ArgumentError, "+other+ #{other_class} must be for the #{repository.name} repository, not #{other_repository.name}"
912
+ end
913
+
914
+ other_model = other.model
915
+ model = self.model
916
+
917
+ unless other_model >= model
918
+ raise ArgumentError, "+other+ #{other_class} must be for the #{model.name} model, not #{other_model.name}"
919
+ end
920
+ end
921
+
922
+ # Handle all the conditions options provided
923
+ #
924
+ # @param [Array<Conditions::AbstractOperation, Conditions::AbstractComparison, Hash, Array>]
925
+ # a list of conditions
926
+ #
927
+ # @return [undefined]
928
+ #
929
+ # @api private
930
+ def merge_conditions(conditions)
931
+ @conditions = Conditions::Operation.new(:and) << @conditions unless @conditions.nil?
932
+
933
+ conditions.compact!
934
+ conditions.each do |condition|
935
+ case condition
936
+ when Conditions::AbstractOperation, Conditions::AbstractComparison
937
+ add_condition(condition)
938
+
939
+ when Hash
940
+ condition.each { |kv| append_condition(*kv) }
941
+
942
+ when Array
943
+ statement, *bind_values = *condition
944
+ raw_condition = [ statement ]
945
+ raw_condition << bind_values if bind_values.size > 0
946
+ add_condition(raw_condition)
947
+ @raw = true
948
+ end
949
+ end
950
+ end
951
+
952
+ # Normalize options
953
+ #
954
+ # @param [Array<Symbol>] options
955
+ # the options to normalize
956
+ #
957
+ # @return [undefined]
958
+ #
959
+ # @api private
960
+ def normalize_options(options = OPTIONS)
961
+ normalize_order if options.include? :order
962
+ normalize_fields if options.include? :fields
963
+ normalize_links if options.include? :links
964
+ normalize_unique if options.include? :unique
965
+ end
966
+
967
+ # Normalize order elements to Query::Direction instances
968
+ #
969
+ # @api private
970
+ def normalize_order
971
+ return if @order.nil?
972
+
973
+ @order = Array(@order).map do |order|
974
+ case order
975
+ when Direction
976
+ order.dup
977
+
978
+ when Operator
979
+ target = order.target
980
+ property = target.kind_of?(Property) ? target : @properties[target]
981
+
982
+ Direction.new(property, order.operator)
983
+
984
+ when Symbol, String
985
+ Direction.new(@properties[order])
986
+
987
+ when Property
988
+ Direction.new(order)
989
+
990
+ when Path
991
+ Direction.new(order.property)
992
+
993
+ else
994
+ order
995
+ end
996
+ end
997
+ end
998
+
999
+ # Normalize fields to Property instances
1000
+ #
1001
+ # @api private
1002
+ def normalize_fields
1003
+ @fields = @fields.map do |field|
1004
+ case field
1005
+ when Symbol, String
1006
+ @properties[field]
1007
+ else
1008
+ field
1009
+ end
1010
+ end
1011
+ end
1012
+
1013
+ # Normalize links to Query::Path
1014
+ #
1015
+ # Normalization means links given as symbols are replaced with
1016
+ # relationships they refer to, intermediate links are "followed"
1017
+ # and duplicates are removed
1018
+ #
1019
+ # @api private
1020
+ def normalize_links
1021
+ stack = @links.dup
1022
+
1023
+ @links.clear
1024
+
1025
+ while link = stack.pop
1026
+ relationship = case link
1027
+ when Symbol, String
1028
+ @relationships[link]
1029
+ else
1030
+ link
1031
+ end
1032
+
1033
+ if relationship.respond_to?(:links)
1034
+ stack.concat(relationship.links)
1035
+ elsif !@links.include?(relationship)
1036
+ @links << relationship
1037
+ end
1038
+ end
1039
+
1040
+ @links.reverse!
1041
+ end
1042
+
1043
+ # Normalize the unique attribute
1044
+ #
1045
+ # If any links are present, and the unique attribute was not
1046
+ # explicitly specified, then make sure the query is marked as unique
1047
+ #
1048
+ # @api private
1049
+ def normalize_unique
1050
+ @unique = links.any? unless @options.key?(:unique)
1051
+ end
1052
+
1053
+ # Append conditions to this Query
1054
+ #
1055
+ # TODO: needs example
1056
+ #
1057
+ # @param [Property, Symbol, String, Operator, Associations::Relationship, Path] subject
1058
+ # the subject to match
1059
+ # @param [Object] bind_value
1060
+ # the value to match on
1061
+ # @param [Symbol] operator
1062
+ # the operator to match with
1063
+ #
1064
+ # @return [Query::Conditions::AbstractOperation]
1065
+ # the Query conditions
1066
+ #
1067
+ # @api private
1068
+ def append_condition(subject, bind_value, model = self.model, operator = :eql)
1069
+ case subject
1070
+ when Property, Associations::Relationship then append_property_condition(subject, bind_value, operator)
1071
+ when Symbol then append_symbol_condition(subject, bind_value, model, operator)
1072
+ when String then append_string_condition(subject, bind_value, model, operator)
1073
+ when Operator then append_operator_conditions(subject, bind_value, model)
1074
+ when Path then append_path(subject, bind_value, model, operator)
1075
+ else
1076
+ raise ArgumentError, "#{subject} is an invalid instance: #{subject.class}"
1077
+ end
1078
+ end
1079
+
1080
+ # @api private
1081
+ def equality_operator_for_type(bind_value)
1082
+ case bind_value
1083
+ when Model, String then :eql
1084
+ when Enumerable then :in
1085
+ when Regexp then :regexp
1086
+ else :eql
1087
+ end
1088
+ end
1089
+
1090
+ # @api private
1091
+ def append_property_condition(subject, bind_value, operator)
1092
+ negated = operator == :not
1093
+
1094
+ if operator == :eql || negated
1095
+ # transform :relationship => nil into :relationship.not => association
1096
+ if subject.respond_to?(:collection_for) && bind_value.nil?
1097
+ negated = !negated
1098
+ bind_value = collection_for_nil(subject)
1099
+ end
1100
+
1101
+ operator = equality_operator_for_type(bind_value)
1102
+ end
1103
+
1104
+ condition = Conditions::Comparison.new(operator, subject, bind_value)
1105
+
1106
+ if negated
1107
+ condition = Conditions::Operation.new(:not, condition)
1108
+ end
1109
+
1110
+ add_condition(condition)
1111
+ end
1112
+
1113
+ # @api private
1114
+ def append_symbol_condition(symbol, bind_value, model, operator)
1115
+ append_condition(symbol.to_s, bind_value, model, operator)
1116
+ end
1117
+
1118
+ # @api private
1119
+ def append_string_condition(string, bind_value, model, operator)
1120
+ if string.include?('.')
1121
+ query_path = model
1122
+
1123
+ target_components = string.split('.')
1124
+ last_component = target_components.last
1125
+ operator = target_components.pop.to_sym if DataMapper::Query::Conditions::Comparison.slugs.any? { |slug| slug.to_s == last_component }
1126
+
1127
+ target_components.each { |method| query_path = query_path.send(method) }
1128
+
1129
+ append_condition(query_path, bind_value, model, operator)
1130
+ else
1131
+ repository_name = repository.name
1132
+ subject = model.properties(repository_name)[string] ||
1133
+ model.relationships(repository_name)[string]
1134
+
1135
+ append_condition(subject, bind_value, model, operator)
1136
+ end
1137
+ end
1138
+
1139
+ # @api private
1140
+ def append_operator_conditions(operator, bind_value, model)
1141
+ append_condition(operator.target, bind_value, model, operator.operator)
1142
+ end
1143
+
1144
+ # @api private
1145
+ def append_path(path, bind_value, model, operator)
1146
+ path.relationships.each do |relationship|
1147
+ inverse = relationship.inverse
1148
+ @links.unshift(inverse) unless @links.include?(inverse)
1149
+ end
1150
+
1151
+ append_condition(path.property, bind_value, path.model, operator)
1152
+ end
1153
+
1154
+ # Add a condition to the Query
1155
+ #
1156
+ # @param [AbstractOperation, AbstractComparison]
1157
+ # the condition to add to the Query
1158
+ #
1159
+ # @return [undefined]
1160
+ #
1161
+ # @api private
1162
+ def add_condition(condition)
1163
+ @conditions = Conditions::Operation.new(:and) if @conditions.nil?
1164
+ @conditions << condition
1165
+ end
1166
+
1167
+ # Extract arguments for #slice and #slice! then return offset and limit
1168
+ #
1169
+ # @param [Integer, Array(Integer), Range] *args the offset,
1170
+ # offset and limit, or range indicating first and last position
1171
+ #
1172
+ # @return [Integer] the offset
1173
+ # @return [Integer, nil] the limit, if any
1174
+ #
1175
+ # @api private
1176
+ def extract_slice_arguments(*args)
1177
+ offset, limit = case args.size
1178
+ when 2 then extract_offset_limit_from_two_arguments(*args)
1179
+ when 1 then extract_offset_limit_from_one_argument(*args)
1180
+ end
1181
+
1182
+ return offset, limit if offset && limit
1183
+
1184
+ raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}"
1185
+ end
1186
+
1187
+ # @api private
1188
+ def extract_offset_limit_from_two_arguments(*args)
1189
+ args if args.all? { |arg| arg.kind_of?(Integer) }
1190
+ end
1191
+
1192
+ # @api private
1193
+ def extract_offset_limit_from_one_argument(arg)
1194
+ case arg
1195
+ when Integer then extract_offset_limit_from_integer(arg)
1196
+ when Range then extract_offset_limit_from_range(arg)
1197
+ end
1198
+ end
1199
+
1200
+ # @api private
1201
+ def extract_offset_limit_from_integer(integer)
1202
+ [ integer, 1 ]
1203
+ end
1204
+
1205
+ # @api private
1206
+ def extract_offset_limit_from_range(range)
1207
+ offset = range.first
1208
+ limit = range.last - offset
1209
+ limit = limit.succ unless range.exclude_end?
1210
+ return offset, limit
1211
+ end
1212
+
1213
+ # @api private
1214
+ def get_relative_position(offset, limit)
1215
+ self_offset = self.offset
1216
+ self_limit = self.limit
1217
+ new_offset = self_offset + offset
1218
+
1219
+ if limit <= 0 || (self_limit && new_offset + limit > self_offset + self_limit)
1220
+ raise RangeError, "offset #{offset} and limit #{limit} are outside allowed range"
1221
+ end
1222
+
1223
+ return new_offset, limit
1224
+ end
1225
+
1226
+ # TODO: DRY this up with conditions
1227
+ # @api private
1228
+ def record_value(record, property)
1229
+ case record
1230
+ when Hash
1231
+ record.fetch(property, record[property.field])
1232
+ when Resource
1233
+ property.get!(record)
1234
+ end
1235
+ end
1236
+
1237
+ # @api private
1238
+ def collection_for_nil(relationship)
1239
+ query = relationship.query.dup
1240
+
1241
+ relationship.target_key.each do |target_key|
1242
+ query[target_key.name.not] = nil if target_key.allow_nil?
1243
+ end
1244
+
1245
+ relationship.target_model.all(query)
1246
+ end
1247
+
1248
+ # @api private
1249
+ def each_comparison
1250
+ operands = conditions.operands.to_a
1251
+
1252
+ while operand = operands.shift
1253
+ if operand.respond_to?(:operands)
1254
+ operands.unshift(*operand.operands)
1255
+ else
1256
+ yield operand
1257
+ end
1258
+ end
1259
+ end
1260
+
1261
+ # Apply a set operation on self and another query
1262
+ #
1263
+ # @param [Symbol] operation
1264
+ # the set operation to apply
1265
+ # @param [Query] other
1266
+ # the other query to apply the set operation on
1267
+ #
1268
+ # @return [Query]
1269
+ # the query that was created for the set operation
1270
+ #
1271
+ # @api private
1272
+ def set_operation(operation, other)
1273
+ assert_valid_other(other)
1274
+ query = self.class.new(@repository, @model, other.to_relative_hash)
1275
+ query.instance_variable_set(:@conditions, other_conditions(other, operation))
1276
+ query
1277
+ end
1278
+
1279
+ # Return the union with another query's conditions
1280
+ #
1281
+ # @param [Query] other
1282
+ # the query conditions to union with
1283
+ #
1284
+ # @return [OrOperation]
1285
+ # the union of the query conditions and other conditions
1286
+ #
1287
+ # @api private
1288
+ def other_conditions(other, operation)
1289
+ self_conditions = query_conditions(self)
1290
+
1291
+ unless self_conditions.kind_of?(Conditions::Operation)
1292
+ operation_slug = case operation
1293
+ when :intersection, :difference then :and
1294
+ when :union then :or
1295
+ end
1296
+
1297
+ self_conditions = Conditions::Operation.new(operation_slug, self_conditions)
1298
+ end
1299
+
1300
+ self_conditions.send(operation, query_conditions(other))
1301
+ end
1302
+
1303
+ # Extract conditions from a Query
1304
+ #
1305
+ # @param [Query] query
1306
+ # the query with conditions
1307
+ #
1308
+ # @return [AbstractOperation]
1309
+ # the operation
1310
+ #
1311
+ # @api private
1312
+ def query_conditions(query)
1313
+ if query.limit || query.links.any?
1314
+ query.to_subquery
1315
+ else
1316
+ query.conditions
1317
+ end
1318
+ end
1319
+
1320
+ # Return a self referrential relationship
1321
+ #
1322
+ # @return [Associations::OneToMany::Relationship]
1323
+ # the 1:m association to the same model
1324
+ #
1325
+ # @api private
1326
+ def self_relationship
1327
+ @self_relationship ||=
1328
+ begin
1329
+ model = self.model
1330
+ Associations::OneToMany::Relationship.new(
1331
+ :self,
1332
+ model,
1333
+ model,
1334
+ self_relationship_options
1335
+ )
1336
+ end
1337
+ end
1338
+
1339
+ # Return options for the self referrential relationship
1340
+ #
1341
+ # @return [Hash]
1342
+ # the options to use with the self referrential relationship
1343
+ #
1344
+ # @api private
1345
+ def self_relationship_options
1346
+ keys = model_key.map { |property| property.name }
1347
+ repository = self.repository
1348
+ {
1349
+ :child_key => keys,
1350
+ :parent_key => keys,
1351
+ :child_repository_name => repository.name,
1352
+ :parent_repository_name => repository.name,
1353
+ }
1354
+ end
1355
+
1356
+ # Return the model key
1357
+ #
1358
+ # @return [PropertySet]
1359
+ # the model key
1360
+ #
1361
+ # @api private
1362
+ def model_key
1363
+ @properties.key
1364
+ end
1365
+ end # class Query
1366
+ end # module DataMapper