ardm-core 1.2.1

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