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