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