datamapper-dm-core 0.9.11 → 0.10.0
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 +17 -14
- data/.gitignore +3 -1
- data/FAQ +6 -5
- data/History.txt +5 -39
- data/Manifest.txt +67 -76
- data/QUICKLINKS +1 -1
- data/README.txt +21 -15
- data/Rakefile +16 -15
- data/SPECS +2 -29
- data/TODO +1 -1
- data/dm-core.gemspec +11 -15
- data/lib/dm-core/adapters/abstract_adapter.rb +182 -185
- data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
- data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
- data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
- data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
- data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
- data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
- data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
- data/lib/dm-core/adapters.rb +135 -16
- data/lib/dm-core/associations/many_to_many.rb +372 -90
- data/lib/dm-core/associations/many_to_one.rb +220 -73
- data/lib/dm-core/associations/one_to_many.rb +319 -255
- data/lib/dm-core/associations/one_to_one.rb +66 -53
- data/lib/dm-core/associations/relationship.rb +560 -158
- data/lib/dm-core/collection.rb +1104 -381
- data/lib/dm-core/core_ext/kernel.rb +12 -0
- data/lib/dm-core/core_ext/symbol.rb +10 -0
- data/lib/dm-core/identity_map.rb +4 -34
- data/lib/dm-core/migrations.rb +1283 -0
- data/lib/dm-core/model/descendant_set.rb +81 -0
- data/lib/dm-core/model/hook.rb +45 -0
- data/lib/dm-core/model/is.rb +32 -0
- data/lib/dm-core/model/property.rb +248 -0
- data/lib/dm-core/model/relationship.rb +335 -0
- data/lib/dm-core/model/scope.rb +90 -0
- data/lib/dm-core/model.rb +570 -369
- data/lib/dm-core/property.rb +753 -280
- data/lib/dm-core/property_set.rb +141 -98
- data/lib/dm-core/query/conditions/comparison.rb +814 -0
- data/lib/dm-core/query/conditions/operation.rb +247 -0
- data/lib/dm-core/query/direction.rb +43 -0
- data/lib/dm-core/query/operator.rb +42 -0
- data/lib/dm-core/query/path.rb +102 -0
- data/lib/dm-core/query/sort.rb +45 -0
- data/lib/dm-core/query.rb +974 -492
- data/lib/dm-core/repository.rb +147 -107
- data/lib/dm-core/resource.rb +644 -429
- data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
- data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
- data/lib/dm-core/support/chainable.rb +20 -0
- data/lib/dm-core/support/deprecate.rb +12 -0
- data/lib/dm-core/support/equalizer.rb +23 -0
- data/lib/dm-core/support/logger.rb +13 -0
- data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
- data/lib/dm-core/transaction.rb +333 -92
- data/lib/dm-core/type.rb +98 -60
- data/lib/dm-core/types/boolean.rb +1 -1
- data/lib/dm-core/types/discriminator.rb +34 -20
- data/lib/dm-core/types/object.rb +7 -4
- data/lib/dm-core/types/paranoid_boolean.rb +11 -9
- data/lib/dm-core/types/paranoid_datetime.rb +11 -9
- data/lib/dm-core/types/serial.rb +3 -3
- data/lib/dm-core/types/text.rb +3 -4
- data/lib/dm-core/version.rb +1 -1
- data/lib/dm-core.rb +106 -110
- data/script/performance.rb +102 -109
- data/script/profile.rb +169 -38
- data/spec/lib/adapter_helpers.rb +105 -0
- data/spec/lib/collection_helpers.rb +18 -0
- data/spec/lib/counter_adapter.rb +34 -0
- data/spec/lib/pending_helpers.rb +27 -0
- data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
- data/spec/public/associations/many_to_many_spec.rb +193 -0
- data/spec/public/associations/many_to_one_spec.rb +73 -0
- data/spec/public/associations/one_to_many_spec.rb +77 -0
- data/spec/public/associations/one_to_one_spec.rb +156 -0
- data/spec/public/collection_spec.rb +65 -0
- data/spec/public/model/relationship_spec.rb +924 -0
- data/spec/public/model_spec.rb +159 -0
- data/spec/public/property_spec.rb +829 -0
- data/spec/public/resource_spec.rb +71 -0
- data/spec/public/sel_spec.rb +44 -0
- data/spec/public/setup_spec.rb +145 -0
- data/spec/public/shared/association_collection_shared_spec.rb +317 -0
- data/spec/public/shared/collection_shared_spec.rb +1723 -0
- data/spec/public/shared/finder_shared_spec.rb +1619 -0
- data/spec/public/shared/resource_shared_spec.rb +924 -0
- data/spec/public/shared/sel_shared_spec.rb +112 -0
- data/spec/public/transaction_spec.rb +129 -0
- data/spec/public/types/discriminator_spec.rb +130 -0
- data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
- data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
- data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
- data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
- data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
- data/spec/semipublic/associations/relationship_spec.rb +194 -0
- data/spec/semipublic/associations_spec.rb +177 -0
- data/spec/semipublic/collection_spec.rb +142 -0
- data/spec/semipublic/property_spec.rb +61 -0
- data/spec/semipublic/query/conditions_spec.rb +528 -0
- data/spec/semipublic/query/path_spec.rb +443 -0
- data/spec/semipublic/query_spec.rb +2626 -0
- data/spec/semipublic/resource_spec.rb +47 -0
- data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
- data/spec/spec.opts +3 -1
- data/spec/spec_helper.rb +80 -57
- data/tasks/ci.rb +19 -31
- data/tasks/dm.rb +43 -48
- data/tasks/doc.rb +8 -11
- data/tasks/gemspec.rb +5 -5
- data/tasks/hoe.rb +15 -16
- data/tasks/install.rb +8 -10
- metadata +72 -93
- data/lib/dm-core/associations/relationship_chain.rb +0 -81
- data/lib/dm-core/associations.rb +0 -207
- data/lib/dm-core/auto_migrations.rb +0 -105
- data/lib/dm-core/dependency_queue.rb +0 -32
- data/lib/dm-core/hook.rb +0 -11
- data/lib/dm-core/is.rb +0 -16
- data/lib/dm-core/logger.rb +0 -232
- data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
- data/lib/dm-core/migrator.rb +0 -29
- data/lib/dm-core/scope.rb +0 -58
- data/lib/dm-core/support/array.rb +0 -13
- data/lib/dm-core/support/assertions.rb +0 -8
- data/lib/dm-core/support/errors.rb +0 -23
- data/lib/dm-core/support/kernel.rb +0 -11
- data/lib/dm-core/support/symbol.rb +0 -41
- data/lib/dm-core/support.rb +0 -7
- data/lib/dm-core/type_map.rb +0 -80
- data/lib/dm-core/types.rb +0 -19
- data/script/all +0 -4
- data/spec/integration/association_spec.rb +0 -1382
- data/spec/integration/association_through_spec.rb +0 -203
- data/spec/integration/associations/many_to_many_spec.rb +0 -449
- data/spec/integration/associations/many_to_one_spec.rb +0 -163
- data/spec/integration/associations/one_to_many_spec.rb +0 -188
- data/spec/integration/auto_migrations_spec.rb +0 -413
- data/spec/integration/collection_spec.rb +0 -1073
- data/spec/integration/data_objects_adapter_spec.rb +0 -32
- data/spec/integration/dependency_queue_spec.rb +0 -46
- data/spec/integration/model_spec.rb +0 -197
- data/spec/integration/mysql_adapter_spec.rb +0 -85
- data/spec/integration/postgres_adapter_spec.rb +0 -731
- data/spec/integration/property_spec.rb +0 -253
- data/spec/integration/query_spec.rb +0 -514
- data/spec/integration/repository_spec.rb +0 -61
- data/spec/integration/resource_spec.rb +0 -513
- data/spec/integration/sqlite3_adapter_spec.rb +0 -352
- data/spec/integration/sti_spec.rb +0 -273
- data/spec/integration/strategic_eager_loading_spec.rb +0 -156
- data/spec/integration/transaction_spec.rb +0 -75
- data/spec/integration/type_spec.rb +0 -275
- data/spec/lib/logging_helper.rb +0 -18
- data/spec/lib/mock_adapter.rb +0 -27
- data/spec/lib/model_loader.rb +0 -100
- data/spec/lib/publicize_methods.rb +0 -28
- data/spec/models/content.rb +0 -16
- data/spec/models/vehicles.rb +0 -34
- data/spec/models/zoo.rb +0 -48
- data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
- data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
- data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
- data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
- data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
- data/spec/unit/associations/many_to_many_spec.rb +0 -32
- data/spec/unit/associations/many_to_one_spec.rb +0 -159
- data/spec/unit/associations/one_to_many_spec.rb +0 -393
- data/spec/unit/associations/one_to_one_spec.rb +0 -7
- data/spec/unit/associations/relationship_spec.rb +0 -71
- data/spec/unit/associations_spec.rb +0 -242
- data/spec/unit/auto_migrations_spec.rb +0 -111
- data/spec/unit/collection_spec.rb +0 -182
- data/spec/unit/data_mapper_spec.rb +0 -35
- data/spec/unit/identity_map_spec.rb +0 -126
- data/spec/unit/is_spec.rb +0 -80
- data/spec/unit/migrator_spec.rb +0 -33
- data/spec/unit/model_spec.rb +0 -321
- data/spec/unit/naming_conventions_spec.rb +0 -36
- data/spec/unit/property_set_spec.rb +0 -90
- data/spec/unit/property_spec.rb +0 -753
- data/spec/unit/query_spec.rb +0 -571
- data/spec/unit/repository_spec.rb +0 -93
- data/spec/unit/resource_spec.rb +0 -649
- data/spec/unit/scope_spec.rb +0 -142
- data/spec/unit/transaction_spec.rb +0 -493
- data/spec/unit/type_map_spec.rb +0 -114
- data/spec/unit/type_spec.rb +0 -119
data/lib/dm-core/query.rb
CHANGED
|
@@ -1,139 +1,525 @@
|
|
|
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
|
+
|
|
1
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
|
+
#
|
|
2
28
|
class Query
|
|
3
|
-
include Assertions
|
|
29
|
+
include Extlib::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
|
+
source_values = []
|
|
51
|
+
|
|
52
|
+
if source.nil?
|
|
53
|
+
source_values << [ nil ] * target_key.size
|
|
54
|
+
else
|
|
55
|
+
Array(source).each do |resource|
|
|
56
|
+
next unless source_key.loaded?(resource)
|
|
57
|
+
source_values << source_key.get!(resource)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
source_values.uniq!
|
|
62
|
+
|
|
63
|
+
if target_key.size == 1
|
|
64
|
+
target_key = target_key.first
|
|
65
|
+
source_values.flatten!
|
|
66
|
+
|
|
67
|
+
if source_values.size == 1
|
|
68
|
+
Conditions::EqualToComparison.new(target_key, source_values.first)
|
|
69
|
+
else
|
|
70
|
+
Conditions::InclusionComparison.new(target_key, source_values)
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
or_operation = Conditions::OrOperation.new
|
|
74
|
+
|
|
75
|
+
source_values.each do |source_value|
|
|
76
|
+
and_operation = Conditions::AndOperation.new
|
|
77
|
+
|
|
78
|
+
target_key.zip(source_value) do |property, value|
|
|
79
|
+
and_operation << Conditions::EqualToComparison.new(property, value)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
or_operation << and_operation
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
or_operation
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns the repository query should be
|
|
90
|
+
# executed in
|
|
91
|
+
#
|
|
92
|
+
# Set in cases like the following:
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
#
|
|
96
|
+
# Document.all(:repository => :medline)
|
|
97
|
+
#
|
|
98
|
+
#
|
|
99
|
+
# @return [Repository]
|
|
100
|
+
# the Repository to retrieve results from
|
|
101
|
+
#
|
|
102
|
+
# @api semipublic
|
|
103
|
+
attr_reader :repository
|
|
104
|
+
|
|
105
|
+
# Returns model (class) that is used
|
|
106
|
+
# to instantiate objects from query result
|
|
107
|
+
# returned by adapter
|
|
108
|
+
#
|
|
109
|
+
# @return [Model]
|
|
110
|
+
# the Model to retrieve results from
|
|
111
|
+
#
|
|
112
|
+
# @api semipublic
|
|
113
|
+
attr_reader :model
|
|
114
|
+
|
|
115
|
+
# Returns the fields
|
|
116
|
+
#
|
|
117
|
+
# Set in cases like the following:
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
#
|
|
121
|
+
# Document.all(:fields => [:title, :vernacular_title, :abstract])
|
|
122
|
+
#
|
|
123
|
+
# @return [PropertySet]
|
|
124
|
+
# the properties in the Model that will be retrieved
|
|
125
|
+
#
|
|
126
|
+
# @api semipublic
|
|
127
|
+
attr_reader :fields
|
|
128
|
+
|
|
129
|
+
# Returns the links (associations) query fetches
|
|
130
|
+
#
|
|
131
|
+
# @return [Array<DataMapper::Associations::Relationship>]
|
|
132
|
+
# the relationships that will be used to scope the results
|
|
133
|
+
#
|
|
134
|
+
# @api private
|
|
135
|
+
attr_reader :links
|
|
136
|
+
|
|
137
|
+
# Returns the conditions of the query
|
|
138
|
+
#
|
|
139
|
+
# In the following example:
|
|
140
|
+
#
|
|
141
|
+
# @example
|
|
142
|
+
#
|
|
143
|
+
# Team.all(:wins.gt => 30, :conference => 'East')
|
|
144
|
+
#
|
|
145
|
+
# Conditions are "greater than" operator for "wins"
|
|
146
|
+
# field and exact match operator for "conference".
|
|
147
|
+
#
|
|
148
|
+
# @return [Array]
|
|
149
|
+
# the conditions that will be used to scope the results
|
|
150
|
+
#
|
|
151
|
+
# @api semipublic
|
|
152
|
+
attr_reader :conditions
|
|
4
153
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
154
|
+
# Returns the offset query uses
|
|
155
|
+
#
|
|
156
|
+
# Set in cases like the following:
|
|
157
|
+
#
|
|
158
|
+
# @example
|
|
159
|
+
#
|
|
160
|
+
# Document.all(:offset => page.offset)
|
|
161
|
+
#
|
|
162
|
+
# @return [Integer]
|
|
163
|
+
# the offset of the results
|
|
164
|
+
#
|
|
165
|
+
# @api semipublic
|
|
166
|
+
attr_reader :offset
|
|
167
|
+
|
|
168
|
+
# Returns the limit query uses
|
|
169
|
+
#
|
|
170
|
+
# Set in cases like the following:
|
|
171
|
+
#
|
|
172
|
+
# @example
|
|
173
|
+
#
|
|
174
|
+
# Document.all(:limit => 10)
|
|
175
|
+
#
|
|
176
|
+
# @return [Integer, NilClass]
|
|
177
|
+
# the maximum number of results
|
|
178
|
+
#
|
|
179
|
+
# @api semipublic
|
|
180
|
+
attr_reader :limit
|
|
181
|
+
|
|
182
|
+
# Returns the order
|
|
183
|
+
#
|
|
184
|
+
# Set in cases like the following:
|
|
185
|
+
#
|
|
186
|
+
# @example
|
|
187
|
+
#
|
|
188
|
+
# Document.all(:order => [:created_at.desc, :length.desc])
|
|
189
|
+
#
|
|
190
|
+
# query order is a set of two ordering rules, descending on
|
|
191
|
+
# "created_at" field and descending again on "length" field
|
|
192
|
+
#
|
|
193
|
+
# @return [Array]
|
|
194
|
+
# the order of results
|
|
195
|
+
#
|
|
196
|
+
# @api semipublic
|
|
197
|
+
attr_reader :order
|
|
198
|
+
|
|
199
|
+
# Returns the original options
|
|
200
|
+
#
|
|
201
|
+
# @return [Hash]
|
|
202
|
+
# the original options
|
|
203
|
+
#
|
|
204
|
+
# @api private
|
|
205
|
+
attr_reader :options
|
|
8
206
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
207
|
+
# Indicates if each result should be returned in reverse order
|
|
208
|
+
#
|
|
209
|
+
# Set in cases like the following:
|
|
210
|
+
#
|
|
211
|
+
# @example
|
|
212
|
+
#
|
|
213
|
+
# Document.all(:limit => 5).reverse
|
|
214
|
+
#
|
|
215
|
+
# Note that :add_reversed option may be used in conditions directly,
|
|
216
|
+
# but this is rarely the case
|
|
217
|
+
#
|
|
218
|
+
# @return [Boolean]
|
|
219
|
+
# true if the results should be reversed, false if not
|
|
220
|
+
#
|
|
221
|
+
# @api private
|
|
222
|
+
def add_reversed?
|
|
223
|
+
@add_reversed
|
|
224
|
+
end
|
|
12
225
|
|
|
226
|
+
# Indicates if the Query results should replace the results in the Identity Map
|
|
227
|
+
#
|
|
228
|
+
# TODO: needs example
|
|
229
|
+
#
|
|
230
|
+
# @return [Boolean]
|
|
231
|
+
# true if the results should be reloaded, false if not
|
|
232
|
+
#
|
|
233
|
+
# @api semipublic
|
|
13
234
|
def reload?
|
|
14
235
|
@reload
|
|
15
236
|
end
|
|
16
237
|
|
|
238
|
+
# Indicates if the Query results should be unique
|
|
239
|
+
#
|
|
240
|
+
# TODO: needs example
|
|
241
|
+
#
|
|
242
|
+
# @return [Boolean]
|
|
243
|
+
# true if the results should be unique, false if not
|
|
244
|
+
#
|
|
245
|
+
# @api semipublic
|
|
17
246
|
def unique?
|
|
18
247
|
@unique
|
|
19
248
|
end
|
|
20
249
|
|
|
250
|
+
# Indicates if the Query has raw conditions
|
|
251
|
+
#
|
|
252
|
+
# @return [Boolean]
|
|
253
|
+
# true if the query has raw conditions, false if not
|
|
254
|
+
#
|
|
255
|
+
# @api semipublic
|
|
256
|
+
def raw?
|
|
257
|
+
@raw
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Indicates if the Query is valid
|
|
261
|
+
#
|
|
262
|
+
# @return [Boolean]
|
|
263
|
+
# true if the query is valid
|
|
264
|
+
#
|
|
265
|
+
# @api semipublic
|
|
266
|
+
def valid?
|
|
267
|
+
conditions.valid?
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Returns a new Query with a reversed order
|
|
271
|
+
#
|
|
272
|
+
# @example
|
|
273
|
+
#
|
|
274
|
+
# Document.all(:limit => 5).reverse
|
|
275
|
+
#
|
|
276
|
+
# Will execute a single query with correct order
|
|
277
|
+
#
|
|
278
|
+
# @return [Query]
|
|
279
|
+
# new Query with reversed order
|
|
280
|
+
#
|
|
281
|
+
# @api semipublic
|
|
21
282
|
def reverse
|
|
22
283
|
dup.reverse!
|
|
23
284
|
end
|
|
24
285
|
|
|
286
|
+
# Reverses the sort order of the Query
|
|
287
|
+
#
|
|
288
|
+
# @example
|
|
289
|
+
#
|
|
290
|
+
# Document.all(:limit => 5).reverse
|
|
291
|
+
#
|
|
292
|
+
# Will execute a single query with original order
|
|
293
|
+
# and then reverse collection in the Ruby space
|
|
294
|
+
#
|
|
295
|
+
# @return [Query]
|
|
296
|
+
# self
|
|
297
|
+
#
|
|
298
|
+
# @api semipublic
|
|
25
299
|
def reverse!
|
|
26
300
|
# reverse the sort order
|
|
27
|
-
|
|
301
|
+
@order.map! { |direction| direction.reverse! }
|
|
302
|
+
|
|
303
|
+
# copy the order to the options
|
|
304
|
+
@options = @options.merge(:order => @order.map { |direction| direction.dup }).freeze
|
|
28
305
|
|
|
29
306
|
self
|
|
30
307
|
end
|
|
31
308
|
|
|
309
|
+
# Updates the Query with another Query or conditions
|
|
310
|
+
#
|
|
311
|
+
# Pretty unrealistic example:
|
|
312
|
+
#
|
|
313
|
+
# @example
|
|
314
|
+
#
|
|
315
|
+
# Journal.all(:limit => 2).query.limit # => 2
|
|
316
|
+
# Journal.all(:limit => 2).query.update(:limit => 3).limit # => 3
|
|
317
|
+
#
|
|
318
|
+
# @param [Query, Hash] other
|
|
319
|
+
# other Query or conditions
|
|
320
|
+
#
|
|
321
|
+
# @return [Query]
|
|
322
|
+
# self
|
|
323
|
+
#
|
|
324
|
+
# @api semipublic
|
|
32
325
|
def update(other)
|
|
33
326
|
assert_kind_of 'other', other, self.class, Hash
|
|
34
327
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
328
|
+
other_options = if other.kind_of? self.class
|
|
329
|
+
if self.eql?(other)
|
|
330
|
+
return self
|
|
331
|
+
end
|
|
332
|
+
assert_valid_other(other)
|
|
333
|
+
other.options
|
|
334
|
+
else
|
|
335
|
+
other
|
|
40
336
|
end
|
|
41
337
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@unique = other.unique? unless other.unique? == false
|
|
50
|
-
@offset = other.offset if other.reload? || other.offset != 0
|
|
51
|
-
@limit = other.limit unless other.limit == nil
|
|
52
|
-
@order = other.order unless other.order == model.default_order
|
|
53
|
-
@add_reversed = other.add_reversed? unless other.add_reversed? == false
|
|
54
|
-
@fields = other.fields unless other.fields == @properties.defaults
|
|
55
|
-
@links = other.links unless other.links == []
|
|
56
|
-
@includes = other.includes unless other.includes == []
|
|
57
|
-
|
|
58
|
-
update_conditions(other)
|
|
338
|
+
unless other_options.empty?
|
|
339
|
+
options = @options.merge(other_options)
|
|
340
|
+
if @options[:conditions] and other_options[:conditions]
|
|
341
|
+
options[:conditions] = @options[:conditions].dup << other_options[:conditions]
|
|
342
|
+
end
|
|
343
|
+
initialize(repository, model, options)
|
|
344
|
+
end
|
|
59
345
|
|
|
60
346
|
self
|
|
61
347
|
end
|
|
62
348
|
|
|
349
|
+
# Similar to Query#update, but acts on a duplicate.
|
|
350
|
+
#
|
|
351
|
+
# @param [Query, Hash] other
|
|
352
|
+
# other query to merge with
|
|
353
|
+
#
|
|
354
|
+
# @return [Query]
|
|
355
|
+
# updated duplicate of original query
|
|
356
|
+
#
|
|
357
|
+
# @api semipublic
|
|
63
358
|
def merge(other)
|
|
64
359
|
dup.update(other)
|
|
65
360
|
end
|
|
66
361
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
bind_values = []
|
|
90
|
-
conditions.each do |tuple|
|
|
91
|
-
next if tuple.size == 2
|
|
92
|
-
operator, property, bind_value = *tuple
|
|
93
|
-
if :raw == operator
|
|
94
|
-
bind_values.push(*bind_value)
|
|
95
|
-
else
|
|
96
|
-
bind_values << bind_value
|
|
97
|
-
end
|
|
362
|
+
# Builds and returns new query that merges
|
|
363
|
+
# original with one given, and slices the result
|
|
364
|
+
# with respect to :limit and :offset options
|
|
365
|
+
#
|
|
366
|
+
# This method is used by Collection to
|
|
367
|
+
# concatenate options from multiple chained
|
|
368
|
+
# calls in cases like the following:
|
|
369
|
+
#
|
|
370
|
+
# @example
|
|
371
|
+
#
|
|
372
|
+
# author.books.all(:year => 2009).all(:published => false)
|
|
373
|
+
#
|
|
374
|
+
# @api semipublic
|
|
375
|
+
def relative(options)
|
|
376
|
+
assert_kind_of 'options', options, Hash
|
|
377
|
+
|
|
378
|
+
options = options.dup
|
|
379
|
+
|
|
380
|
+
repository = options.delete(:repository) || self.repository
|
|
381
|
+
|
|
382
|
+
if repository.kind_of?(Symbol)
|
|
383
|
+
repository = DataMapper.repository(repository)
|
|
98
384
|
end
|
|
99
|
-
bind_values
|
|
100
|
-
end
|
|
101
385
|
|
|
102
|
-
|
|
103
|
-
|
|
386
|
+
if options.key?(:offset) && (options.key?(:limit) || self.limit)
|
|
387
|
+
offset = options.delete(:offset)
|
|
388
|
+
limit = options.delete(:limit) || self.limit - offset
|
|
389
|
+
|
|
390
|
+
self.class.new(repository, model, @options.merge(options)).slice!(offset, limit)
|
|
391
|
+
else
|
|
392
|
+
self.class.new(repository, model, @options.merge(options))
|
|
393
|
+
end
|
|
104
394
|
end
|
|
105
395
|
|
|
106
|
-
|
|
107
|
-
|
|
396
|
+
# Takes an Enumerable of records, and destructively filters it.
|
|
397
|
+
# First finds all matching conditions, then sorts it,
|
|
398
|
+
# then does offset & limit
|
|
399
|
+
#
|
|
400
|
+
# @param [Enumerable] records
|
|
401
|
+
# The set of records to be filtered
|
|
402
|
+
#
|
|
403
|
+
# @return [Enumerable]
|
|
404
|
+
# Whats left of the given array after the filtering
|
|
405
|
+
#
|
|
406
|
+
# @api semipublic
|
|
407
|
+
def filter_records(records)
|
|
408
|
+
records = records.uniq if unique?
|
|
409
|
+
records = match_records(records)
|
|
410
|
+
records = sort_records(records)
|
|
411
|
+
records = limit_records(records)
|
|
412
|
+
records
|
|
108
413
|
end
|
|
109
414
|
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
415
|
+
# Filter a set of records by the conditions
|
|
416
|
+
#
|
|
417
|
+
# @param [Enumerable] records
|
|
418
|
+
# The set of records to be filtered
|
|
419
|
+
#
|
|
420
|
+
# @return [Enumerable]
|
|
421
|
+
# Whats left of the given array after the matching
|
|
422
|
+
#
|
|
423
|
+
# @api semipublic
|
|
424
|
+
def match_records(records)
|
|
425
|
+
return records if conditions.nil?
|
|
426
|
+
records.select do |record|
|
|
427
|
+
conditions.matches?(record)
|
|
114
428
|
end
|
|
115
429
|
end
|
|
116
430
|
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
431
|
+
# Sorts a list of Records by the order
|
|
432
|
+
#
|
|
433
|
+
# @param [Enumerable] records
|
|
434
|
+
# A list of Resources to sort
|
|
120
435
|
#
|
|
121
|
-
|
|
122
|
-
|
|
436
|
+
# @return [Enumerable]
|
|
437
|
+
# The sorted records
|
|
438
|
+
#
|
|
439
|
+
# @api semipublic
|
|
440
|
+
def sort_records(records)
|
|
441
|
+
sort_order = order.map { |direction| [ direction.target, direction.operator == :asc ] }
|
|
123
442
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
value.conditions.each do |subquery_tuple|
|
|
128
|
-
new_conditions << subquery_tuple
|
|
129
|
-
end
|
|
130
|
-
else
|
|
131
|
-
new_conditions << tuple
|
|
443
|
+
records.sort_by do |record|
|
|
444
|
+
sort_order.map do |(property, ascending)|
|
|
445
|
+
Sort.new(record_value(record, property), ascending)
|
|
132
446
|
end
|
|
133
447
|
end
|
|
134
|
-
@conditions = new_conditions
|
|
135
448
|
end
|
|
136
449
|
|
|
450
|
+
# Limits a set of records by the offset and/or limit
|
|
451
|
+
#
|
|
452
|
+
# @param [Enumerable] records
|
|
453
|
+
# A list of Recrods to sort
|
|
454
|
+
#
|
|
455
|
+
# @return [Enumerable]
|
|
456
|
+
# The offset & limited records
|
|
457
|
+
#
|
|
458
|
+
# @api semipublic
|
|
459
|
+
def limit_records(records)
|
|
460
|
+
size = records.size
|
|
461
|
+
|
|
462
|
+
if offset > size - 1
|
|
463
|
+
[]
|
|
464
|
+
elsif (limit && limit != size) || offset > 0
|
|
465
|
+
records[offset, limit || size] || []
|
|
466
|
+
else
|
|
467
|
+
records.dup
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Slices collection by adding limit and offset to the
|
|
472
|
+
# query, so a single query is executed
|
|
473
|
+
#
|
|
474
|
+
# @example
|
|
475
|
+
#
|
|
476
|
+
# Journal.all(:limit => 10).slice(3, 5)
|
|
477
|
+
#
|
|
478
|
+
# will execute query with the following limit and offset
|
|
479
|
+
# (when repository uses DataObjects adapter, and thus
|
|
480
|
+
# queries use SQL):
|
|
481
|
+
#
|
|
482
|
+
# LIMIT 5 OFFSET 3
|
|
483
|
+
#
|
|
484
|
+
# @api semipublic
|
|
485
|
+
def slice(*args)
|
|
486
|
+
dup.slice!(*args)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
alias [] slice
|
|
490
|
+
|
|
491
|
+
# Slices collection by adding limit and offset to the
|
|
492
|
+
# query, so a single query is executed
|
|
493
|
+
#
|
|
494
|
+
# @example
|
|
495
|
+
#
|
|
496
|
+
# Journal.all(:limit => 10).slice!(3, 5)
|
|
497
|
+
#
|
|
498
|
+
# will execute query with the following limit
|
|
499
|
+
# (when repository uses DataObjects adapter, and thus
|
|
500
|
+
# queries use SQL):
|
|
501
|
+
#
|
|
502
|
+
# LIMIT 10
|
|
503
|
+
#
|
|
504
|
+
# and then takes a slice of collection in the Ruby space
|
|
505
|
+
#
|
|
506
|
+
# @api semipublic
|
|
507
|
+
def slice!(*args)
|
|
508
|
+
offset, limit = extract_slice_arguments(*args)
|
|
509
|
+
|
|
510
|
+
if self.limit || self.offset > 0
|
|
511
|
+
offset, limit = get_relative_position(offset, limit)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
update(:offset => offset, :limit => limit)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Returns detailed human readable
|
|
518
|
+
# string representation of the query
|
|
519
|
+
#
|
|
520
|
+
# @return [String] detailed string representation of the query
|
|
521
|
+
#
|
|
522
|
+
# @api semipublic
|
|
137
523
|
def inspect
|
|
138
524
|
attrs = [
|
|
139
525
|
[ :repository, repository.name ],
|
|
@@ -148,529 +534,625 @@ module DataMapper
|
|
|
148
534
|
[ :unique, unique? ],
|
|
149
535
|
]
|
|
150
536
|
|
|
151
|
-
"#<#{self.class.name} #{attrs.map { |
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# TODO: add docs
|
|
155
|
-
# @api public
|
|
156
|
-
def to_hash
|
|
157
|
-
hash = {
|
|
158
|
-
:reload => reload?,
|
|
159
|
-
:unique => unique?,
|
|
160
|
-
:offset => offset,
|
|
161
|
-
:order => order,
|
|
162
|
-
:add_reversed => add_reversed?,
|
|
163
|
-
:fields => fields,
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
hash[:limit] = limit unless limit == nil
|
|
167
|
-
hash[:links] = links unless links == []
|
|
168
|
-
hash[:includes] = includes unless includes == []
|
|
169
|
-
|
|
170
|
-
conditions = {}
|
|
171
|
-
raw_queries = []
|
|
172
|
-
bind_values = []
|
|
173
|
-
|
|
174
|
-
conditions.each do |condition|
|
|
175
|
-
if condition[0] == :raw
|
|
176
|
-
raw_queries << condition[1]
|
|
177
|
-
bind_values << condition[2]
|
|
178
|
-
else
|
|
179
|
-
operator, property, bind_value = condition
|
|
180
|
-
conditions[ Query::Operator.new(property, operator) ] = bind_value
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
if raw_queries.any?
|
|
185
|
-
hash[:conditions] = [ raw_queries.join(' ') ].concat(bind_values)
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
hash.update(conditions)
|
|
537
|
+
"#<#{self.class.name} #{attrs.map { |key, value| "@#{key}=#{value.inspect}" }.join(' ')}>"
|
|
189
538
|
end
|
|
190
539
|
|
|
191
|
-
#
|
|
540
|
+
# Get the properties used in the conditions
|
|
541
|
+
#
|
|
542
|
+
# @return [Set<Property>]
|
|
543
|
+
# Set of properties used in the conditions
|
|
544
|
+
#
|
|
192
545
|
# @api private
|
|
193
|
-
def
|
|
194
|
-
|
|
546
|
+
def condition_properties
|
|
547
|
+
properties = Set.new
|
|
548
|
+
|
|
549
|
+
each_comparison do |comparison|
|
|
550
|
+
properties << comparison.subject if comparison.subject.kind_of?(Property)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
properties
|
|
195
554
|
end
|
|
196
555
|
|
|
197
|
-
#
|
|
556
|
+
# Return a list of fields in predictable order
|
|
557
|
+
#
|
|
558
|
+
# @return [Array<Property>]
|
|
559
|
+
# list of fields sorted in deterministic order
|
|
560
|
+
#
|
|
198
561
|
# @api private
|
|
199
|
-
def
|
|
200
|
-
|
|
562
|
+
def sorted_fields
|
|
563
|
+
fields.sort_by { |property| property.hash }
|
|
201
564
|
end
|
|
202
565
|
|
|
203
566
|
private
|
|
204
567
|
|
|
568
|
+
# Initializes a Query instance
|
|
569
|
+
#
|
|
570
|
+
# @example
|
|
571
|
+
#
|
|
572
|
+
# JournalIssue.all(:repository => :medline, :created_on.gte => Date.today - 7)
|
|
573
|
+
#
|
|
574
|
+
# initialized a query with repository defined with name :medline,
|
|
575
|
+
# model JournalIssue and options { :created_on.gte => Date.today - 7 }
|
|
576
|
+
#
|
|
577
|
+
# @param [Repository] repository
|
|
578
|
+
# the Repository to retrieve results from
|
|
579
|
+
# @param [Model] model
|
|
580
|
+
# the Model to retrieve results from
|
|
581
|
+
# @param [Hash] options
|
|
582
|
+
# the conditions and scope
|
|
583
|
+
#
|
|
584
|
+
# @api semipublic
|
|
205
585
|
def initialize(repository, model, options = {})
|
|
206
586
|
assert_kind_of 'repository', repository, Repository
|
|
207
587
|
assert_kind_of 'model', model, Model
|
|
208
|
-
assert_kind_of 'options', options, Hash
|
|
209
588
|
|
|
210
|
-
|
|
589
|
+
@repository = repository
|
|
590
|
+
@model = model
|
|
591
|
+
@options = options.dup.freeze
|
|
592
|
+
|
|
593
|
+
repository_name = repository.name
|
|
211
594
|
|
|
212
|
-
|
|
595
|
+
@properties = @model.properties(repository_name)
|
|
596
|
+
@relationships = @model.relationships(repository_name)
|
|
213
597
|
|
|
214
|
-
@
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
@
|
|
218
|
-
@
|
|
219
|
-
@
|
|
220
|
-
@
|
|
221
|
-
@
|
|
222
|
-
@
|
|
223
|
-
@add_reversed = options.fetch :add_reversed, false
|
|
224
|
-
@
|
|
225
|
-
@
|
|
226
|
-
|
|
227
|
-
@
|
|
228
|
-
|
|
229
|
-
# normalize order and fields
|
|
230
|
-
@order = normalize_order(@order)
|
|
231
|
-
@fields = normalize_fields(@fields)
|
|
232
|
-
|
|
233
|
-
# XXX: should I validate that each property in @order corresponds
|
|
234
|
-
# to something in @fields? Many DB engines require they match,
|
|
235
|
-
# and I can think of no valid queries where a field would be so
|
|
236
|
-
# important that you sort on it, but not important enough to
|
|
237
|
-
# return.
|
|
238
|
-
|
|
239
|
-
# normalize links and includes.
|
|
240
|
-
# NOTE: this must be done after order and fields
|
|
241
|
-
@links = normalize_links(@links)
|
|
242
|
-
@includes = normalize_includes(@includes)
|
|
598
|
+
assert_valid_options(@options)
|
|
599
|
+
|
|
600
|
+
@fields = @options.fetch :fields, @properties.defaults
|
|
601
|
+
@links = @options.fetch :links, []
|
|
602
|
+
@conditions = Conditions::Operation.new(:null)
|
|
603
|
+
@offset = @options.fetch :offset, 0
|
|
604
|
+
@limit = @options.fetch :limit, nil
|
|
605
|
+
@order = @options.fetch :order, @model.default_order(repository_name)
|
|
606
|
+
@unique = @options.fetch :unique, false
|
|
607
|
+
@add_reversed = @options.fetch :add_reversed, false
|
|
608
|
+
@reload = @options.fetch :reload, false
|
|
609
|
+
@raw = false
|
|
610
|
+
|
|
611
|
+
@links = @links.dup
|
|
243
612
|
|
|
244
613
|
# treat all non-options as conditions
|
|
245
|
-
|
|
246
|
-
append_condition(k, options[k])
|
|
247
|
-
end
|
|
614
|
+
@options.except(*OPTIONS).each { |kv| append_condition(*kv) }
|
|
248
615
|
|
|
249
|
-
# parse
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
conditions
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
end
|
|
262
|
-
end
|
|
616
|
+
# parse @options[:conditions] differently
|
|
617
|
+
case conditions = @options[:conditions]
|
|
618
|
+
when Conditions::AbstractOperation, Conditions::AbstractComparison
|
|
619
|
+
add_condition(conditions)
|
|
620
|
+
|
|
621
|
+
when Hash
|
|
622
|
+
conditions.each { |kv| append_condition(*kv) }
|
|
623
|
+
|
|
624
|
+
when Array
|
|
625
|
+
statement, *bind_values = *conditions
|
|
626
|
+
add_condition([ statement, bind_values ])
|
|
627
|
+
@raw = true
|
|
263
628
|
end
|
|
629
|
+
|
|
630
|
+
normalize_order
|
|
631
|
+
normalize_fields
|
|
632
|
+
normalize_links
|
|
264
633
|
end
|
|
265
634
|
|
|
635
|
+
# Copying contructor, called for Query#dup
|
|
636
|
+
#
|
|
637
|
+
# @api semipublic
|
|
266
638
|
def initialize_copy(original)
|
|
267
|
-
|
|
268
|
-
@conditions = original.conditions.map { |tuple| tuple.dup }
|
|
639
|
+
initialize(original.repository, original.model, original.options)
|
|
269
640
|
end
|
|
270
641
|
|
|
271
|
-
#
|
|
642
|
+
# Validate the options
|
|
643
|
+
#
|
|
644
|
+
# @param [#each] options
|
|
645
|
+
# the options to validate
|
|
646
|
+
#
|
|
647
|
+
# @raise [ArgumentError]
|
|
648
|
+
# if any pairs in +options+ are invalid options
|
|
649
|
+
#
|
|
650
|
+
# @api private
|
|
272
651
|
def assert_valid_options(options)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
options.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
652
|
+
assert_kind_of 'options', options, Hash
|
|
653
|
+
|
|
654
|
+
options.each do |attribute, value|
|
|
655
|
+
case attribute
|
|
656
|
+
when :fields then assert_valid_fields(value, options[:unique])
|
|
657
|
+
when :links then assert_valid_links(value)
|
|
658
|
+
when :conditions then assert_valid_conditions(value)
|
|
659
|
+
when :offset then assert_valid_offset(value, options[:limit])
|
|
660
|
+
when :limit then assert_valid_limit(value)
|
|
661
|
+
when :order then assert_valid_order(value, options[:fields])
|
|
662
|
+
when :unique, :add_reversed, :reload then assert_valid_boolean("options[:#{attribute}]", value)
|
|
663
|
+
else
|
|
664
|
+
assert_valid_conditions(attribute => value)
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
end
|
|
282
668
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
raise ArgumentError, "+options[:limit]+ must be greater than or equal to 1, but was #{options[:limit].inspect}", caller(2)
|
|
290
|
-
end
|
|
669
|
+
# Verifies that value of :fields option
|
|
670
|
+
# refers to existing properties
|
|
671
|
+
#
|
|
672
|
+
# @api private
|
|
673
|
+
def assert_valid_fields(fields, unique)
|
|
674
|
+
assert_kind_of 'options[:fields]', fields, Array
|
|
291
675
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
elsif attribute == :order
|
|
302
|
-
if options[:fields] && options[:fields].any? { |p| !p.kind_of?(Operator) }
|
|
303
|
-
raise ArgumentError, '+options[:order]+ cannot be empty if +options[:fields] contains a non-operator', caller(2)
|
|
304
|
-
end
|
|
305
|
-
else
|
|
306
|
-
raise ArgumentError, "+options[:#{attribute}]+ cannot be empty", caller(2)
|
|
676
|
+
if fields.empty? && unique == false
|
|
677
|
+
raise ArgumentError, '+options[:fields]+ should not be empty if +options[:unique]+ is false'
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
fields.each do |field|
|
|
681
|
+
case field
|
|
682
|
+
when Symbol, String
|
|
683
|
+
unless @properties.named?(field)
|
|
684
|
+
raise ArgumentError, "+options[:fields]+ entry #{field.inspect} does not map to a property in #{model}"
|
|
307
685
|
end
|
|
308
|
-
end
|
|
309
686
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
687
|
+
when Property
|
|
688
|
+
unless @properties.include?(field)
|
|
689
|
+
raise ArgumentError, "+options[:field]+ entry #{field.name.inspect} does not map to a property in #{model}"
|
|
690
|
+
end
|
|
313
691
|
|
|
314
|
-
|
|
315
|
-
raise ArgumentError,
|
|
316
|
-
end
|
|
692
|
+
else
|
|
693
|
+
raise ArgumentError, "+options[:fields]+ entry #{field.inspect} of an unsupported object #{field.class}"
|
|
317
694
|
end
|
|
318
695
|
end
|
|
319
696
|
end
|
|
320
697
|
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
|
|
698
|
+
# Verifies that value of :links option
|
|
699
|
+
# refers to existing associations
|
|
700
|
+
#
|
|
701
|
+
# @api private
|
|
702
|
+
def assert_valid_links(links)
|
|
703
|
+
assert_kind_of 'options[:links]', links, Array
|
|
324
704
|
|
|
325
|
-
|
|
326
|
-
raise ArgumentError,
|
|
705
|
+
if links.empty?
|
|
706
|
+
raise ArgumentError, '+options[:links]+ should not be empty'
|
|
327
707
|
end
|
|
328
708
|
|
|
329
|
-
|
|
330
|
-
|
|
709
|
+
links.each do |link|
|
|
710
|
+
case link
|
|
711
|
+
when Symbol, String
|
|
712
|
+
unless @relationships.key?(link.to_sym)
|
|
713
|
+
raise ArgumentError, "+options[:links]+ entry #{link.inspect} does not map to a relationship in #{model}"
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
when Associations::Relationship
|
|
717
|
+
# TODO: figure out how to validate links from other models
|
|
718
|
+
#unless @relationships.value?(link)
|
|
719
|
+
# raise ArgumentError, "+options[:links]+ entry #{link.name.inspect} does not map to a relationship in #{model}"
|
|
720
|
+
#end
|
|
721
|
+
|
|
722
|
+
else
|
|
723
|
+
raise ArgumentError, "+options[:links]+ entry #{link.inspect} of an unsupported object #{link.class}"
|
|
724
|
+
end
|
|
331
725
|
end
|
|
332
726
|
end
|
|
333
727
|
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
728
|
+
# Verifies that value of :conditions option
|
|
729
|
+
# refers to existing properties
|
|
730
|
+
#
|
|
731
|
+
# @api private
|
|
732
|
+
def assert_valid_conditions(conditions)
|
|
733
|
+
assert_kind_of 'options[:conditions]', conditions, Conditions::AbstractOperation, Conditions::AbstractComparison, Hash, Array
|
|
734
|
+
|
|
735
|
+
case conditions
|
|
736
|
+
when Hash
|
|
737
|
+
conditions.each do |subject, bind_value|
|
|
738
|
+
case subject
|
|
739
|
+
when Symbol, String
|
|
740
|
+
unless subject.to_s.include?('.') || @properties.named?(subject) || @relationships.key?(subject)
|
|
741
|
+
raise ArgumentError, "condition #{subject.inspect} does not map to a property or relationship in #{model}"
|
|
742
|
+
end
|
|
346
743
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
# eg:
|
|
352
|
-
#if property.model != self.model
|
|
353
|
-
# @links << discover_path_for_property(property)
|
|
354
|
-
#end
|
|
744
|
+
when Operator
|
|
745
|
+
unless (Conditions::Comparison.slugs | [ :not ]).include?(subject.operator)
|
|
746
|
+
raise ArgumentError, "condition #{subject.inspect} used an invalid operator #{subject.operator}"
|
|
747
|
+
end
|
|
355
748
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
749
|
+
assert_valid_conditions(subject.target => bind_value)
|
|
750
|
+
|
|
751
|
+
if subject.operator == :not && bind_value.kind_of?(Array) && bind_value.empty?
|
|
752
|
+
raise ArgumentError, "Cannot use 'not' operator with a bind value that is an empty Array for #{subject.inspect}"
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
when Path
|
|
756
|
+
assert_valid_links(subject.relationships)
|
|
362
757
|
|
|
363
|
-
|
|
364
|
-
|
|
758
|
+
when Associations::Relationship, Property
|
|
759
|
+
# TODO: validate that it belongs to the current model, or to any
|
|
760
|
+
# model in the links
|
|
761
|
+
#unless @properties.include?(subject)
|
|
762
|
+
# raise ArgumentError, "condition #{subject.name.inspect} does not map to a property in #{model}"
|
|
763
|
+
#end
|
|
764
|
+
|
|
765
|
+
else
|
|
766
|
+
raise ArgumentError, "condition #{subject.inspect} of an unsupported object #{subject.class}"
|
|
365
767
|
end
|
|
768
|
+
end
|
|
366
769
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
raise ArgumentError,
|
|
370
|
-
|
|
770
|
+
when Array
|
|
771
|
+
if conditions.empty?
|
|
772
|
+
raise ArgumentError, '+options[:conditions]+ should not be empty'
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
unless conditions.first.kind_of?(String) && !conditions.first.blank?
|
|
776
|
+
raise ArgumentError, '+options[:conditions]+ should have a statement for the first entry'
|
|
777
|
+
end
|
|
371
778
|
end
|
|
372
779
|
end
|
|
373
780
|
|
|
374
|
-
#
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
fields.map do |field|
|
|
379
|
-
case field
|
|
380
|
-
when Property, Operator
|
|
381
|
-
# TODO: if the Property's model doesn't match
|
|
382
|
-
# self.model, append the property's model to @links
|
|
383
|
-
# eg:
|
|
384
|
-
#if property.model != self.model
|
|
385
|
-
# @links << discover_path_for_property(property)
|
|
386
|
-
#end
|
|
387
|
-
field
|
|
388
|
-
when Symbol, String
|
|
389
|
-
property = @properties[field]
|
|
781
|
+
# Verifies that query offset is non-negative and only used together with limit
|
|
782
|
+
# @api private
|
|
783
|
+
def assert_valid_offset(offset, limit)
|
|
784
|
+
assert_kind_of 'options[:offset]', offset, Integer
|
|
390
785
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
786
|
+
unless offset >= 0
|
|
787
|
+
raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{offset.inspect}"
|
|
788
|
+
end
|
|
394
789
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
raise ArgumentError, "+options[:fields]+ entry #{field.inspect} not supported", caller(2)
|
|
398
|
-
end
|
|
790
|
+
if offset > 0 && limit.nil?
|
|
791
|
+
raise ArgumentError, '+options[:offset]+ cannot be greater than 0 if limit is not specified'
|
|
399
792
|
end
|
|
400
793
|
end
|
|
401
794
|
|
|
402
|
-
#
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
795
|
+
# Verifies the limit is equal to or greater than 0
|
|
796
|
+
#
|
|
797
|
+
# @raise [ArgumentError]
|
|
798
|
+
# raised if the limit is not an Integer or less than 0
|
|
799
|
+
#
|
|
800
|
+
# @api private
|
|
801
|
+
def assert_valid_limit(limit)
|
|
802
|
+
assert_kind_of 'options[:limit]', limit, Integer
|
|
803
|
+
|
|
804
|
+
unless limit >= 0
|
|
805
|
+
raise ArgumentError, "+options[:limit]+ must be greater than or equal to 0, but was #{limit.inspect}"
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# Verifies that :order option uses proper operator and refers
|
|
810
|
+
# to existing property
|
|
811
|
+
#
|
|
812
|
+
# @api private
|
|
813
|
+
def assert_valid_order(order, fields)
|
|
814
|
+
return if order.nil?
|
|
815
|
+
|
|
816
|
+
assert_kind_of 'options[:order]', order, Array
|
|
817
|
+
|
|
818
|
+
if order.empty? && fields && fields.any? { |property| !property.kind_of?(Operator) }
|
|
819
|
+
raise ArgumentError, '+options[:order]+ should not be empty if +options[:fields] contains a non-operator'
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
order.each do |order_entry|
|
|
823
|
+
case order_entry
|
|
412
824
|
when Symbol, String
|
|
413
|
-
|
|
825
|
+
unless @properties.named?(order_entry)
|
|
826
|
+
raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} does not map to a property in #{model}"
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
when Property
|
|
830
|
+
unless @properties.include?(order_entry)
|
|
831
|
+
raise ArgumentError, "+options[:order]+ entry #{order_entry.name.inspect} does not map to a property in #{model}"
|
|
832
|
+
end
|
|
414
833
|
|
|
415
|
-
|
|
416
|
-
|
|
834
|
+
when Operator, Direction
|
|
835
|
+
unless order_entry.operator == :asc || order_entry.operator == :desc
|
|
836
|
+
raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} used an invalid operator #{order_entry.operator}"
|
|
417
837
|
end
|
|
418
838
|
|
|
419
|
-
|
|
839
|
+
assert_valid_order([ order_entry.target ], fields)
|
|
840
|
+
|
|
420
841
|
else
|
|
421
|
-
raise ArgumentError, "+options[:
|
|
842
|
+
raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} of an unsupported object #{order_entry.class}"
|
|
422
843
|
end
|
|
423
844
|
end
|
|
424
845
|
end
|
|
425
846
|
|
|
426
|
-
#
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
847
|
+
# Used to verify value of boolean properties in conditions
|
|
848
|
+
# @api private
|
|
849
|
+
def assert_valid_boolean(name, value)
|
|
850
|
+
if value != true && value != false
|
|
851
|
+
raise ArgumentError, "+#{name}+ should be true or false, but was #{value.inspect}"
|
|
852
|
+
end
|
|
432
853
|
end
|
|
433
854
|
|
|
434
|
-
#
|
|
855
|
+
# Verifies that associations given in conditions belong
|
|
856
|
+
# to the same repository as query's model
|
|
435
857
|
#
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
858
|
+
# @api private
|
|
859
|
+
def assert_valid_other(other)
|
|
860
|
+
unless other.repository == repository
|
|
861
|
+
raise ArgumentError, "+other+ #{other.class} must be for the #{repository.name} repository, not #{other.repository.name}"
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
unless other.model >= model
|
|
865
|
+
raise ArgumentError, "+other+ #{other.class} must be for the #{model.name} model, not #{other.model.name}"
|
|
439
866
|
end
|
|
440
867
|
end
|
|
441
868
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
869
|
+
# Normalize order elements to Query::Direction instances
|
|
870
|
+
#
|
|
871
|
+
# TODO: needs example
|
|
872
|
+
#
|
|
873
|
+
# @api private
|
|
874
|
+
def normalize_order
|
|
875
|
+
return if @order.nil?
|
|
445
876
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
operator = clause.operator
|
|
454
|
-
return if operator == :not && bind_value == []
|
|
455
|
-
if clause.target.is_a?(Symbol)
|
|
456
|
-
@properties[clause.target]
|
|
457
|
-
elsif clause.target.is_a?(Query::Path)
|
|
458
|
-
validate_query_path_links(clause.target)
|
|
459
|
-
clause.target
|
|
460
|
-
end
|
|
461
|
-
when Symbol
|
|
462
|
-
@properties[clause]
|
|
463
|
-
when String
|
|
464
|
-
if clause =~ /\w\.\w/
|
|
465
|
-
query_path = @model
|
|
466
|
-
clause.split(".").each { |piece| query_path = query_path.send(piece) }
|
|
467
|
-
append_condition(query_path, bind_value)
|
|
468
|
-
return
|
|
469
|
-
else
|
|
470
|
-
@properties[clause]
|
|
471
|
-
end
|
|
472
|
-
else
|
|
473
|
-
raise ArgumentError, "Condition type #{clause.inspect} not supported", caller(2)
|
|
474
|
-
end
|
|
877
|
+
# TODO: should Query::Path objects be permitted? If so, then it
|
|
878
|
+
# should probably be normalized to a Direction object
|
|
879
|
+
@order = @order.map do |order|
|
|
880
|
+
case order
|
|
881
|
+
when Operator
|
|
882
|
+
target = order.target
|
|
883
|
+
property = target.kind_of?(Property) ? target : @properties[target]
|
|
475
884
|
|
|
476
|
-
|
|
477
|
-
raise ArgumentError, "Clause #{clause.inspect} does not map to a DataMapper::Property", caller(2)
|
|
478
|
-
end
|
|
885
|
+
Direction.new(property, order.operator)
|
|
479
886
|
|
|
480
|
-
|
|
887
|
+
when Symbol, String
|
|
888
|
+
Direction.new(@properties[order])
|
|
481
889
|
|
|
482
|
-
|
|
890
|
+
when Property
|
|
891
|
+
Direction.new(order)
|
|
892
|
+
|
|
893
|
+
when Direction
|
|
894
|
+
order.dup
|
|
895
|
+
end
|
|
896
|
+
end
|
|
483
897
|
end
|
|
484
898
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
899
|
+
# Normalize fields to Property instances
|
|
900
|
+
#
|
|
901
|
+
# TODO: needs example
|
|
902
|
+
#
|
|
903
|
+
# @api private
|
|
904
|
+
def normalize_fields
|
|
905
|
+
@fields = @fields.map do |field|
|
|
906
|
+
case field
|
|
907
|
+
when Symbol, String
|
|
908
|
+
@properties[field]
|
|
909
|
+
|
|
910
|
+
when Property, Operator
|
|
911
|
+
field
|
|
494
912
|
end
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
# TODO: check for other mutually exclusive operator + property
|
|
501
|
-
# combinations. For example if self's conditions were
|
|
502
|
-
# [ :gt, :amount, 5 ] and the other's condition is [ :lt, :amount, 2 ]
|
|
503
|
-
# there is a conflict. When in conflict the other's conditions
|
|
504
|
-
# overwrites self's conditions.
|
|
505
|
-
|
|
506
|
-
# TODO: Another condition is when the other condition operator is
|
|
507
|
-
# eql, this should over-write all the like,range and list operators
|
|
508
|
-
# for the same property, since we are now looking for an exact match.
|
|
509
|
-
# Vice versa, passing in eql should overwrite all of those operators.
|
|
510
|
-
|
|
511
|
-
def update_conditions(other)
|
|
512
|
-
@conditions = @conditions.dup
|
|
513
|
-
|
|
514
|
-
# build an index of conditions by the property and operator to
|
|
515
|
-
# avoid nested looping
|
|
516
|
-
conditions_index = {}
|
|
517
|
-
@conditions.each do |condition|
|
|
518
|
-
operator, property = *condition
|
|
519
|
-
next if :raw == operator
|
|
520
|
-
conditions_index[property] ||= {}
|
|
521
|
-
conditions_index[property][operator] = condition
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
# loop over each of the other's conditions, and overwrite the
|
|
525
|
-
# conditions when in conflict
|
|
526
|
-
other.conditions.each do |other_condition|
|
|
527
|
-
other_operator, other_property, other_bind_value = *other_condition
|
|
528
|
-
|
|
529
|
-
unless :raw == other_operator
|
|
530
|
-
conditions_index[other_property] ||= {}
|
|
531
|
-
if condition = conditions_index[other_property][other_operator]
|
|
532
|
-
operator, property, bind_value = *condition
|
|
533
|
-
|
|
534
|
-
next if bind_value == other_bind_value
|
|
535
|
-
|
|
536
|
-
# overwrite the bind value in the existing condition
|
|
537
|
-
condition[2] = case operator
|
|
538
|
-
when :eql, :like then other_bind_value
|
|
539
|
-
when :gt, :gte then [ bind_value, other_bind_value ].min
|
|
540
|
-
when :lt, :lte then [ bind_value, other_bind_value ].max
|
|
541
|
-
when :not, :in
|
|
542
|
-
if bind_value.kind_of?(Array)
|
|
543
|
-
bind_value |= other_bind_value
|
|
544
|
-
elsif other_bind_value.kind_of?(Array)
|
|
545
|
-
other_bind_value |= bind_value
|
|
546
|
-
else
|
|
547
|
-
other_bind_value
|
|
548
|
-
end
|
|
549
|
-
end
|
|
913
|
+
end
|
|
914
|
+
end
|
|
550
915
|
|
|
551
|
-
|
|
552
|
-
|
|
916
|
+
# Normalize links to Query::Path
|
|
917
|
+
#
|
|
918
|
+
# Normalization means links given as symbols are replaced with
|
|
919
|
+
# relationships they refer to, intermediate links are "followed"
|
|
920
|
+
# and duplicates are removed
|
|
921
|
+
#
|
|
922
|
+
# @api private
|
|
923
|
+
def normalize_links
|
|
924
|
+
links = @links.dup
|
|
925
|
+
|
|
926
|
+
@links.clear
|
|
927
|
+
|
|
928
|
+
while link = links.shift
|
|
929
|
+
relationship = case link
|
|
930
|
+
when Symbol, String then @relationships[link]
|
|
931
|
+
when Associations::Relationship then link
|
|
553
932
|
end
|
|
554
933
|
|
|
555
|
-
|
|
556
|
-
@conditions << other_condition.dup
|
|
557
|
-
end
|
|
934
|
+
next if @links.include?(relationship)
|
|
558
935
|
|
|
559
|
-
|
|
560
|
-
|
|
936
|
+
if relationship.respond_to?(:links)
|
|
937
|
+
links.concat(relationship.links)
|
|
938
|
+
else
|
|
939
|
+
repository_name = relationship.relative_target_repository_name
|
|
940
|
+
model = relationship.target_model
|
|
561
941
|
|
|
562
|
-
|
|
563
|
-
|
|
942
|
+
# TODO: see if this can handle extracting the :order option and sort the
|
|
943
|
+
# resulting collection using the order specified by through relationships
|
|
564
944
|
|
|
565
|
-
|
|
945
|
+
model.current_scope.merge(relationship.query).each do |subject, value|
|
|
946
|
+
# TODO: figure out how to merge Query options from links
|
|
947
|
+
if OPTIONS.include?(subject)
|
|
948
|
+
next # skip for now
|
|
949
|
+
end
|
|
566
950
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
hash == other.hash
|
|
570
|
-
end
|
|
951
|
+
# set @repository when appending conditions
|
|
952
|
+
original, @repository = @repository, DataMapper.repository(repository_name)
|
|
571
953
|
|
|
572
|
-
|
|
954
|
+
begin
|
|
955
|
+
append_condition(subject, value, model)
|
|
956
|
+
ensure
|
|
957
|
+
@repository = original
|
|
958
|
+
end
|
|
959
|
+
end
|
|
573
960
|
|
|
574
|
-
|
|
575
|
-
|
|
961
|
+
@links << relationship
|
|
962
|
+
end
|
|
576
963
|
end
|
|
964
|
+
end
|
|
577
965
|
|
|
578
|
-
|
|
579
|
-
|
|
966
|
+
# Append conditions to this Query
|
|
967
|
+
#
|
|
968
|
+
# TODO: needs example
|
|
969
|
+
#
|
|
970
|
+
# @param [Property, Symbol, String, Operator, Associations::Relationship, Path] subject
|
|
971
|
+
# the subject to match
|
|
972
|
+
# @param [Object] bind_value
|
|
973
|
+
# the value to match on
|
|
974
|
+
# @param [Symbol] operator
|
|
975
|
+
# the operator to match with
|
|
976
|
+
#
|
|
977
|
+
# @return [Query::Conditions::AbstractOperation]
|
|
978
|
+
# the Query conditions
|
|
979
|
+
#
|
|
980
|
+
# @api private
|
|
981
|
+
def append_condition(subject, bind_value, model = self.model, operator = :eql)
|
|
982
|
+
case subject
|
|
983
|
+
when Property, Associations::Relationship then append_property_condition(subject, bind_value, operator)
|
|
984
|
+
when Symbol then append_symbol_condition(subject, bind_value, model, operator)
|
|
985
|
+
when String then append_string_condition(subject, bind_value, model, operator)
|
|
986
|
+
when Operator then append_operator_conditions(subject, bind_value, model)
|
|
987
|
+
when Path then append_path(subject, bind_value, model, operator)
|
|
988
|
+
else
|
|
989
|
+
raise ArgumentError, "#{subject} is an invalid instance: #{subject.class}"
|
|
580
990
|
end
|
|
991
|
+
end
|
|
581
992
|
|
|
582
|
-
|
|
583
|
-
|
|
993
|
+
# TODO: document
|
|
994
|
+
# @api private
|
|
995
|
+
def append_property_condition(property, bind_value, operator)
|
|
996
|
+
bind_value = normalize_bind_value(property, bind_value)
|
|
997
|
+
negated = operator == :not
|
|
998
|
+
|
|
999
|
+
if operator == :eql || negated
|
|
1000
|
+
operator = case bind_value
|
|
1001
|
+
when Array, Range, Set, Collection then :in
|
|
1002
|
+
when Regexp then :regexp
|
|
1003
|
+
else :eql
|
|
1004
|
+
end
|
|
584
1005
|
end
|
|
585
1006
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
def initialize(property, direction = :asc)
|
|
589
|
-
assert_kind_of 'property', property, Property
|
|
590
|
-
assert_kind_of 'direction', direction, Symbol
|
|
1007
|
+
condition = Conditions::Comparison.new(operator, property, bind_value)
|
|
591
1008
|
|
|
592
|
-
|
|
593
|
-
|
|
1009
|
+
if negated
|
|
1010
|
+
condition = Conditions::Operation.new(:not, condition)
|
|
594
1011
|
end
|
|
595
|
-
end # class Direction
|
|
596
|
-
|
|
597
|
-
class Operator
|
|
598
|
-
include Assertions
|
|
599
1012
|
|
|
600
|
-
|
|
1013
|
+
add_condition(condition)
|
|
1014
|
+
end
|
|
601
1015
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1016
|
+
# TODO: document
|
|
1017
|
+
# @api private
|
|
1018
|
+
def append_symbol_condition(symbol, bind_value, model, operator)
|
|
1019
|
+
append_condition(symbol.to_s, bind_value, model, operator)
|
|
1020
|
+
end
|
|
605
1021
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1022
|
+
# TODO: document
|
|
1023
|
+
# @api private
|
|
1024
|
+
def append_string_condition(string, bind_value, model, operator)
|
|
1025
|
+
if string.include?('.')
|
|
1026
|
+
query_path = model
|
|
611
1027
|
|
|
612
|
-
|
|
1028
|
+
target_components = string.split('.')
|
|
1029
|
+
operator = target_components.pop.to_sym if DataMapper::Query::Conditions::Comparison.slugs.map{ |s| s.to_s }.include? target_components.last
|
|
1030
|
+
target_components.each { |method| query_path = query_path.send(method) }
|
|
613
1031
|
|
|
614
|
-
|
|
615
|
-
|
|
1032
|
+
append_condition(query_path, bind_value, model, operator)
|
|
1033
|
+
else
|
|
1034
|
+
repository_name = repository.name
|
|
1035
|
+
subject = model.properties(repository_name)[string] ||
|
|
1036
|
+
model.relationships(repository_name)[string]
|
|
616
1037
|
|
|
617
|
-
|
|
618
|
-
@operator = operator
|
|
1038
|
+
append_condition(subject, bind_value, model, operator)
|
|
619
1039
|
end
|
|
620
|
-
end
|
|
1040
|
+
end
|
|
621
1041
|
|
|
622
|
-
|
|
623
|
-
|
|
1042
|
+
# TODO: document
|
|
1043
|
+
# @api private
|
|
1044
|
+
def append_operator_conditions(operator, bind_value, model)
|
|
1045
|
+
append_condition(operator.target, bind_value, model, operator.operator)
|
|
1046
|
+
end
|
|
624
1047
|
|
|
625
|
-
|
|
1048
|
+
# TODO: document
|
|
1049
|
+
# @api private
|
|
1050
|
+
def append_path(path, bind_value, model, operator)
|
|
1051
|
+
@links.unshift(*path.relationships.reverse.map { |relationship| relationship.inverse })
|
|
1052
|
+
append_condition(path.property, bind_value, path.model, operator)
|
|
1053
|
+
end
|
|
626
1054
|
|
|
627
|
-
|
|
1055
|
+
# Add a condition to the Query
|
|
1056
|
+
#
|
|
1057
|
+
# @param [AbstractOperation, AbstractComparison]
|
|
1058
|
+
# the condition to add to the Query
|
|
1059
|
+
#
|
|
1060
|
+
# @return [undefined]
|
|
1061
|
+
#
|
|
1062
|
+
# @api private
|
|
1063
|
+
def add_condition(condition)
|
|
1064
|
+
@conditions = Conditions::Operation.new(:and) if @conditions.nil?
|
|
1065
|
+
@conditions << condition
|
|
1066
|
+
end
|
|
628
1067
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1068
|
+
# TODO: make this typecast all bind values that do not match the
|
|
1069
|
+
# property primitive
|
|
1070
|
+
|
|
1071
|
+
# TODO: document
|
|
1072
|
+
# @api private
|
|
1073
|
+
def normalize_bind_value(property_or_path, bind_value)
|
|
1074
|
+
# TODO: defer this inside the comparison
|
|
1075
|
+
if bind_value.respond_to?(:call)
|
|
1076
|
+
bind_value = bind_value.call
|
|
635
1077
|
end
|
|
636
1078
|
|
|
637
|
-
#
|
|
638
|
-
|
|
639
|
-
|
|
1079
|
+
# TODO: bypass this for Collection, once subqueries can be handled by adapters
|
|
1080
|
+
if bind_value.respond_to?(:to_ary)
|
|
1081
|
+
bind_value = bind_value.to_ary
|
|
1082
|
+
bind_value.uniq!
|
|
640
1083
|
end
|
|
641
1084
|
|
|
642
|
-
#
|
|
643
|
-
|
|
644
|
-
|
|
1085
|
+
# FIXME: causes m:m specs to fail with in-memory adapter
|
|
1086
|
+
# if bind_value.instance_of?(Array) && bind_value.size == 1
|
|
1087
|
+
# bind_value = bind_value.first
|
|
1088
|
+
# end
|
|
1089
|
+
|
|
1090
|
+
bind_value
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
# Extract arguments for #slice and #slice! then return offset and limit
|
|
1094
|
+
#
|
|
1095
|
+
# @param [Integer, Array(Integer), Range] *args the offset,
|
|
1096
|
+
# offset and limit, or range indicating first and last position
|
|
1097
|
+
#
|
|
1098
|
+
# @return [Integer] the offset
|
|
1099
|
+
# @return [Integer, NilClass] the limit, if any
|
|
1100
|
+
#
|
|
1101
|
+
# @api private
|
|
1102
|
+
def extract_slice_arguments(*args)
|
|
1103
|
+
first_arg, second_arg = args
|
|
1104
|
+
|
|
1105
|
+
if args.size == 2 && first_arg.kind_of?(Integer) && second_arg.kind_of?(Integer)
|
|
1106
|
+
return first_arg, second_arg
|
|
1107
|
+
elsif args.size == 1
|
|
1108
|
+
if first_arg.kind_of?(Integer)
|
|
1109
|
+
return first_arg, 1
|
|
1110
|
+
elsif first_arg.kind_of?(Range)
|
|
1111
|
+
offset = first_arg.first
|
|
1112
|
+
limit = first_arg.last - offset
|
|
1113
|
+
limit += 1 unless first_arg.exclude_end?
|
|
1114
|
+
return offset, limit
|
|
1115
|
+
end
|
|
645
1116
|
end
|
|
646
1117
|
|
|
647
|
-
|
|
1118
|
+
raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}"
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
# TODO: document
|
|
1122
|
+
# @api private
|
|
1123
|
+
def get_relative_position(offset, limit)
|
|
1124
|
+
new_offset = self.offset + offset
|
|
1125
|
+
|
|
1126
|
+
if limit <= 0 || (self.limit && new_offset + limit > self.offset + self.limit)
|
|
1127
|
+
raise RangeError, "offset #{offset} and limit #{limit} are outside allowed range"
|
|
1128
|
+
end
|
|
648
1129
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
assert_kind_of 'relationships', relationships, Array
|
|
652
|
-
assert_kind_of 'model', model, Model
|
|
653
|
-
assert_kind_of 'property_name', property_name, Symbol unless property_name.nil?
|
|
1130
|
+
return new_offset, limit
|
|
1131
|
+
end
|
|
654
1132
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1133
|
+
# TODO: DRY this up with conditions
|
|
1134
|
+
# @api private
|
|
1135
|
+
def record_value(record, property)
|
|
1136
|
+
case record
|
|
1137
|
+
when Hash
|
|
1138
|
+
record.fetch(property, record[property.field])
|
|
1139
|
+
when Resource
|
|
1140
|
+
property.get!(record)
|
|
659
1141
|
end
|
|
1142
|
+
end
|
|
660
1143
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
end
|
|
1144
|
+
# TODO: document
|
|
1145
|
+
# @api private
|
|
1146
|
+
def each_comparison
|
|
1147
|
+
operands = conditions.operands.dup
|
|
666
1148
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1149
|
+
while operand = operands.shift
|
|
1150
|
+
if operand.respond_to?(:operands)
|
|
1151
|
+
operands.concat(operand.operands)
|
|
1152
|
+
else
|
|
1153
|
+
yield operand
|
|
670
1154
|
end
|
|
671
|
-
|
|
672
|
-
raise NoMethodError, "undefined property or association `#{method}' on #{@model}"
|
|
673
1155
|
end
|
|
674
|
-
end
|
|
1156
|
+
end
|
|
675
1157
|
end # class Query
|
|
676
1158
|
end # module DataMapper
|