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/collection.rb
CHANGED
|
@@ -1,189 +1,313 @@
|
|
|
1
|
+
# TODO: if Collection is scoped by a unique property, should adding
|
|
2
|
+
# new Resources be denied?
|
|
3
|
+
|
|
4
|
+
# TODO: add #copy method
|
|
5
|
+
|
|
6
|
+
# TODO: move Collection#loaded_entries to LazyArray
|
|
7
|
+
# TODO: move Collection#partially_loaded to LazyArray
|
|
8
|
+
|
|
1
9
|
module DataMapper
|
|
10
|
+
# The Collection class represents a list of resources persisted in
|
|
11
|
+
# a repository and identified by a query.
|
|
12
|
+
#
|
|
13
|
+
# A Collection should act like an Array in every way, except that
|
|
14
|
+
# it will attempt to defer loading until the results from the
|
|
15
|
+
# repository are needed.
|
|
16
|
+
#
|
|
17
|
+
# A Collection is typically returned by the Model#all
|
|
18
|
+
# method.
|
|
2
19
|
class Collection < LazyArray
|
|
3
|
-
|
|
20
|
+
extend Deprecate
|
|
21
|
+
|
|
22
|
+
deprecate :add, :<<
|
|
23
|
+
deprecate :build, :new
|
|
4
24
|
|
|
25
|
+
# Returns the Query the Collection is scoped with
|
|
26
|
+
#
|
|
27
|
+
# @return [Query]
|
|
28
|
+
# the Query the Collection is scoped with
|
|
29
|
+
#
|
|
30
|
+
# @api semipublic
|
|
5
31
|
attr_reader :query
|
|
6
32
|
|
|
7
|
-
|
|
8
|
-
# @return [Repository] the repository the collection is
|
|
9
|
-
# associated with
|
|
33
|
+
# Returns the Repository
|
|
10
34
|
#
|
|
11
|
-
# @
|
|
35
|
+
# @return [Repository]
|
|
36
|
+
# the Repository this Collection is associated with
|
|
37
|
+
#
|
|
38
|
+
# @api semipublic
|
|
12
39
|
def repository
|
|
13
40
|
query.repository
|
|
14
41
|
end
|
|
15
42
|
|
|
16
|
-
|
|
17
|
-
# loads the entries for the collection. Used by the
|
|
18
|
-
# adapters to load the instances of the declared
|
|
19
|
-
# model for this collection's query.
|
|
43
|
+
# Returns the Model
|
|
20
44
|
#
|
|
21
|
-
# @
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
# @return [Model]
|
|
46
|
+
# the Model the Collection is associated with
|
|
47
|
+
#
|
|
48
|
+
# @api semipublic
|
|
49
|
+
def model
|
|
50
|
+
query.model
|
|
24
51
|
end
|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
#
|
|
53
|
+
# Reloads the Collection from the repository
|
|
54
|
+
#
|
|
55
|
+
# If +query+ is provided, updates this Collection's query with its conditions
|
|
28
56
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
57
|
+
# cars_from_91 = Cars.all(:year_manufactured => 1991)
|
|
58
|
+
# cars_from_91.first.year_manufactured = 2001 # note: not saved
|
|
59
|
+
# cars_from_91.reload
|
|
60
|
+
# cars_from_91.first.year #=> 1991
|
|
32
61
|
#
|
|
33
|
-
# @
|
|
62
|
+
# @param [Query, Hash] query (optional)
|
|
63
|
+
# further restrict results with query
|
|
64
|
+
#
|
|
65
|
+
# @return [self]
|
|
34
66
|
#
|
|
35
67
|
# @api public
|
|
36
|
-
def reload(query =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
def reload(query = nil)
|
|
69
|
+
query = query.nil? ? self.query.dup : self.query.merge(query)
|
|
70
|
+
|
|
71
|
+
# make sure the Identity Map contains all the existing resources
|
|
72
|
+
identity_map = repository.identity_map(model)
|
|
73
|
+
|
|
74
|
+
loaded_entries.each do |resource|
|
|
75
|
+
identity_map[resource.key] = resource
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
properties = model.properties(repository.name)
|
|
79
|
+
fields = properties.key | query.fields
|
|
80
|
+
|
|
81
|
+
if discriminator = properties.discriminator
|
|
82
|
+
fields |= [ discriminator ]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# sort fields based on declared order, for more consistent reload queries
|
|
86
|
+
fields = properties & fields
|
|
87
|
+
|
|
88
|
+
# replace the list of resources
|
|
89
|
+
replace(all(query.update(:fields => fields, :reload => true)))
|
|
42
90
|
end
|
|
43
91
|
|
|
44
|
-
|
|
45
|
-
#
|
|
92
|
+
# Lookup a Resource in the Collection by key
|
|
93
|
+
#
|
|
94
|
+
# This looksup a Resource by key, typecasting the key to the
|
|
95
|
+
# proper object if necessary.
|
|
46
96
|
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
97
|
+
# toyotas = Cars.all(:manufacturer => 'Toyota')
|
|
98
|
+
# toyo = Cars.first(:manufacturer => 'Toyota')
|
|
99
|
+
# toyotas.get(toyo.id) == toyo #=> true
|
|
49
100
|
#
|
|
50
|
-
# @
|
|
51
|
-
#
|
|
101
|
+
# @param [Enumerable] *key
|
|
102
|
+
# keys which uniquely identify a resource in the Collection
|
|
103
|
+
#
|
|
104
|
+
# @return [Resource]
|
|
105
|
+
# Resource which matches the supplied key
|
|
106
|
+
# @return [nil]
|
|
107
|
+
# No Resource matches the supplied key
|
|
52
108
|
#
|
|
53
109
|
# @api public
|
|
54
110
|
def get(*key)
|
|
55
|
-
key = model.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
each {|r| @cache[r.key] = r } if @cache.empty?
|
|
59
|
-
@cache[key]
|
|
60
|
-
elsif query.limit || query.offset > 0
|
|
111
|
+
key = model.key(repository.name).typecast(key)
|
|
112
|
+
|
|
113
|
+
resource = @identity_map[key] || if !loaded? && (query.limit || query.offset > 0)
|
|
61
114
|
# current query is exclusive, find resource within the set
|
|
62
115
|
|
|
63
|
-
# TODO: use a subquery to retrieve the
|
|
116
|
+
# TODO: use a subquery to retrieve the Collection and then match
|
|
64
117
|
# it up against the key. This will require some changes to
|
|
65
118
|
# how subqueries are generated, since the key may be a
|
|
66
119
|
# composite key. In the case of DO adapters, it means subselects
|
|
67
|
-
# like the form "(a, b) IN(SELECT a,b FROM ...)", which will
|
|
120
|
+
# like the form "(a, b) IN(SELECT a, b FROM ...)", which will
|
|
68
121
|
# require making it so the Query condition key can be a
|
|
69
122
|
# Property or an Array of Property objects
|
|
70
123
|
|
|
71
124
|
# use the brute force approach until subquery lookups work
|
|
72
125
|
lazy_load
|
|
73
|
-
|
|
126
|
+
@identity_map[key]
|
|
74
127
|
else
|
|
75
128
|
# current query is all inclusive, lookup using normal approach
|
|
76
|
-
first(model.
|
|
129
|
+
first(model.key_conditions(repository, key))
|
|
77
130
|
end
|
|
131
|
+
|
|
132
|
+
return if resource.nil?
|
|
133
|
+
|
|
134
|
+
orphan_resource(resource)
|
|
78
135
|
end
|
|
79
136
|
|
|
80
|
-
|
|
81
|
-
# retrieves an entry out of the collection's entry by key,
|
|
82
|
-
# raising an exception if the object cannot be found
|
|
137
|
+
# Lookup a Resource in the Collection by key, raising an exception if not found
|
|
83
138
|
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
139
|
+
# This looksup a Resource by key, typecasting the key to the
|
|
140
|
+
# proper object if necessary.
|
|
86
141
|
#
|
|
87
|
-
# @
|
|
142
|
+
# @param [Enumerable] *key
|
|
143
|
+
# keys which uniquely identify a resource in the Collection
|
|
88
144
|
#
|
|
89
|
-
# @
|
|
145
|
+
# @return [Resource]
|
|
146
|
+
# Resource which matches the supplied key
|
|
147
|
+
# @return [nil]
|
|
148
|
+
# No Resource matches the supplied key
|
|
149
|
+
#
|
|
150
|
+
# @raise [ObjectNotFoundError] Resource could not be found by key
|
|
90
151
|
#
|
|
91
152
|
# @api public
|
|
92
153
|
def get!(*key)
|
|
93
|
-
get(*key) || raise(ObjectNotFoundError, "Could not find #{model.name} with key #{key.inspect}
|
|
154
|
+
get(*key) || raise(ObjectNotFoundError, "Could not find #{model.name} with key #{key.inspect}")
|
|
94
155
|
end
|
|
95
156
|
|
|
96
|
-
|
|
97
|
-
#
|
|
98
|
-
#
|
|
157
|
+
# Returns a new Collection optionally scoped by +query+
|
|
158
|
+
#
|
|
159
|
+
# This returns a new Collection scoped relative to the current
|
|
160
|
+
# Collection.
|
|
99
161
|
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
162
|
+
# cars_from_91 = Cars.all(:year_manufactured => 1991)
|
|
163
|
+
# toyotas_91 = cars_from_91.all(:manufacturer => 'Toyota')
|
|
164
|
+
# toyotas_91.all? { |car| car.year_manufactured == 1991 } #=> true
|
|
165
|
+
# toyotas_91.all? { |car| car.manufacturer == 'Toyota' } #=> true
|
|
102
166
|
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
167
|
+
# If +query+ is a Hash, results will be found by merging +query+ with this Collection's query.
|
|
168
|
+
# If +query+ is a Query, results will be found using +query+ as an absolute query.
|
|
169
|
+
#
|
|
170
|
+
# @param [Hash, Query] query
|
|
171
|
+
# optional parameters to scope results with
|
|
172
|
+
#
|
|
173
|
+
# @return [Collection]
|
|
174
|
+
# Collection scoped by +query+
|
|
105
175
|
#
|
|
106
176
|
# @api public
|
|
107
|
-
def all(query =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
177
|
+
def all(query = nil)
|
|
178
|
+
if query.nil? || (query.kind_of?(Hash) && query.empty?)
|
|
179
|
+
dup
|
|
180
|
+
else
|
|
181
|
+
# TODO: if there is no order parameter, and the Collection is not loaded
|
|
182
|
+
# check to see if the query can be satisfied by the head/tail
|
|
183
|
+
|
|
184
|
+
new_collection(scoped_query(query))
|
|
185
|
+
end
|
|
112
186
|
end
|
|
113
187
|
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
# there are no arguments
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
188
|
+
# Return the first Resource or the first N Resources in the Collection with an optional query
|
|
189
|
+
#
|
|
190
|
+
# When there are no arguments, return the first Resource in the
|
|
191
|
+
# Collection. When the first argument is an Integer, return a
|
|
192
|
+
# Collection containing the first N Resources. When the last
|
|
193
|
+
# (optional) argument is a Hash scope the results to the query.
|
|
120
194
|
#
|
|
121
|
-
# @param [Integer
|
|
195
|
+
# @param [Integer] limit (optional)
|
|
196
|
+
# limit the returned Collection to a specific number of entries
|
|
197
|
+
# @param [Hash] query (optional)
|
|
198
|
+
# scope the returned Resource or Collection to the supplied query
|
|
122
199
|
#
|
|
123
|
-
# @return [
|
|
124
|
-
# first resource in the entries of this collection,
|
|
125
|
-
# a new collection whose query has been merged
|
|
200
|
+
# @return [Resource, Collection]
|
|
201
|
+
# The first resource in the entries of this collection,
|
|
202
|
+
# or a new collection whose query has been merged
|
|
126
203
|
#
|
|
127
204
|
# @api public
|
|
128
205
|
def first(*args)
|
|
129
|
-
|
|
130
|
-
if loaded?
|
|
131
|
-
if args.empty?
|
|
132
|
-
return super
|
|
133
|
-
elsif args.size == 1 && args.first.kind_of?(Integer)
|
|
134
|
-
limit = args.shift
|
|
135
|
-
return self.class.new(scoped_query(:limit => limit)) { |c| c.replace(super(limit)) }
|
|
136
|
-
end
|
|
137
|
-
end
|
|
206
|
+
last_arg = args.last
|
|
138
207
|
|
|
139
|
-
|
|
140
|
-
|
|
208
|
+
limit = args.first if args.first.kind_of?(Integer)
|
|
209
|
+
with_query = last_arg.respond_to?(:merge) && !last_arg.blank?
|
|
141
210
|
|
|
142
|
-
|
|
143
|
-
|
|
211
|
+
query = with_query ? last_arg : {}
|
|
212
|
+
query = self.query.slice(0, limit || 1).update(query)
|
|
213
|
+
|
|
214
|
+
# TODO: when a query provided, and there are enough elements in head to
|
|
215
|
+
# satisfy the query.limit, filter the head with the query, and make
|
|
216
|
+
# sure it matches the limit exactly. if so, use that result instead
|
|
217
|
+
# of calling all()
|
|
218
|
+
# - this can probably only be done if there is no :order parameter
|
|
219
|
+
|
|
220
|
+
collection = if !with_query && (loaded? || lazy_possible?(head, query.limit))
|
|
221
|
+
new_collection(query, super(query.limit))
|
|
222
|
+
else
|
|
223
|
+
all(query)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
if limit
|
|
227
|
+
collection
|
|
144
228
|
else
|
|
145
|
-
|
|
229
|
+
collection.to_a.first
|
|
146
230
|
end
|
|
147
231
|
end
|
|
148
232
|
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
# there are no arguments
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
# internal query is scoped and a new collection is returned
|
|
233
|
+
# Return the last Resource or the last N Resources in the Collection with an optional query
|
|
234
|
+
#
|
|
235
|
+
# When there are no arguments, return the last Resource in the
|
|
236
|
+
# Collection. When the first argument is an Integer, return a
|
|
237
|
+
# Collection containing the last N Resources. When the last
|
|
238
|
+
# (optional) argument is a Hash scope the results to the query.
|
|
156
239
|
#
|
|
157
|
-
# @
|
|
240
|
+
# @param [Integer] limit (optional)
|
|
241
|
+
# limit the returned Collection to a specific number of entries
|
|
242
|
+
# @param [Hash] query (optional)
|
|
243
|
+
# scope the returned Resource or Collection to the supplied query
|
|
244
|
+
#
|
|
245
|
+
# @return [Resource, Collection]
|
|
246
|
+
# The last resource in the entries of this collection,
|
|
247
|
+
# or a new collection whose query has been merged
|
|
158
248
|
#
|
|
159
249
|
# @api public
|
|
160
250
|
def last(*args)
|
|
161
|
-
|
|
251
|
+
last_arg = args.last
|
|
252
|
+
|
|
253
|
+
limit = args.first if args.first.kind_of?(Integer)
|
|
254
|
+
with_query = last_arg.respond_to?(:merge) && !last_arg.blank?
|
|
255
|
+
|
|
256
|
+
query = with_query ? last_arg : {}
|
|
257
|
+
query = self.query.slice(0, limit || 1).update(query).reverse!
|
|
162
258
|
|
|
163
|
-
|
|
259
|
+
# tell the Query to prepend each result from the adapter
|
|
260
|
+
query.update(:add_reversed => !query.add_reversed?)
|
|
164
261
|
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
|
|
262
|
+
# TODO: when a query provided, and there are enough elements in tail to
|
|
263
|
+
# satisfy the query.limit, filter the tail with the query, and make
|
|
264
|
+
# sure it matches the limit exactly. if so, use that result instead
|
|
265
|
+
# of calling all()
|
|
266
|
+
|
|
267
|
+
collection = if !with_query && (loaded? || lazy_possible?(tail, query.limit))
|
|
268
|
+
new_collection(query, super(query.limit))
|
|
269
|
+
else
|
|
270
|
+
all(query)
|
|
271
|
+
end
|
|
168
272
|
|
|
169
|
-
|
|
273
|
+
if limit
|
|
274
|
+
collection
|
|
275
|
+
else
|
|
276
|
+
collection.to_a.last
|
|
277
|
+
end
|
|
170
278
|
end
|
|
171
279
|
|
|
172
|
-
|
|
173
|
-
# Simulates Array#at and returns the entry at that index.
|
|
174
|
-
# Also accepts negative indexes and appropriate reverses
|
|
175
|
-
# the order of the query
|
|
280
|
+
# Lookup a Resource from the Collection by offset
|
|
176
281
|
#
|
|
177
|
-
# @
|
|
178
|
-
#
|
|
282
|
+
# @param [Integer] offset
|
|
283
|
+
# offset of the Resource in the Collection
|
|
284
|
+
#
|
|
285
|
+
# @return [Resource]
|
|
286
|
+
# Resource which matches the supplied offset
|
|
287
|
+
# @return [nil]
|
|
288
|
+
# No Resource matches the supplied offset
|
|
179
289
|
#
|
|
180
290
|
# @api public
|
|
181
291
|
def at(offset)
|
|
182
|
-
|
|
183
|
-
|
|
292
|
+
if loaded? || partially_loaded?(offset)
|
|
293
|
+
return unless resource = super
|
|
294
|
+
orphan_resource(resource)
|
|
295
|
+
elsif offset >= 0
|
|
296
|
+
first(:offset => offset)
|
|
297
|
+
else
|
|
298
|
+
last(:offset => offset.abs - 1)
|
|
299
|
+
end
|
|
184
300
|
end
|
|
185
301
|
|
|
186
|
-
|
|
302
|
+
# Access LazyArray#slice directly
|
|
303
|
+
#
|
|
304
|
+
# Collection#[]= uses this to bypass Collection#slice and access
|
|
305
|
+
# the resources directly so that it can orphan them properly.
|
|
306
|
+
#
|
|
307
|
+
# @api private
|
|
308
|
+
alias superclass_slice slice
|
|
309
|
+
private :superclass_slice
|
|
310
|
+
|
|
187
311
|
# Simulates Array#slice and returns a new Collection
|
|
188
312
|
# whose query has a new offset or limit according to the
|
|
189
313
|
# arguments provided.
|
|
@@ -191,479 +315,1077 @@ module DataMapper
|
|
|
191
315
|
# If you provide a range, the min is used as the offset
|
|
192
316
|
# and the max minues the offset is used as the limit.
|
|
193
317
|
#
|
|
194
|
-
# @param [Integer, Array(Integer), Range] args
|
|
195
|
-
# offset and limit, or range indicating
|
|
318
|
+
# @param [Integer, Array(Integer), Range] *args
|
|
319
|
+
# the offset, offset and limit, or range indicating first and last position
|
|
196
320
|
#
|
|
197
|
-
# @return [
|
|
321
|
+
# @return [Resource, Collection, nil]
|
|
198
322
|
# The entry which resides at that offset and limit,
|
|
199
323
|
# or a new Collection object with the set limits and offset
|
|
324
|
+
# @return [nil]
|
|
325
|
+
# The offset (or starting offset) is out of range
|
|
200
326
|
#
|
|
201
327
|
# @raise [ArgumentError] "arguments may be 1 or 2 Integers,
|
|
202
328
|
# or 1 Range object, was: #{args.inspect}"
|
|
203
329
|
#
|
|
204
|
-
# @alias []
|
|
205
|
-
#
|
|
206
330
|
# @api public
|
|
207
|
-
def
|
|
208
|
-
|
|
331
|
+
def [](*args)
|
|
332
|
+
offset, limit = extract_slice_arguments(*args)
|
|
209
333
|
|
|
210
|
-
if args.size ==
|
|
211
|
-
offset
|
|
212
|
-
elsif args.size == 1 && args.first.kind_of?(Range)
|
|
213
|
-
range = args.first
|
|
214
|
-
offset = range.first
|
|
215
|
-
limit = range.last - offset
|
|
216
|
-
limit += 1 unless range.exclude_end?
|
|
217
|
-
else
|
|
218
|
-
raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}", caller
|
|
334
|
+
if args.size == 1 && args.first.kind_of?(Integer)
|
|
335
|
+
return at(offset)
|
|
219
336
|
end
|
|
220
337
|
|
|
221
|
-
|
|
338
|
+
query = sliced_query(offset, limit)
|
|
339
|
+
|
|
340
|
+
if loaded? || partially_loaded?(offset, limit)
|
|
341
|
+
new_collection(query, super)
|
|
342
|
+
else
|
|
343
|
+
new_collection(query)
|
|
344
|
+
end
|
|
222
345
|
end
|
|
223
346
|
|
|
224
|
-
alias []
|
|
347
|
+
alias slice []
|
|
225
348
|
|
|
226
|
-
|
|
349
|
+
# Deletes and Returns the Resources given by an offset or a Range
|
|
227
350
|
#
|
|
228
|
-
# @
|
|
229
|
-
#
|
|
351
|
+
# @param [Integer, Array(Integer), Range] *args
|
|
352
|
+
# the offset, offset and limit, or range indicating first and last position
|
|
230
353
|
#
|
|
231
|
-
# @
|
|
354
|
+
# @return [Resource, Collection]
|
|
355
|
+
# The entry which resides at that offset and limit, or
|
|
356
|
+
# a new Collection object with the set limits and offset
|
|
357
|
+
# @return [Resource, Collection, nil]
|
|
358
|
+
# The offset is out of range
|
|
359
|
+
#
|
|
360
|
+
# @api public
|
|
361
|
+
def slice!(*args)
|
|
362
|
+
removed = super
|
|
363
|
+
|
|
364
|
+
resources_removed(removed) unless removed.nil?
|
|
365
|
+
|
|
366
|
+
# Workaround for Ruby <= 1.8.6
|
|
367
|
+
compact! if RUBY_VERSION <= '1.8.6'
|
|
368
|
+
|
|
369
|
+
unless removed.kind_of?(Enumerable)
|
|
370
|
+
return removed
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
offset, limit = extract_slice_arguments(*args)
|
|
374
|
+
|
|
375
|
+
query = sliced_query(offset, limit)
|
|
376
|
+
|
|
377
|
+
new_collection(query, removed)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Splice a list of Resources at a given offset or range
|
|
381
|
+
#
|
|
382
|
+
# When nil is provided instead of a Resource or a list of Resources
|
|
383
|
+
# this will remove all of the Resources at the specified position.
|
|
384
|
+
#
|
|
385
|
+
# @param [Integer, Array(Integer), Range] *args
|
|
386
|
+
# The offset, offset and limit, or range indicating first and last position.
|
|
387
|
+
# The last argument may be a Resource, a list of Resources or nil.
|
|
388
|
+
#
|
|
389
|
+
# @return [Resource, Enumerable]
|
|
390
|
+
# the Resource or list of Resources that was spliced into the Collection
|
|
391
|
+
# @return [nil]
|
|
392
|
+
# If nil was used to delete the entries
|
|
393
|
+
#
|
|
394
|
+
# @api public
|
|
395
|
+
def []=(*args)
|
|
396
|
+
orphans = Array(superclass_slice(*args[0..-2]))
|
|
397
|
+
|
|
398
|
+
# relate new resources
|
|
399
|
+
resources = resources_added(super)
|
|
400
|
+
|
|
401
|
+
# mark resources as removed
|
|
402
|
+
resources_removed(orphans - loaded_entries)
|
|
403
|
+
|
|
404
|
+
# ensure remaining orphans are still related
|
|
405
|
+
(orphans & loaded_entries).each { |resource| relate_resource(resource) }
|
|
406
|
+
|
|
407
|
+
resources
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
alias splice []=
|
|
411
|
+
|
|
412
|
+
# Return a copy of the Collection sorted in reverse
|
|
413
|
+
#
|
|
414
|
+
# @return [Collection]
|
|
415
|
+
# Collection equal to +self+ but ordered in reverse
|
|
232
416
|
#
|
|
233
417
|
# @api public
|
|
234
418
|
def reverse
|
|
235
|
-
|
|
419
|
+
dup.reverse!
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Return the Collection sorted in reverse
|
|
423
|
+
#
|
|
424
|
+
# @return [self]
|
|
425
|
+
#
|
|
426
|
+
# @api public
|
|
427
|
+
def reverse!
|
|
428
|
+
query.reverse!
|
|
429
|
+
|
|
430
|
+
# reverse without kicking if possible
|
|
431
|
+
if loaded?
|
|
432
|
+
@array.reverse!
|
|
433
|
+
else
|
|
434
|
+
# reverse and swap the head and tail
|
|
435
|
+
@head, @tail = tail.reverse!, head.reverse!
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
self
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Invoke the block for each resource and replace it the return value
|
|
442
|
+
#
|
|
443
|
+
# @yield [Resource] Each resource in the collection
|
|
444
|
+
#
|
|
445
|
+
# @return [self]
|
|
446
|
+
#
|
|
447
|
+
# @api public
|
|
448
|
+
def collect!
|
|
449
|
+
super { |resource| resource_added(yield(resource_removed(resource))) }
|
|
236
450
|
end
|
|
237
451
|
|
|
238
|
-
|
|
239
|
-
|
|
452
|
+
alias map! collect!
|
|
453
|
+
|
|
454
|
+
# Append one Resource to the Collection and relate it
|
|
455
|
+
#
|
|
456
|
+
# @param [Resource] resource
|
|
457
|
+
# the resource to add to this collection
|
|
458
|
+
#
|
|
459
|
+
# @return [self]
|
|
240
460
|
#
|
|
241
461
|
# @api public
|
|
242
462
|
def <<(resource)
|
|
463
|
+
if resource.kind_of?(Hash)
|
|
464
|
+
resource = new(resource)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
resource_added(resource)
|
|
468
|
+
super
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Appends the resources to self
|
|
472
|
+
#
|
|
473
|
+
# @param [Enumerable] resources
|
|
474
|
+
# List of Resources to append to the collection
|
|
475
|
+
#
|
|
476
|
+
# @return [self]
|
|
477
|
+
#
|
|
478
|
+
# @api public
|
|
479
|
+
def concat(resources)
|
|
480
|
+
resources_added(resources)
|
|
243
481
|
super
|
|
244
|
-
relate_resource(resource)
|
|
245
|
-
self
|
|
246
482
|
end
|
|
247
483
|
|
|
248
|
-
|
|
249
|
-
#
|
|
484
|
+
# Append one or more Resources to the Collection
|
|
485
|
+
#
|
|
486
|
+
# This should append one or more Resources to the Collection and
|
|
487
|
+
# relate each to the Collection.
|
|
488
|
+
#
|
|
489
|
+
# @param [Enumerable] *resources
|
|
490
|
+
# List of Resources to append
|
|
491
|
+
#
|
|
492
|
+
# @return [self]
|
|
250
493
|
#
|
|
251
494
|
# @api public
|
|
252
495
|
def push(*resources)
|
|
496
|
+
resources_added(resources)
|
|
253
497
|
super
|
|
254
|
-
resources.each { |resource| relate_resource(resource) }
|
|
255
|
-
self
|
|
256
498
|
end
|
|
257
499
|
|
|
258
|
-
|
|
259
|
-
#
|
|
500
|
+
# Prepend one or more Resources to the Collection
|
|
501
|
+
#
|
|
502
|
+
# This should prepend one or more Resources to the Collection and
|
|
503
|
+
# relate each to the Collection.
|
|
504
|
+
#
|
|
505
|
+
# @param [Enumerable] *resources
|
|
506
|
+
# The Resources to prepend
|
|
507
|
+
#
|
|
508
|
+
# @return [self]
|
|
260
509
|
#
|
|
261
510
|
# @api public
|
|
262
511
|
def unshift(*resources)
|
|
512
|
+
resources_added(resources)
|
|
263
513
|
super
|
|
264
|
-
resources.each { |resource| relate_resource(resource) }
|
|
265
|
-
self
|
|
266
514
|
end
|
|
267
515
|
|
|
268
|
-
|
|
269
|
-
#
|
|
516
|
+
# Inserts the Resources before the Resource at the offset (which may be negative).
|
|
517
|
+
#
|
|
518
|
+
# @param [Integer] offset
|
|
519
|
+
# The offset to insert the Resources before
|
|
520
|
+
# @param [Enumerable] *resources
|
|
521
|
+
# List of Resources to insert
|
|
522
|
+
#
|
|
523
|
+
# @return [self]
|
|
270
524
|
#
|
|
271
525
|
# @api public
|
|
272
|
-
def
|
|
273
|
-
|
|
274
|
-
each { |resource| orphan_resource(resource) }
|
|
275
|
-
end
|
|
526
|
+
def insert(offset, *resources)
|
|
527
|
+
resources_added(resources)
|
|
276
528
|
super
|
|
277
|
-
other.each { |resource| relate_resource(resource) }
|
|
278
|
-
self
|
|
279
529
|
end
|
|
280
530
|
|
|
281
|
-
|
|
282
|
-
#
|
|
531
|
+
# Removes and returns the last Resource in the Collection
|
|
532
|
+
#
|
|
533
|
+
# @return [Resource]
|
|
534
|
+
# the last Resource in the Collection
|
|
283
535
|
#
|
|
284
536
|
# @api public
|
|
285
537
|
def pop
|
|
286
|
-
|
|
538
|
+
return nil unless resource = super
|
|
539
|
+
resource_removed(resource)
|
|
287
540
|
end
|
|
288
541
|
|
|
289
|
-
|
|
290
|
-
#
|
|
542
|
+
# Removes and returns the first Resource in the Collection
|
|
543
|
+
#
|
|
544
|
+
# @return [Resource]
|
|
545
|
+
# the first Resource in the Collection
|
|
291
546
|
#
|
|
292
547
|
# @api public
|
|
293
548
|
def shift
|
|
294
|
-
|
|
549
|
+
return nil unless resource = super
|
|
550
|
+
resource_removed(resource)
|
|
295
551
|
end
|
|
296
552
|
|
|
297
|
-
|
|
298
|
-
#
|
|
553
|
+
# Remove Resource from the Collection
|
|
554
|
+
#
|
|
555
|
+
# This should remove an included Resource from the Collection and
|
|
556
|
+
# orphan it from the Collection. If the Resource is not within the
|
|
557
|
+
# Collection, it should return nil.
|
|
558
|
+
#
|
|
559
|
+
# @param [Resource] resource the Resource to remove from
|
|
560
|
+
# the Collection
|
|
561
|
+
#
|
|
562
|
+
# @return [Resource]
|
|
563
|
+
# If +resource+ is within the Collection
|
|
564
|
+
# @return [nil]
|
|
565
|
+
# If +resource+ is not within the Collection
|
|
299
566
|
#
|
|
300
567
|
# @api public
|
|
301
568
|
def delete(resource)
|
|
302
|
-
|
|
569
|
+
return nil unless resource = super
|
|
570
|
+
resource_removed(resource)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Remove Resource from the Collection by offset
|
|
574
|
+
#
|
|
575
|
+
# This should remove the Resource from the Collection at a given
|
|
576
|
+
# offset and orphan it from the Collection. If the offset is out of
|
|
577
|
+
# range return nil.
|
|
578
|
+
#
|
|
579
|
+
# @param [Integer] offset
|
|
580
|
+
# the offset of the Resource to remove from the Collection
|
|
581
|
+
#
|
|
582
|
+
# @return [Resource]
|
|
583
|
+
# If +offset+ is within the Collection
|
|
584
|
+
# @return [nil]
|
|
585
|
+
# If +offset+ is not within the Collection
|
|
586
|
+
#
|
|
587
|
+
# @api public
|
|
588
|
+
def delete_at(offset)
|
|
589
|
+
return nil unless resource = super
|
|
590
|
+
resource_removed(resource)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Deletes every Resource for which block evaluates to true.
|
|
594
|
+
#
|
|
595
|
+
# @yield [Resource] Each resource in the Collection
|
|
596
|
+
#
|
|
597
|
+
# @return [self]
|
|
598
|
+
#
|
|
599
|
+
# @api public
|
|
600
|
+
def delete_if
|
|
601
|
+
super { |resource| yield(resource) && resource_removed(resource) }
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Deletes every Resource for which block evaluates to true
|
|
605
|
+
#
|
|
606
|
+
# @yield [Resource] Each resource in the Collection
|
|
607
|
+
#
|
|
608
|
+
# @return [Collection]
|
|
609
|
+
# If resources were removed
|
|
610
|
+
# @return [nil]
|
|
611
|
+
# If no resources were removed
|
|
612
|
+
#
|
|
613
|
+
# @api public
|
|
614
|
+
def reject!
|
|
615
|
+
super { |resource| yield(resource) && resource_removed(resource) }
|
|
303
616
|
end
|
|
304
617
|
|
|
305
|
-
|
|
306
|
-
#
|
|
618
|
+
# Replace the Resources within the Collection
|
|
619
|
+
#
|
|
620
|
+
# @param [Enumerable] other
|
|
621
|
+
# List of other Resources to replace with
|
|
622
|
+
#
|
|
623
|
+
# @return [self]
|
|
307
624
|
#
|
|
308
625
|
# @api public
|
|
309
|
-
def
|
|
310
|
-
|
|
626
|
+
def replace(other)
|
|
627
|
+
other = other.map do |resource|
|
|
628
|
+
if resource.kind_of?(Hash)
|
|
629
|
+
new(resource)
|
|
630
|
+
else
|
|
631
|
+
resource
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
if loaded?
|
|
636
|
+
resources_removed(self - other)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
super(resources_added(other))
|
|
311
640
|
end
|
|
312
641
|
|
|
313
|
-
|
|
314
|
-
#
|
|
642
|
+
# Access Collection#replace directly
|
|
643
|
+
#
|
|
644
|
+
# @api private
|
|
645
|
+
alias collection_replace replace
|
|
646
|
+
private :collection_replace
|
|
647
|
+
|
|
648
|
+
# Removes all Resources from the Collection
|
|
649
|
+
#
|
|
650
|
+
# This should remove and orphan each Resource from the Collection
|
|
651
|
+
#
|
|
652
|
+
# @return [self]
|
|
315
653
|
#
|
|
316
654
|
# @api public
|
|
317
655
|
def clear
|
|
318
656
|
if loaded?
|
|
319
|
-
|
|
657
|
+
resources_removed(self)
|
|
320
658
|
end
|
|
321
659
|
super
|
|
322
|
-
self
|
|
323
660
|
end
|
|
324
661
|
|
|
325
|
-
#
|
|
662
|
+
# Finds the first Resource by conditions, or initializes a new
|
|
663
|
+
# Resource with the attributes if none found
|
|
326
664
|
#
|
|
327
|
-
# @param Hash
|
|
328
|
-
#
|
|
665
|
+
# @param [Hash] conditions
|
|
666
|
+
# The conditions to be used to search
|
|
667
|
+
# @param [Hash] attributes
|
|
668
|
+
# The attributes to be used to initialize the resource with if none found
|
|
669
|
+
# @return [Resource]
|
|
670
|
+
# The instance found by +query+, or created with +attributes+ if none found
|
|
329
671
|
#
|
|
330
672
|
# @api public
|
|
331
|
-
def
|
|
332
|
-
|
|
333
|
-
resource = model.new(default_attributes.merge(attributes))
|
|
334
|
-
self << resource
|
|
335
|
-
resource
|
|
336
|
-
end
|
|
673
|
+
def first_or_new(conditions = {}, attributes = {})
|
|
674
|
+
first(conditions) || new(conditions.merge(attributes))
|
|
337
675
|
end
|
|
338
676
|
|
|
339
|
-
|
|
340
|
-
#
|
|
677
|
+
# Finds the first Resource by conditions, or creates a new
|
|
678
|
+
# Resource with the attributes if none found
|
|
341
679
|
#
|
|
342
|
-
# @param Hash
|
|
343
|
-
#
|
|
680
|
+
# @param [Hash] conditions
|
|
681
|
+
# The conditions to be used to search
|
|
682
|
+
# @param [Hash] attributes
|
|
683
|
+
# The attributes to be used to create the resource with if none found
|
|
684
|
+
# @return [Resource]
|
|
685
|
+
# The instance found by +query+, or created with +attributes+ if none found
|
|
686
|
+
#
|
|
687
|
+
# @api public
|
|
688
|
+
def first_or_create(conditions = {}, attributes = {})
|
|
689
|
+
first(conditions) || create(conditions.merge(attributes))
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Initializes a Resource and appends it to the Collection
|
|
693
|
+
#
|
|
694
|
+
# @param [Hash] attributes
|
|
695
|
+
# Attributes with which to initialize the new resource
|
|
696
|
+
#
|
|
697
|
+
# @return [Resource]
|
|
698
|
+
# a new Resource initialized with +attributes+
|
|
699
|
+
#
|
|
700
|
+
# @api public
|
|
701
|
+
def new(attributes = {})
|
|
702
|
+
resource = repository.scope { model.new(attributes) }
|
|
703
|
+
self << resource
|
|
704
|
+
resource
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Create a Resource in the Collection
|
|
708
|
+
#
|
|
709
|
+
# @param [Hash(Symbol => Object)] attributes
|
|
710
|
+
# attributes to set
|
|
711
|
+
#
|
|
712
|
+
# @return [Resource]
|
|
713
|
+
# the newly created Resource instance
|
|
344
714
|
#
|
|
345
715
|
# @api public
|
|
346
716
|
def create(attributes = {})
|
|
347
|
-
|
|
348
|
-
resource = model.create(default_attributes.merge(attributes))
|
|
349
|
-
self << resource unless resource.new_record?
|
|
350
|
-
resource
|
|
351
|
-
end
|
|
717
|
+
_create(true, attributes)
|
|
352
718
|
end
|
|
353
719
|
|
|
354
|
-
|
|
355
|
-
|
|
720
|
+
# Create a Resource in the Collection, bypassing hooks
|
|
721
|
+
#
|
|
722
|
+
# @param [Hash(Symbol => Object)] attributes
|
|
723
|
+
# attributes to set
|
|
724
|
+
#
|
|
725
|
+
# @return [Resource]
|
|
726
|
+
# the newly created Resource instance
|
|
727
|
+
#
|
|
728
|
+
# @api public
|
|
729
|
+
def create!(attributes = {})
|
|
730
|
+
_create(false, attributes)
|
|
356
731
|
end
|
|
357
732
|
|
|
358
|
-
|
|
359
|
-
# batch updates the entries belongs to this collection, and skip
|
|
360
|
-
# validations for all resources.
|
|
733
|
+
# Update every Resource in the Collection
|
|
361
734
|
#
|
|
362
|
-
#
|
|
363
|
-
# Person.all(:age.gte => 21).update!(:allow_beer => true)
|
|
735
|
+
# Person.all(:age.gte => 21).update(:allow_beer => true)
|
|
364
736
|
#
|
|
365
|
-
# @param
|
|
366
|
-
#
|
|
367
|
-
# will have loaded resources reflect updates.
|
|
737
|
+
# @param [Hash] attributes
|
|
738
|
+
# attributes to update with
|
|
368
739
|
#
|
|
369
|
-
# @return [
|
|
370
|
-
#
|
|
371
|
-
# FalseClass indicates that some entries were affected
|
|
740
|
+
# @return [Boolean]
|
|
741
|
+
# true if the resources were successfully updated
|
|
372
742
|
#
|
|
373
743
|
# @api public
|
|
374
|
-
def update
|
|
375
|
-
|
|
376
|
-
return true if attributes.empty?
|
|
744
|
+
def update(attributes = {})
|
|
745
|
+
assert_update_clean_only(:update)
|
|
377
746
|
|
|
378
|
-
dirty_attributes =
|
|
747
|
+
dirty_attributes = model.new(attributes).dirty_attributes
|
|
748
|
+
dirty_attributes.empty? || all? { |resource| resource.update(attributes) }
|
|
749
|
+
end
|
|
379
750
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
751
|
+
# Update every Resource in the Collection bypassing validation
|
|
752
|
+
#
|
|
753
|
+
# Person.all(:age.gte => 21).update!(:allow_beer => true)
|
|
754
|
+
#
|
|
755
|
+
# @param [Hash] attributes
|
|
756
|
+
# attributes to update
|
|
757
|
+
#
|
|
758
|
+
# @return [Boolean]
|
|
759
|
+
# true if the resources were successfully updated
|
|
760
|
+
#
|
|
761
|
+
# @api public
|
|
762
|
+
def update!(attributes = {})
|
|
763
|
+
assert_update_clean_only(:update!)
|
|
383
764
|
|
|
384
|
-
|
|
385
|
-
# each { |resource| resource.attributes = attributes } if loaded?
|
|
765
|
+
dirty_attributes = model.new(attributes).dirty_attributes
|
|
386
766
|
|
|
387
|
-
|
|
767
|
+
if dirty_attributes.empty?
|
|
768
|
+
true
|
|
769
|
+
elsif dirty_attributes.any? { |property, value| !property.nullable? && value.nil? }
|
|
770
|
+
false
|
|
771
|
+
else
|
|
772
|
+
unless _update(dirty_attributes)
|
|
773
|
+
return false
|
|
774
|
+
end
|
|
388
775
|
|
|
389
|
-
|
|
390
|
-
|
|
776
|
+
if loaded?
|
|
777
|
+
each do |resource|
|
|
778
|
+
dirty_attributes.each { |property, value| property.set!(resource, value) }
|
|
779
|
+
repository.identity_map(model)[resource.key] = resource
|
|
780
|
+
end
|
|
781
|
+
end
|
|
391
782
|
|
|
392
|
-
|
|
393
|
-
reload_query = @key_properties.zip(identity_map.keys.transpose).to_hash
|
|
394
|
-
model.all(reload_query.merge(attributes)).reload(:fields => attributes.keys)
|
|
783
|
+
true
|
|
395
784
|
end
|
|
785
|
+
end
|
|
396
786
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
787
|
+
# Save every Resource in the Collection
|
|
788
|
+
#
|
|
789
|
+
# @return [Boolean]
|
|
790
|
+
# true if the resources were successfully saved
|
|
791
|
+
#
|
|
792
|
+
# @api public
|
|
793
|
+
def save
|
|
794
|
+
_save(true)
|
|
401
795
|
end
|
|
402
796
|
|
|
797
|
+
# Save every Resource in the Collection bypassing validation
|
|
798
|
+
#
|
|
799
|
+
# @return [Boolean]
|
|
800
|
+
# true if the resources were successfully saved
|
|
801
|
+
#
|
|
802
|
+
# @api public
|
|
803
|
+
def save!
|
|
804
|
+
_save(false)
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Remove every Resource in the Collection from the repository
|
|
808
|
+
#
|
|
809
|
+
# This performs a deletion of each Resource in the Collection from
|
|
810
|
+
# the repository and clears the Collection.
|
|
811
|
+
#
|
|
812
|
+
# @return [Boolean]
|
|
813
|
+
# true if the resources were successfully destroyed
|
|
814
|
+
#
|
|
815
|
+
# @api public
|
|
403
816
|
def destroy
|
|
404
|
-
|
|
817
|
+
if destroyed = all? { |resource| resource.destroy }
|
|
818
|
+
clear
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
destroyed
|
|
405
822
|
end
|
|
406
823
|
|
|
407
|
-
|
|
408
|
-
# batch destroy the entries belongs to this collection, and skip
|
|
409
|
-
# validations for all resources.
|
|
824
|
+
# Remove all Resources from the repository, bypassing validation
|
|
410
825
|
#
|
|
411
|
-
#
|
|
412
|
-
#
|
|
826
|
+
# This performs a deletion of each Resource in the Collection from
|
|
827
|
+
# the repository and clears the Collection while skipping
|
|
828
|
+
# validation.
|
|
413
829
|
#
|
|
414
|
-
# @return [
|
|
415
|
-
#
|
|
416
|
-
# FalseClass indicates that some entries were affected
|
|
830
|
+
# @return [Boolean]
|
|
831
|
+
# true if the resources were successfully destroyed
|
|
417
832
|
#
|
|
418
833
|
# @api public
|
|
419
834
|
def destroy!
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
835
|
+
if query.limit || query.offset > 0 || query.links.any?
|
|
836
|
+
key = model.key(repository.name)
|
|
837
|
+
conditions = Query.target_conditions(self, key, key)
|
|
423
838
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
identity_map.delete(resource.key)
|
|
427
|
-
resource.dirty_attributes.clear
|
|
428
|
-
|
|
429
|
-
model.properties(repository.name).each do |property|
|
|
430
|
-
next unless resource.attribute_loaded?(property.name)
|
|
431
|
-
resource.dirty_attributes[property] = property.get(resource)
|
|
432
|
-
end
|
|
839
|
+
unless model.all(:repository => repository, :conditions => conditions).destroy!
|
|
840
|
+
return false
|
|
433
841
|
end
|
|
434
842
|
else
|
|
435
|
-
|
|
843
|
+
repository.delete(self)
|
|
844
|
+
mark_loaded
|
|
436
845
|
end
|
|
437
846
|
|
|
438
|
-
|
|
847
|
+
if loaded?
|
|
848
|
+
each { |resource| resource.reset }
|
|
849
|
+
clear
|
|
850
|
+
end
|
|
439
851
|
|
|
440
852
|
true
|
|
441
853
|
end
|
|
442
854
|
|
|
443
|
-
|
|
444
|
-
#
|
|
445
|
-
#
|
|
855
|
+
# Check to see if collection can respond to the method
|
|
856
|
+
#
|
|
857
|
+
# @param [Symbol] method
|
|
858
|
+
# method to check in the object
|
|
859
|
+
# @param [Boolean] include_private
|
|
860
|
+
# if set to true, collection will check private methods
|
|
861
|
+
#
|
|
862
|
+
# @return [Boolean]
|
|
863
|
+
# true if method can be responded to
|
|
446
864
|
#
|
|
447
865
|
# @api public
|
|
866
|
+
def respond_to?(method, include_private = false)
|
|
867
|
+
super || model.respond_to?(method) || relationships.key?(method)
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# Checks if all the resources have no changes to save
|
|
871
|
+
#
|
|
872
|
+
# @return [Boolean]
|
|
873
|
+
# true if the resource may not be persisted
|
|
874
|
+
#
|
|
875
|
+
# @api public
|
|
876
|
+
def clean?
|
|
877
|
+
!dirty?
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
# Checks if any resources have unsaved changes
|
|
881
|
+
#
|
|
882
|
+
# @return [Boolean]
|
|
883
|
+
# true if a resource may be persisted
|
|
884
|
+
#
|
|
885
|
+
# @api public
|
|
886
|
+
def dirty?
|
|
887
|
+
loaded_entries.any? { |resource| resource.dirty? }
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
# Gets a Human-readable representation of this collection,
|
|
891
|
+
# showing all elements contained in it
|
|
892
|
+
#
|
|
893
|
+
# @return [String]
|
|
894
|
+
# Human-readable representation of this collection, showing all elements
|
|
895
|
+
#
|
|
896
|
+
# @api public
|
|
897
|
+
def inspect
|
|
898
|
+
"[#{map { |resource| resource.inspect }.join(', ')}]"
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
# Returns the PropertySet representing the fields in the Collection scope
|
|
902
|
+
#
|
|
903
|
+
# @return [PropertySet]
|
|
904
|
+
# The set of properties this Collection's query will retrieve
|
|
905
|
+
#
|
|
906
|
+
# @api semipublic
|
|
448
907
|
def properties
|
|
449
908
|
PropertySet.new(query.fields)
|
|
450
909
|
end
|
|
451
910
|
|
|
452
|
-
|
|
453
|
-
# @return [DataMapper::Relationship] The model's relationships
|
|
911
|
+
# Returns the Relationships for the Collection's Model
|
|
454
912
|
#
|
|
455
|
-
# @
|
|
913
|
+
# @return [Hash]
|
|
914
|
+
# The model's relationships, mapping the name to the
|
|
915
|
+
# Associations::Relationship object
|
|
916
|
+
#
|
|
917
|
+
# @api semipublic
|
|
456
918
|
def relationships
|
|
457
919
|
model.relationships(repository.name)
|
|
458
920
|
end
|
|
459
921
|
|
|
460
|
-
|
|
461
|
-
|
|
922
|
+
private
|
|
923
|
+
|
|
924
|
+
# Initializes a new Collection identified by the query
|
|
462
925
|
#
|
|
463
|
-
# @
|
|
926
|
+
# @param [Query] query
|
|
927
|
+
# Scope the results of the Collection
|
|
928
|
+
# @param [Enumerable] resources (optional)
|
|
929
|
+
# List of resources to initialize the Collection with
|
|
464
930
|
#
|
|
465
|
-
# @
|
|
931
|
+
# @return [self]
|
|
466
932
|
#
|
|
467
|
-
# @api
|
|
468
|
-
def
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
933
|
+
# @api private
|
|
934
|
+
def initialize(query, resources = nil)
|
|
935
|
+
raise "#{self.class}#new with a block is deprecated" if block_given?
|
|
936
|
+
|
|
937
|
+
@query = query
|
|
938
|
+
@identity_map = IdentityMap.new
|
|
939
|
+
@removed = Set.new
|
|
472
940
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
941
|
+
super()
|
|
942
|
+
|
|
943
|
+
# TODO: change LazyArray to not use a load proc at all
|
|
944
|
+
remove_instance_variable(:@load_with_proc)
|
|
477
945
|
|
|
478
|
-
|
|
946
|
+
if resources
|
|
947
|
+
replace(resources)
|
|
479
948
|
end
|
|
480
|
-
default_attributes
|
|
481
949
|
end
|
|
482
950
|
|
|
483
|
-
|
|
484
|
-
# check to see if collection can respond to the method
|
|
951
|
+
# Copies the original Collection state
|
|
485
952
|
#
|
|
486
|
-
# @param
|
|
487
|
-
#
|
|
488
|
-
# collection will check private methods
|
|
953
|
+
# @param [Collection] original
|
|
954
|
+
# the original collection to copy from
|
|
489
955
|
#
|
|
490
|
-
# @return [
|
|
491
|
-
# TrueClass indicates the method can be responded to by the collection
|
|
492
|
-
# FalseClass indicates the method can not be responded to by the collection
|
|
956
|
+
# @return [undefined]
|
|
493
957
|
#
|
|
494
|
-
# @api
|
|
495
|
-
def
|
|
496
|
-
super
|
|
958
|
+
# @api private
|
|
959
|
+
def initialize_copy(original)
|
|
960
|
+
super
|
|
961
|
+
@query = @query.dup
|
|
962
|
+
@identity_map = @identity_map.dup
|
|
963
|
+
@removed = @removed.dup
|
|
497
964
|
end
|
|
498
965
|
|
|
499
|
-
#
|
|
966
|
+
# Test if the collection is loaded between the offset and limit
|
|
967
|
+
#
|
|
968
|
+
# @param [Integer] offset
|
|
969
|
+
# the offset of the collection to test
|
|
970
|
+
# @param [Integer] limit
|
|
971
|
+
# optional limit for how many entries to be loaded
|
|
972
|
+
#
|
|
973
|
+
# @return [Boolean]
|
|
974
|
+
# true if the collection is loaded from the offset to the limit
|
|
975
|
+
#
|
|
500
976
|
# @api private
|
|
501
|
-
def
|
|
502
|
-
|
|
977
|
+
def partially_loaded?(offset, limit = 1)
|
|
978
|
+
if offset >= 0
|
|
979
|
+
lazy_possible?(head, offset + limit)
|
|
980
|
+
else
|
|
981
|
+
lazy_possible?(tail, offset.abs)
|
|
982
|
+
end
|
|
503
983
|
end
|
|
504
984
|
|
|
505
|
-
#
|
|
985
|
+
# Lazy loads a Collection
|
|
986
|
+
#
|
|
987
|
+
# @return [self]
|
|
988
|
+
#
|
|
506
989
|
# @api private
|
|
507
|
-
def
|
|
508
|
-
|
|
990
|
+
def lazy_load
|
|
991
|
+
if loaded?
|
|
992
|
+
return self
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
mark_loaded
|
|
996
|
+
|
|
997
|
+
resources = repository.read(query)
|
|
998
|
+
|
|
999
|
+
# remove already known results
|
|
1000
|
+
resources -= head if head.any?
|
|
1001
|
+
resources -= tail if tail.any?
|
|
1002
|
+
resources -= @removed.to_a if @removed.any?
|
|
1003
|
+
|
|
1004
|
+
query.add_reversed? ? unshift(*resources.reverse) : concat(resources)
|
|
1005
|
+
|
|
1006
|
+
# TODO: DRY this up with LazyArray
|
|
1007
|
+
@array.unshift(*head)
|
|
1008
|
+
@array.concat(tail)
|
|
509
1009
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
# to initialize an object. This should be fixed in the edge
|
|
514
|
-
# branch dkubb/dm-core
|
|
1010
|
+
@head = @tail = nil
|
|
1011
|
+
@reapers.each { |resource| @array.delete_if(&resource) } if @reapers
|
|
1012
|
+
@array.freeze if frozen?
|
|
515
1013
|
|
|
516
|
-
|
|
517
|
-
collection.instance_variable_set(:@query, query)
|
|
518
|
-
collection.instance_variable_set(:@array, array)
|
|
519
|
-
collection.instance_variable_set(:@loaded, true)
|
|
520
|
-
collection.instance_variable_set(:@key_properties, collection.send(:model).key(collection.repository.name))
|
|
521
|
-
collection.instance_variable_set(:@cache, {})
|
|
522
|
-
collection
|
|
1014
|
+
self
|
|
523
1015
|
end
|
|
524
1016
|
|
|
525
|
-
|
|
1017
|
+
# Loaded Resources in the collection
|
|
1018
|
+
#
|
|
1019
|
+
# @return [Array<Resource>]
|
|
1020
|
+
# Resources in the collection
|
|
1021
|
+
#
|
|
1022
|
+
# @api private
|
|
1023
|
+
def loaded_entries
|
|
1024
|
+
loaded? ? self : head + tail
|
|
1025
|
+
end
|
|
526
1026
|
|
|
527
|
-
|
|
1027
|
+
# Initializes a new Collection
|
|
1028
|
+
#
|
|
1029
|
+
# @return [Collection]
|
|
1030
|
+
# A new Collection object
|
|
1031
|
+
#
|
|
528
1032
|
# @api private
|
|
529
|
-
def
|
|
530
|
-
|
|
1033
|
+
def new_collection(query, resources = nil, &block)
|
|
1034
|
+
if loaded?
|
|
1035
|
+
resources ||= filter(query)
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
# TOOD: figure out a way to pass not-yet-saved Resources to this newly
|
|
1039
|
+
# created Collection. If the new resource matches the conditions, then
|
|
1040
|
+
# it should be added to the collection (keep in mind limit/offset too)
|
|
1041
|
+
|
|
1042
|
+
self.class.new(query, resources, &block)
|
|
531
1043
|
end
|
|
532
1044
|
|
|
533
|
-
|
|
1045
|
+
# Creates a resource in the collection
|
|
1046
|
+
#
|
|
1047
|
+
# @param [Boolean] safe
|
|
1048
|
+
# Whether to use the safe or unsafe create
|
|
1049
|
+
# @param [Hash] attributes
|
|
1050
|
+
# Attributes with which to create the new resource
|
|
1051
|
+
#
|
|
1052
|
+
# @return [Resource]
|
|
1053
|
+
# a saved Resource
|
|
1054
|
+
#
|
|
1055
|
+
# @api private
|
|
1056
|
+
def _create(safe, attributes)
|
|
1057
|
+
resource = repository.scope { model.send(safe ? :create : :create!, default_attributes.merge(attributes)) }
|
|
1058
|
+
self << resource if resource.saved?
|
|
1059
|
+
resource
|
|
1060
|
+
end
|
|
534
1061
|
|
|
535
|
-
|
|
536
|
-
#
|
|
537
|
-
|
|
538
|
-
|
|
1062
|
+
# Updates a collection
|
|
1063
|
+
#
|
|
1064
|
+
# @return [Boolean]
|
|
1065
|
+
# Returns true if collection was updated
|
|
1066
|
+
#
|
|
1067
|
+
# @api private
|
|
1068
|
+
def _update(dirty_attributes)
|
|
1069
|
+
if query.limit || query.offset > 0 || query.links.any?
|
|
1070
|
+
attributes = dirty_attributes.map { |property, value| [ property.name, value ] }.to_hash
|
|
1071
|
+
|
|
1072
|
+
key = model.key(repository.name)
|
|
1073
|
+
conditions = Query.target_conditions(self, key, key)
|
|
539
1074
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1075
|
+
unless model.all(:repository => repository, :conditions => conditions).update!(attributes)
|
|
1076
|
+
return false
|
|
1077
|
+
end
|
|
1078
|
+
else
|
|
1079
|
+
repository.update(dirty_attributes, self)
|
|
543
1080
|
end
|
|
544
1081
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
@cache = {}
|
|
1082
|
+
true
|
|
1083
|
+
end
|
|
548
1084
|
|
|
549
|
-
|
|
1085
|
+
# Saves a collection
|
|
1086
|
+
#
|
|
1087
|
+
# @param [Symbol] method
|
|
1088
|
+
# The name of the Resource method to save the collection with
|
|
1089
|
+
#
|
|
1090
|
+
# @return [Boolean]
|
|
1091
|
+
# Returns true if collection was updated
|
|
1092
|
+
#
|
|
1093
|
+
# @api private
|
|
1094
|
+
def _save(safe)
|
|
1095
|
+
loaded_entries.each { |resource| set_default_attributes(resource) }
|
|
1096
|
+
@removed.clear
|
|
1097
|
+
loaded_entries.all? { |resource| resource.send(safe ? :save : :save!) }
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# Returns default values to initialize new Resources in the Collection
|
|
1101
|
+
#
|
|
1102
|
+
# @return [Hash] The default attributes for new instances in this Collection
|
|
1103
|
+
#
|
|
1104
|
+
# @api private
|
|
1105
|
+
def default_attributes
|
|
1106
|
+
@default_attributes ||=
|
|
1107
|
+
begin
|
|
1108
|
+
unless query.conditions.kind_of?(Query::Conditions::AndOperation)
|
|
1109
|
+
return
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
default_attributes = {}
|
|
1113
|
+
|
|
1114
|
+
repository_name = repository.name
|
|
1115
|
+
relationships = self.relationships.values
|
|
1116
|
+
properties = model.properties(repository_name)
|
|
1117
|
+
key = model.key(repository_name)
|
|
1118
|
+
|
|
1119
|
+
# if all the key properties are included in the conditions,
|
|
1120
|
+
# then do not allow them to be default attributes
|
|
1121
|
+
if query.condition_properties.to_set.superset?(key.to_set)
|
|
1122
|
+
properties -= key
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
query.conditions.each do |condition|
|
|
1126
|
+
unless condition.kind_of?(Query::Conditions::EqualToComparison) &&
|
|
1127
|
+
(properties.include?(condition.subject) || (condition.relationship? && condition.subject.source_model == model))
|
|
1128
|
+
next
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
default_attributes[condition.subject] = condition.value
|
|
1132
|
+
end
|
|
550
1133
|
|
|
551
|
-
|
|
1134
|
+
default_attributes.freeze
|
|
1135
|
+
end
|
|
552
1136
|
end
|
|
553
1137
|
|
|
554
|
-
|
|
1138
|
+
# Set the default attributes for a non-frozen resource
|
|
1139
|
+
#
|
|
1140
|
+
# @param [Resource] resource
|
|
1141
|
+
# the resource to set the default attributes for
|
|
1142
|
+
#
|
|
1143
|
+
# @return [undefined]
|
|
1144
|
+
#
|
|
555
1145
|
# @api private
|
|
556
|
-
def
|
|
557
|
-
|
|
558
|
-
|
|
1146
|
+
def set_default_attributes(resource)
|
|
1147
|
+
unless resource.frozen?
|
|
1148
|
+
resource.attributes = default_attributes
|
|
1149
|
+
end
|
|
559
1150
|
end
|
|
560
1151
|
|
|
561
|
-
|
|
1152
|
+
# Relates a Resource to the Collection
|
|
1153
|
+
#
|
|
1154
|
+
# This is used by SEL related code to reload a Resource and the
|
|
1155
|
+
# Collection it belongs to.
|
|
1156
|
+
#
|
|
1157
|
+
# @param [Resource] resource
|
|
1158
|
+
# The Resource to relate
|
|
1159
|
+
#
|
|
1160
|
+
# @return [Resource]
|
|
1161
|
+
# If Resource was successfully related
|
|
1162
|
+
# @return [nil]
|
|
1163
|
+
# If a nil resource was provided
|
|
1164
|
+
#
|
|
562
1165
|
# @api private
|
|
563
1166
|
def relate_resource(resource)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1167
|
+
unless resource.frozen?
|
|
1168
|
+
resource.collection = self
|
|
1169
|
+
end
|
|
1170
|
+
|
|
567
1171
|
resource
|
|
568
1172
|
end
|
|
569
1173
|
|
|
570
|
-
|
|
1174
|
+
# Orphans a Resource from the Collection
|
|
1175
|
+
#
|
|
1176
|
+
# Removes the association between the Resource and Collection so that
|
|
1177
|
+
# SEL related code will not load the Collection.
|
|
1178
|
+
#
|
|
1179
|
+
# @param [Resource] resource
|
|
1180
|
+
# The Resource to orphan
|
|
1181
|
+
#
|
|
1182
|
+
# @return [Resource]
|
|
1183
|
+
# The Resource that was orphaned
|
|
1184
|
+
# @return [nil]
|
|
1185
|
+
# If a nil resource was provided
|
|
1186
|
+
#
|
|
571
1187
|
# @api private
|
|
572
1188
|
def orphan_resource(resource)
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1189
|
+
if resource.collection.equal?(self) && !resource.frozen?
|
|
1190
|
+
resource.collection = nil
|
|
1191
|
+
end
|
|
1192
|
+
|
|
576
1193
|
resource
|
|
577
1194
|
end
|
|
578
1195
|
|
|
579
|
-
|
|
1196
|
+
# Track the added resource
|
|
1197
|
+
#
|
|
1198
|
+
# @param [Resource] resource
|
|
1199
|
+
# the resource that was added
|
|
1200
|
+
#
|
|
1201
|
+
# @return [Resource]
|
|
1202
|
+
# the resource that was added
|
|
1203
|
+
#
|
|
580
1204
|
# @api private
|
|
581
|
-
def
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1205
|
+
def resource_added(resource)
|
|
1206
|
+
if resource.saved?
|
|
1207
|
+
@identity_map[resource.key] = resource
|
|
1208
|
+
@removed.delete(resource)
|
|
1209
|
+
else
|
|
1210
|
+
set_default_attributes(resource)
|
|
1211
|
+
end
|
|
585
1212
|
|
|
586
|
-
|
|
1213
|
+
relate_resource(resource)
|
|
1214
|
+
end
|
|
587
1215
|
|
|
588
|
-
|
|
589
|
-
|
|
1216
|
+
# Track the added resources
|
|
1217
|
+
#
|
|
1218
|
+
# @param [Array<Resource>] resources
|
|
1219
|
+
# the resources that were added
|
|
1220
|
+
#
|
|
1221
|
+
# @return [Array<Resource>]
|
|
1222
|
+
# the resources that were added
|
|
1223
|
+
#
|
|
1224
|
+
# @api private
|
|
1225
|
+
def resources_added(resources)
|
|
1226
|
+
if resources.kind_of?(Enumerable)
|
|
1227
|
+
resources.each { |resource| resource_added(resource) }
|
|
590
1228
|
else
|
|
591
|
-
|
|
1229
|
+
resource_added(resources)
|
|
592
1230
|
end
|
|
1231
|
+
end
|
|
593
1232
|
|
|
594
|
-
|
|
595
|
-
|
|
1233
|
+
# Track the removed resource
|
|
1234
|
+
#
|
|
1235
|
+
# @param [Resource] resource
|
|
1236
|
+
# the resource that was removed
|
|
1237
|
+
#
|
|
1238
|
+
# @return [Resource]
|
|
1239
|
+
# the resource that was removed
|
|
1240
|
+
#
|
|
1241
|
+
# @api private
|
|
1242
|
+
def resource_removed(resource)
|
|
1243
|
+
if resource.saved?
|
|
1244
|
+
@identity_map.delete(resource.key)
|
|
1245
|
+
@removed << resource
|
|
596
1246
|
end
|
|
597
1247
|
|
|
598
|
-
|
|
1248
|
+
orphan_resource(resource)
|
|
599
1249
|
end
|
|
600
1250
|
|
|
601
|
-
|
|
1251
|
+
# Track the removed resources
|
|
1252
|
+
#
|
|
1253
|
+
# @param [Array<Resource>] resources
|
|
1254
|
+
# the resources that were removed
|
|
1255
|
+
#
|
|
1256
|
+
# @return [Array<Resource>]
|
|
1257
|
+
# the resources that were removed
|
|
1258
|
+
#
|
|
602
1259
|
# @api private
|
|
603
|
-
def
|
|
604
|
-
|
|
605
|
-
|
|
1260
|
+
def resources_removed(resources)
|
|
1261
|
+
if resources.kind_of?(Enumerable)
|
|
1262
|
+
resources.each { |resource| resource_removed(resource) }
|
|
1263
|
+
else
|
|
1264
|
+
resource_removed(resources)
|
|
1265
|
+
end
|
|
606
1266
|
end
|
|
607
1267
|
|
|
608
|
-
|
|
1268
|
+
# Filter resources in the collection based on a Query
|
|
1269
|
+
#
|
|
1270
|
+
# @param [Query] query
|
|
1271
|
+
# the query to match each resource in the collection
|
|
1272
|
+
#
|
|
1273
|
+
# @return [Array]
|
|
1274
|
+
# the resources that match the Query
|
|
1275
|
+
# @return [nil]
|
|
1276
|
+
# nil if no resources match the Query
|
|
1277
|
+
#
|
|
609
1278
|
# @api private
|
|
610
|
-
def
|
|
611
|
-
|
|
1279
|
+
def filter(query)
|
|
1280
|
+
fields = self.query.fields.to_set
|
|
1281
|
+
|
|
1282
|
+
if query.links.empty? &&
|
|
1283
|
+
(query.unique? || (!query.unique? && !self.query.unique?)) &&
|
|
1284
|
+
!query.reload? &&
|
|
1285
|
+
!query.raw? &&
|
|
1286
|
+
query.fields.to_set.subset?(fields) &&
|
|
1287
|
+
query.condition_properties.subset?(fields)
|
|
1288
|
+
then
|
|
1289
|
+
query.filter_records(to_a.dup)
|
|
1290
|
+
end
|
|
612
1291
|
end
|
|
613
1292
|
|
|
614
|
-
|
|
1293
|
+
# Return the absolute or relative scoped query
|
|
1294
|
+
#
|
|
1295
|
+
# @param [Query, Hash] query
|
|
1296
|
+
# the query to scope the collection with
|
|
1297
|
+
#
|
|
1298
|
+
# @return [Query]
|
|
1299
|
+
# the absolute or relative scoped query
|
|
1300
|
+
#
|
|
615
1301
|
# @api private
|
|
616
|
-
def
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
return if query.limit.nil? && self.query.limit.nil?
|
|
1302
|
+
def scoped_query(query)
|
|
1303
|
+
if query.kind_of?(Query)
|
|
1304
|
+
query.dup
|
|
1305
|
+
else
|
|
1306
|
+
self.query.relative(query)
|
|
622
1307
|
end
|
|
1308
|
+
end
|
|
623
1309
|
|
|
624
|
-
|
|
625
|
-
|
|
1310
|
+
# @api private
|
|
1311
|
+
def sliced_query(offset, limit)
|
|
1312
|
+
query = self.query
|
|
626
1313
|
|
|
627
|
-
if
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
end
|
|
1314
|
+
if offset >= 0
|
|
1315
|
+
query.slice(offset, limit)
|
|
1316
|
+
else
|
|
1317
|
+
query = query.slice((limit + offset).abs, limit).reverse!
|
|
632
1318
|
|
|
633
|
-
|
|
634
|
-
|
|
1319
|
+
# tell the Query to prepend each result from the adapter
|
|
1320
|
+
query.update(:add_reversed => !query.add_reversed?)
|
|
635
1321
|
end
|
|
636
|
-
|
|
637
|
-
query.update(:offset => first_pos)
|
|
638
|
-
query.update(:limit => last_pos - first_pos) if last_pos
|
|
639
1322
|
end
|
|
640
1323
|
|
|
641
|
-
|
|
642
|
-
#
|
|
1324
|
+
# Delegates to Model, Relationships or the superclass (LazyArray)
|
|
1325
|
+
#
|
|
1326
|
+
# When this receives a method that belongs to the Model the
|
|
1327
|
+
# Collection is scoped to, it will execute the method within the
|
|
1328
|
+
# same scope as the Collection and return the results.
|
|
1329
|
+
#
|
|
1330
|
+
# When this receives a method that is a relationship the Model has
|
|
1331
|
+
# defined, it will execute the association method within the same
|
|
1332
|
+
# scope as the Collection and return the results.
|
|
1333
|
+
#
|
|
1334
|
+
# Otherwise this method will delegate to a method in the superclass
|
|
1335
|
+
# (LazyArray) and return the results.
|
|
1336
|
+
#
|
|
1337
|
+
# @return [Object]
|
|
1338
|
+
# the return values of the delegated methods
|
|
1339
|
+
#
|
|
1340
|
+
# @api public
|
|
643
1341
|
def method_missing(method, *args, &block)
|
|
644
|
-
if model.
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
# subquery to scope the results rather than a join
|
|
1342
|
+
if model.model_method_defined?(method)
|
|
1343
|
+
delegate_to_model(method, *args, &block)
|
|
1344
|
+
elsif relationship = relationships[method] || relationships[method.to_s.singular.to_sym]
|
|
1345
|
+
delegate_to_relationship(relationship, *args)
|
|
1346
|
+
else
|
|
1347
|
+
super
|
|
1348
|
+
end
|
|
1349
|
+
end
|
|
653
1350
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1351
|
+
# Delegate the method to the Model
|
|
1352
|
+
#
|
|
1353
|
+
# @param [Symbol] method
|
|
1354
|
+
# the name of the method in the model to execute
|
|
1355
|
+
# @param [Array] *args
|
|
1356
|
+
# the arguments for the method
|
|
1357
|
+
#
|
|
1358
|
+
# @return [Object]
|
|
1359
|
+
# the return value of the model method
|
|
1360
|
+
#
|
|
1361
|
+
# @api private
|
|
1362
|
+
def delegate_to_model(method, *args, &block)
|
|
1363
|
+
model.__send__(:with_scope, query) do
|
|
1364
|
+
model.send(method, *args, &block)
|
|
1365
|
+
end
|
|
1366
|
+
end
|
|
658
1367
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1368
|
+
# Delegate the method to the Relationship
|
|
1369
|
+
#
|
|
1370
|
+
# @return [Collection]
|
|
1371
|
+
# the associated Resources
|
|
1372
|
+
#
|
|
1373
|
+
# @api private
|
|
1374
|
+
def delegate_to_relationship(relationship, query = nil)
|
|
1375
|
+
relationship.eager_load(self, query)
|
|
1376
|
+
end
|
|
663
1377
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1378
|
+
# Raises an exception if #update is performed on a dirty resource
|
|
1379
|
+
#
|
|
1380
|
+
# @raise [UpdateConflictError]
|
|
1381
|
+
# raise if the resource is dirty
|
|
1382
|
+
#
|
|
1383
|
+
# @return [undefined]
|
|
1384
|
+
#
|
|
1385
|
+
# @api private
|
|
1386
|
+
def assert_update_clean_only(method)
|
|
1387
|
+
if dirty?
|
|
1388
|
+
raise UpdateConflictError, "##{method} cannot be called on a dirty collection"
|
|
667
1389
|
end
|
|
668
1390
|
end
|
|
669
1391
|
end # class Collection
|