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