ardm-core 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (259) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +29 -0
  3. data/.document +5 -0
  4. data/.gitignore +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