ghost_dm-core 1.3.0.beta

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