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