datamapper-dm-core 0.9.11 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.autotest +17 -14
- data/.gitignore +3 -1
- data/FAQ +6 -5
- data/History.txt +5 -39
- data/Manifest.txt +67 -76
- data/QUICKLINKS +1 -1
- data/README.txt +21 -15
- data/Rakefile +16 -15
- data/SPECS +2 -29
- data/TODO +1 -1
- data/dm-core.gemspec +11 -15
- data/lib/dm-core/adapters/abstract_adapter.rb +182 -185
- data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
- data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
- data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
- data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
- data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
- data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
- data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
- data/lib/dm-core/adapters.rb +135 -16
- data/lib/dm-core/associations/many_to_many.rb +372 -90
- data/lib/dm-core/associations/many_to_one.rb +220 -73
- data/lib/dm-core/associations/one_to_many.rb +319 -255
- data/lib/dm-core/associations/one_to_one.rb +66 -53
- data/lib/dm-core/associations/relationship.rb +560 -158
- data/lib/dm-core/collection.rb +1104 -381
- data/lib/dm-core/core_ext/kernel.rb +12 -0
- data/lib/dm-core/core_ext/symbol.rb +10 -0
- data/lib/dm-core/identity_map.rb +4 -34
- data/lib/dm-core/migrations.rb +1283 -0
- data/lib/dm-core/model/descendant_set.rb +81 -0
- data/lib/dm-core/model/hook.rb +45 -0
- data/lib/dm-core/model/is.rb +32 -0
- data/lib/dm-core/model/property.rb +248 -0
- data/lib/dm-core/model/relationship.rb +335 -0
- data/lib/dm-core/model/scope.rb +90 -0
- data/lib/dm-core/model.rb +570 -369
- data/lib/dm-core/property.rb +753 -280
- data/lib/dm-core/property_set.rb +141 -98
- data/lib/dm-core/query/conditions/comparison.rb +814 -0
- data/lib/dm-core/query/conditions/operation.rb +247 -0
- data/lib/dm-core/query/direction.rb +43 -0
- data/lib/dm-core/query/operator.rb +42 -0
- data/lib/dm-core/query/path.rb +102 -0
- data/lib/dm-core/query/sort.rb +45 -0
- data/lib/dm-core/query.rb +974 -492
- data/lib/dm-core/repository.rb +147 -107
- data/lib/dm-core/resource.rb +644 -429
- data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
- data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
- data/lib/dm-core/support/chainable.rb +20 -0
- data/lib/dm-core/support/deprecate.rb +12 -0
- data/lib/dm-core/support/equalizer.rb +23 -0
- data/lib/dm-core/support/logger.rb +13 -0
- data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
- data/lib/dm-core/transaction.rb +333 -92
- data/lib/dm-core/type.rb +98 -60
- data/lib/dm-core/types/boolean.rb +1 -1
- data/lib/dm-core/types/discriminator.rb +34 -20
- data/lib/dm-core/types/object.rb +7 -4
- data/lib/dm-core/types/paranoid_boolean.rb +11 -9
- data/lib/dm-core/types/paranoid_datetime.rb +11 -9
- data/lib/dm-core/types/serial.rb +3 -3
- data/lib/dm-core/types/text.rb +3 -4
- data/lib/dm-core/version.rb +1 -1
- data/lib/dm-core.rb +106 -110
- data/script/performance.rb +102 -109
- data/script/profile.rb +169 -38
- data/spec/lib/adapter_helpers.rb +105 -0
- data/spec/lib/collection_helpers.rb +18 -0
- data/spec/lib/counter_adapter.rb +34 -0
- data/spec/lib/pending_helpers.rb +27 -0
- data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
- data/spec/public/associations/many_to_many_spec.rb +193 -0
- data/spec/public/associations/many_to_one_spec.rb +73 -0
- data/spec/public/associations/one_to_many_spec.rb +77 -0
- data/spec/public/associations/one_to_one_spec.rb +156 -0
- data/spec/public/collection_spec.rb +65 -0
- data/spec/public/model/relationship_spec.rb +924 -0
- data/spec/public/model_spec.rb +159 -0
- data/spec/public/property_spec.rb +829 -0
- data/spec/public/resource_spec.rb +71 -0
- data/spec/public/sel_spec.rb +44 -0
- data/spec/public/setup_spec.rb +145 -0
- data/spec/public/shared/association_collection_shared_spec.rb +317 -0
- data/spec/public/shared/collection_shared_spec.rb +1723 -0
- data/spec/public/shared/finder_shared_spec.rb +1619 -0
- data/spec/public/shared/resource_shared_spec.rb +924 -0
- data/spec/public/shared/sel_shared_spec.rb +112 -0
- data/spec/public/transaction_spec.rb +129 -0
- data/spec/public/types/discriminator_spec.rb +130 -0
- data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
- data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
- data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
- data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
- data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
- data/spec/semipublic/associations/relationship_spec.rb +194 -0
- data/spec/semipublic/associations_spec.rb +177 -0
- data/spec/semipublic/collection_spec.rb +142 -0
- data/spec/semipublic/property_spec.rb +61 -0
- data/spec/semipublic/query/conditions_spec.rb +528 -0
- data/spec/semipublic/query/path_spec.rb +443 -0
- data/spec/semipublic/query_spec.rb +2626 -0
- data/spec/semipublic/resource_spec.rb +47 -0
- data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
- data/spec/spec.opts +3 -1
- data/spec/spec_helper.rb +80 -57
- data/tasks/ci.rb +19 -31
- data/tasks/dm.rb +43 -48
- data/tasks/doc.rb +8 -11
- data/tasks/gemspec.rb +5 -5
- data/tasks/hoe.rb +15 -16
- data/tasks/install.rb +8 -10
- metadata +72 -93
- data/lib/dm-core/associations/relationship_chain.rb +0 -81
- data/lib/dm-core/associations.rb +0 -207
- data/lib/dm-core/auto_migrations.rb +0 -105
- data/lib/dm-core/dependency_queue.rb +0 -32
- data/lib/dm-core/hook.rb +0 -11
- data/lib/dm-core/is.rb +0 -16
- data/lib/dm-core/logger.rb +0 -232
- data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
- data/lib/dm-core/migrator.rb +0 -29
- data/lib/dm-core/scope.rb +0 -58
- data/lib/dm-core/support/array.rb +0 -13
- data/lib/dm-core/support/assertions.rb +0 -8
- data/lib/dm-core/support/errors.rb +0 -23
- data/lib/dm-core/support/kernel.rb +0 -11
- data/lib/dm-core/support/symbol.rb +0 -41
- data/lib/dm-core/support.rb +0 -7
- data/lib/dm-core/type_map.rb +0 -80
- data/lib/dm-core/types.rb +0 -19
- data/script/all +0 -4
- data/spec/integration/association_spec.rb +0 -1382
- data/spec/integration/association_through_spec.rb +0 -203
- data/spec/integration/associations/many_to_many_spec.rb +0 -449
- data/spec/integration/associations/many_to_one_spec.rb +0 -163
- data/spec/integration/associations/one_to_many_spec.rb +0 -188
- data/spec/integration/auto_migrations_spec.rb +0 -413
- data/spec/integration/collection_spec.rb +0 -1073
- data/spec/integration/data_objects_adapter_spec.rb +0 -32
- data/spec/integration/dependency_queue_spec.rb +0 -46
- data/spec/integration/model_spec.rb +0 -197
- data/spec/integration/mysql_adapter_spec.rb +0 -85
- data/spec/integration/postgres_adapter_spec.rb +0 -731
- data/spec/integration/property_spec.rb +0 -253
- data/spec/integration/query_spec.rb +0 -514
- data/spec/integration/repository_spec.rb +0 -61
- data/spec/integration/resource_spec.rb +0 -513
- data/spec/integration/sqlite3_adapter_spec.rb +0 -352
- data/spec/integration/sti_spec.rb +0 -273
- data/spec/integration/strategic_eager_loading_spec.rb +0 -156
- data/spec/integration/transaction_spec.rb +0 -75
- data/spec/integration/type_spec.rb +0 -275
- data/spec/lib/logging_helper.rb +0 -18
- data/spec/lib/mock_adapter.rb +0 -27
- data/spec/lib/model_loader.rb +0 -100
- data/spec/lib/publicize_methods.rb +0 -28
- data/spec/models/content.rb +0 -16
- data/spec/models/vehicles.rb +0 -34
- data/spec/models/zoo.rb +0 -48
- data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
- data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
- data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
- data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
- data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
- data/spec/unit/associations/many_to_many_spec.rb +0 -32
- data/spec/unit/associations/many_to_one_spec.rb +0 -159
- data/spec/unit/associations/one_to_many_spec.rb +0 -393
- data/spec/unit/associations/one_to_one_spec.rb +0 -7
- data/spec/unit/associations/relationship_spec.rb +0 -71
- data/spec/unit/associations_spec.rb +0 -242
- data/spec/unit/auto_migrations_spec.rb +0 -111
- data/spec/unit/collection_spec.rb +0 -182
- data/spec/unit/data_mapper_spec.rb +0 -35
- data/spec/unit/identity_map_spec.rb +0 -126
- data/spec/unit/is_spec.rb +0 -80
- data/spec/unit/migrator_spec.rb +0 -33
- data/spec/unit/model_spec.rb +0 -321
- data/spec/unit/naming_conventions_spec.rb +0 -36
- data/spec/unit/property_set_spec.rb +0 -90
- data/spec/unit/property_spec.rb +0 -753
- data/spec/unit/query_spec.rb +0 -571
- data/spec/unit/repository_spec.rb +0 -93
- data/spec/unit/resource_spec.rb +0 -649
- data/spec/unit/scope_spec.rb +0 -142
- data/spec/unit/transaction_spec.rb +0 -493
- data/spec/unit/type_map_spec.rb +0 -114
- data/spec/unit/type_spec.rb +0 -119
data/lib/dm-core/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))
|
|
144
222
|
else
|
|
145
|
-
|
|
223
|
+
all(query)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
if limit
|
|
227
|
+
collection
|
|
228
|
+
else
|
|
229
|
+
collection.to_a.first
|
|
146
230
|
end
|
|
147
231
|
end
|
|
148
232
|
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
# there are no arguments
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
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.
|
|
239
|
+
#
|
|
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
|
|
156
244
|
#
|
|
157
|
-
# @
|
|
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
|
|
162
252
|
|
|
163
|
-
|
|
253
|
+
limit = args.first if args.first.kind_of?(Integer)
|
|
254
|
+
with_query = last_arg.respond_to?(:merge) && !last_arg.blank?
|
|
164
255
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
reversed.query.add_reversed = !query.add_reversed?
|
|
256
|
+
query = with_query ? last_arg : {}
|
|
257
|
+
query = self.query.slice(0, limit || 1).update(query).reverse!
|
|
168
258
|
|
|
169
|
-
|
|
259
|
+
# tell the Query to prepend each result from the adapter
|
|
260
|
+
query.update(:add_reversed => !query.add_reversed?)
|
|
261
|
+
|
|
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
|
|
272
|
+
|
|
273
|
+
if limit
|
|
274
|
+
collection
|
|
275
|
+
else
|
|
276
|
+
collection.to_a.last
|
|
277
|
+
end
|
|
170
278
|
end
|
|
171
279
|
|
|
172
|
-
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
# the
|
|
280
|
+
# Lookup a Resource from the Collection by offset
|
|
281
|
+
#
|
|
282
|
+
# @param [Integer] offset
|
|
283
|
+
# offset of the Resource in the Collection
|
|
176
284
|
#
|
|
177
|
-
# @
|
|
178
|
-
#
|
|
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,1078 @@ 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 []
|
|
348
|
+
|
|
349
|
+
# Deletes and Returns the Resources given by an offset or a Range
|
|
350
|
+
#
|
|
351
|
+
# @param [Integer, Array(Integer), Range] *args
|
|
352
|
+
# the offset, offset and limit, or range indicating first and last position
|
|
353
|
+
#
|
|
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
|
|
225
379
|
|
|
226
|
-
|
|
380
|
+
# Splice a list of Resources at a given offset or range
|
|
227
381
|
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
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.
|
|
230
384
|
#
|
|
231
|
-
# @
|
|
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
|
|
236
439
|
end
|
|
237
440
|
|
|
238
|
-
|
|
239
|
-
#
|
|
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))) }
|
|
450
|
+
end
|
|
451
|
+
|
|
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
|
-
def pop
|
|
286
|
-
|
|
537
|
+
def pop(*)
|
|
538
|
+
if removed = super
|
|
539
|
+
resources_removed(removed)
|
|
540
|
+
end
|
|
287
541
|
end
|
|
288
542
|
|
|
289
|
-
|
|
290
|
-
#
|
|
543
|
+
# Removes and returns the first Resource in the Collection
|
|
544
|
+
#
|
|
545
|
+
# @return [Resource]
|
|
546
|
+
# the first Resource in the Collection
|
|
291
547
|
#
|
|
292
548
|
# @api public
|
|
293
|
-
def shift
|
|
294
|
-
|
|
549
|
+
def shift(*)
|
|
550
|
+
if removed = super
|
|
551
|
+
resources_removed(removed)
|
|
552
|
+
end
|
|
295
553
|
end
|
|
296
554
|
|
|
297
|
-
|
|
298
|
-
#
|
|
555
|
+
# Remove Resource from the Collection
|
|
556
|
+
#
|
|
557
|
+
# This should remove an included Resource from the Collection and
|
|
558
|
+
# orphan it from the Collection. If the Resource is not within the
|
|
559
|
+
# Collection, it should return nil.
|
|
560
|
+
#
|
|
561
|
+
# @param [Resource] resource the Resource to remove from
|
|
562
|
+
# the Collection
|
|
563
|
+
#
|
|
564
|
+
# @return [Resource]
|
|
565
|
+
# If +resource+ is within the Collection
|
|
566
|
+
# @return [nil]
|
|
567
|
+
# If +resource+ is not within the Collection
|
|
299
568
|
#
|
|
300
569
|
# @api public
|
|
301
570
|
def delete(resource)
|
|
302
|
-
|
|
571
|
+
if resource = super
|
|
572
|
+
resource_removed(resource)
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# Remove Resource from the Collection by offset
|
|
577
|
+
#
|
|
578
|
+
# This should remove the Resource from the Collection at a given
|
|
579
|
+
# offset and orphan it from the Collection. If the offset is out of
|
|
580
|
+
# range return nil.
|
|
581
|
+
#
|
|
582
|
+
# @param [Integer] offset
|
|
583
|
+
# the offset of the Resource to remove from the Collection
|
|
584
|
+
#
|
|
585
|
+
# @return [Resource]
|
|
586
|
+
# If +offset+ is within the Collection
|
|
587
|
+
# @return [nil]
|
|
588
|
+
# If +offset+ is not within the Collection
|
|
589
|
+
#
|
|
590
|
+
# @api public
|
|
591
|
+
def delete_at(offset)
|
|
592
|
+
if resource = super
|
|
593
|
+
resource_removed(resource)
|
|
594
|
+
end
|
|
303
595
|
end
|
|
304
596
|
|
|
305
|
-
|
|
306
|
-
#
|
|
597
|
+
# Deletes every Resource for which block evaluates to true.
|
|
598
|
+
#
|
|
599
|
+
# @yield [Resource] Each resource in the Collection
|
|
600
|
+
#
|
|
601
|
+
# @return [self]
|
|
307
602
|
#
|
|
308
603
|
# @api public
|
|
309
|
-
def
|
|
310
|
-
|
|
604
|
+
def delete_if
|
|
605
|
+
super { |resource| yield(resource) && resource_removed(resource) }
|
|
311
606
|
end
|
|
312
607
|
|
|
313
|
-
|
|
314
|
-
#
|
|
608
|
+
# Deletes every Resource for which block evaluates to true
|
|
609
|
+
#
|
|
610
|
+
# @yield [Resource] Each resource in the Collection
|
|
611
|
+
#
|
|
612
|
+
# @return [Collection]
|
|
613
|
+
# If resources were removed
|
|
614
|
+
# @return [nil]
|
|
615
|
+
# If no resources were removed
|
|
616
|
+
#
|
|
617
|
+
# @api public
|
|
618
|
+
def reject!
|
|
619
|
+
super { |resource| yield(resource) && resource_removed(resource) }
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Replace the Resources within the Collection
|
|
623
|
+
#
|
|
624
|
+
# @param [Enumerable] other
|
|
625
|
+
# List of other Resources to replace with
|
|
626
|
+
#
|
|
627
|
+
# @return [self]
|
|
628
|
+
#
|
|
629
|
+
# @api public
|
|
630
|
+
def replace(other)
|
|
631
|
+
other = other.map do |resource|
|
|
632
|
+
if resource.kind_of?(Hash)
|
|
633
|
+
new(resource)
|
|
634
|
+
else
|
|
635
|
+
resource
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
if loaded?
|
|
640
|
+
resources_removed(self - other)
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
super(resources_added(other))
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# Access Collection#replace directly
|
|
647
|
+
#
|
|
648
|
+
# @api private
|
|
649
|
+
alias collection_replace replace
|
|
650
|
+
private :collection_replace
|
|
651
|
+
|
|
652
|
+
# Removes all Resources from the Collection
|
|
653
|
+
#
|
|
654
|
+
# This should remove and orphan each Resource from the Collection
|
|
655
|
+
#
|
|
656
|
+
# @return [self]
|
|
315
657
|
#
|
|
316
658
|
# @api public
|
|
317
659
|
def clear
|
|
318
660
|
if loaded?
|
|
319
|
-
|
|
661
|
+
resources_removed(self)
|
|
320
662
|
end
|
|
321
663
|
super
|
|
322
|
-
self
|
|
323
664
|
end
|
|
324
665
|
|
|
325
|
-
#
|
|
666
|
+
# Finds the first Resource by conditions, or initializes a new
|
|
667
|
+
# Resource with the attributes if none found
|
|
326
668
|
#
|
|
327
|
-
# @param Hash
|
|
328
|
-
#
|
|
669
|
+
# @param [Hash] conditions
|
|
670
|
+
# The conditions to be used to search
|
|
671
|
+
# @param [Hash] attributes
|
|
672
|
+
# The attributes to be used to initialize the resource with if none found
|
|
673
|
+
# @return [Resource]
|
|
674
|
+
# The instance found by +query+, or created with +attributes+ if none found
|
|
329
675
|
#
|
|
330
676
|
# @api public
|
|
331
|
-
def
|
|
332
|
-
|
|
333
|
-
resource = model.new(default_attributes.merge(attributes))
|
|
334
|
-
self << resource
|
|
335
|
-
resource
|
|
336
|
-
end
|
|
677
|
+
def first_or_new(conditions = {}, attributes = {})
|
|
678
|
+
first(conditions) || new(conditions.merge(attributes))
|
|
337
679
|
end
|
|
338
680
|
|
|
339
|
-
|
|
340
|
-
#
|
|
681
|
+
# Finds the first Resource by conditions, or creates a new
|
|
682
|
+
# Resource with the attributes if none found
|
|
341
683
|
#
|
|
342
|
-
# @param Hash
|
|
343
|
-
#
|
|
684
|
+
# @param [Hash] conditions
|
|
685
|
+
# The conditions to be used to search
|
|
686
|
+
# @param [Hash] attributes
|
|
687
|
+
# The attributes to be used to create the resource with if none found
|
|
688
|
+
# @return [Resource]
|
|
689
|
+
# The instance found by +query+, or created with +attributes+ if none found
|
|
690
|
+
#
|
|
691
|
+
# @api public
|
|
692
|
+
def first_or_create(conditions = {}, attributes = {})
|
|
693
|
+
first(conditions) || create(conditions.merge(attributes))
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Initializes a Resource and appends it to the Collection
|
|
697
|
+
#
|
|
698
|
+
# @param [Hash] attributes
|
|
699
|
+
# Attributes with which to initialize the new resource
|
|
700
|
+
#
|
|
701
|
+
# @return [Resource]
|
|
702
|
+
# a new Resource initialized with +attributes+
|
|
703
|
+
#
|
|
704
|
+
# @api public
|
|
705
|
+
def new(attributes = {})
|
|
706
|
+
resource = repository.scope { model.new(attributes) }
|
|
707
|
+
self << resource
|
|
708
|
+
resource
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Create a Resource in the Collection
|
|
712
|
+
#
|
|
713
|
+
# @param [Hash(Symbol => Object)] attributes
|
|
714
|
+
# attributes to set
|
|
715
|
+
#
|
|
716
|
+
# @return [Resource]
|
|
717
|
+
# the newly created Resource instance
|
|
344
718
|
#
|
|
345
719
|
# @api public
|
|
346
720
|
def create(attributes = {})
|
|
347
|
-
|
|
348
|
-
resource = model.create(default_attributes.merge(attributes))
|
|
349
|
-
self << resource unless resource.new_record?
|
|
350
|
-
resource
|
|
351
|
-
end
|
|
721
|
+
_create(true, attributes)
|
|
352
722
|
end
|
|
353
723
|
|
|
354
|
-
|
|
355
|
-
|
|
724
|
+
# Create a Resource in the Collection, bypassing hooks
|
|
725
|
+
#
|
|
726
|
+
# @param [Hash(Symbol => Object)] attributes
|
|
727
|
+
# attributes to set
|
|
728
|
+
#
|
|
729
|
+
# @return [Resource]
|
|
730
|
+
# the newly created Resource instance
|
|
731
|
+
#
|
|
732
|
+
# @api public
|
|
733
|
+
def create!(attributes = {})
|
|
734
|
+
_create(false, attributes)
|
|
356
735
|
end
|
|
357
736
|
|
|
358
|
-
|
|
359
|
-
# batch updates the entries belongs to this collection, and skip
|
|
360
|
-
# validations for all resources.
|
|
737
|
+
# Update every Resource in the Collection
|
|
361
738
|
#
|
|
362
|
-
#
|
|
363
|
-
# Person.all(:age.gte => 21).update!(:allow_beer => true)
|
|
739
|
+
# Person.all(:age.gte => 21).update(:allow_beer => true)
|
|
364
740
|
#
|
|
365
|
-
# @param
|
|
366
|
-
#
|
|
367
|
-
# will have loaded resources reflect updates.
|
|
741
|
+
# @param [Hash] attributes
|
|
742
|
+
# attributes to update with
|
|
368
743
|
#
|
|
369
|
-
# @return [
|
|
370
|
-
#
|
|
371
|
-
# FalseClass indicates that some entries were affected
|
|
744
|
+
# @return [Boolean]
|
|
745
|
+
# true if the resources were successfully updated
|
|
372
746
|
#
|
|
373
747
|
# @api public
|
|
374
|
-
def update
|
|
375
|
-
|
|
376
|
-
return true if attributes.empty?
|
|
748
|
+
def update(attributes = {})
|
|
749
|
+
assert_update_clean_only(:update)
|
|
377
750
|
|
|
378
|
-
dirty_attributes =
|
|
751
|
+
dirty_attributes = model.new(attributes).dirty_attributes
|
|
752
|
+
dirty_attributes.empty? || all? { |resource| resource.update(attributes) }
|
|
753
|
+
end
|
|
379
754
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
755
|
+
# Update every Resource in the Collection bypassing validation
|
|
756
|
+
#
|
|
757
|
+
# Person.all(:age.gte => 21).update!(:allow_beer => true)
|
|
758
|
+
#
|
|
759
|
+
# @param [Hash] attributes
|
|
760
|
+
# attributes to update
|
|
761
|
+
#
|
|
762
|
+
# @return [Boolean]
|
|
763
|
+
# true if the resources were successfully updated
|
|
764
|
+
#
|
|
765
|
+
# @api public
|
|
766
|
+
def update!(attributes = {})
|
|
767
|
+
assert_update_clean_only(:update!)
|
|
383
768
|
|
|
384
|
-
|
|
385
|
-
# each { |resource| resource.attributes = attributes } if loaded?
|
|
769
|
+
dirty_attributes = model.new(attributes).dirty_attributes
|
|
386
770
|
|
|
387
|
-
|
|
771
|
+
if dirty_attributes.empty?
|
|
772
|
+
true
|
|
773
|
+
elsif dirty_attributes.any? { |property, value| !property.nullable? && value.nil? }
|
|
774
|
+
false
|
|
775
|
+
else
|
|
776
|
+
unless _update(dirty_attributes)
|
|
777
|
+
return false
|
|
778
|
+
end
|
|
388
779
|
|
|
389
|
-
|
|
390
|
-
|
|
780
|
+
if loaded?
|
|
781
|
+
each do |resource|
|
|
782
|
+
dirty_attributes.each { |property, value| property.set!(resource, value) }
|
|
783
|
+
repository.identity_map(model)[resource.key] = resource
|
|
784
|
+
end
|
|
785
|
+
end
|
|
391
786
|
|
|
392
|
-
|
|
393
|
-
reload_query = @key_properties.zip(identity_map.keys.transpose).to_hash
|
|
394
|
-
model.all(reload_query.merge(attributes)).reload(:fields => attributes.keys)
|
|
787
|
+
true
|
|
395
788
|
end
|
|
789
|
+
end
|
|
396
790
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
791
|
+
# Save every Resource in the Collection
|
|
792
|
+
#
|
|
793
|
+
# @return [Boolean]
|
|
794
|
+
# true if the resources were successfully saved
|
|
795
|
+
#
|
|
796
|
+
# @api public
|
|
797
|
+
def save
|
|
798
|
+
_save(true)
|
|
401
799
|
end
|
|
402
800
|
|
|
801
|
+
# Save every Resource in the Collection bypassing validation
|
|
802
|
+
#
|
|
803
|
+
# @return [Boolean]
|
|
804
|
+
# true if the resources were successfully saved
|
|
805
|
+
#
|
|
806
|
+
# @api public
|
|
807
|
+
def save!
|
|
808
|
+
_save(false)
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# Remove every Resource in the Collection from the repository
|
|
812
|
+
#
|
|
813
|
+
# This performs a deletion of each Resource in the Collection from
|
|
814
|
+
# the repository and clears the Collection.
|
|
815
|
+
#
|
|
816
|
+
# @return [Boolean]
|
|
817
|
+
# true if the resources were successfully destroyed
|
|
818
|
+
#
|
|
819
|
+
# @api public
|
|
403
820
|
def destroy
|
|
404
|
-
|
|
821
|
+
if destroyed = all? { |resource| resource.destroy }
|
|
822
|
+
clear
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
destroyed
|
|
405
826
|
end
|
|
406
827
|
|
|
407
|
-
|
|
408
|
-
# batch destroy the entries belongs to this collection, and skip
|
|
409
|
-
# validations for all resources.
|
|
828
|
+
# Remove all Resources from the repository, bypassing validation
|
|
410
829
|
#
|
|
411
|
-
#
|
|
412
|
-
#
|
|
830
|
+
# This performs a deletion of each Resource in the Collection from
|
|
831
|
+
# the repository and clears the Collection while skipping
|
|
832
|
+
# validation.
|
|
413
833
|
#
|
|
414
|
-
# @return [
|
|
415
|
-
#
|
|
416
|
-
# FalseClass indicates that some entries were affected
|
|
834
|
+
# @return [Boolean]
|
|
835
|
+
# true if the resources were successfully destroyed
|
|
417
836
|
#
|
|
418
837
|
# @api public
|
|
419
838
|
def destroy!
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
each do |resource|
|
|
425
|
-
resource.instance_variable_set(:@new_record, true)
|
|
426
|
-
identity_map.delete(resource.key)
|
|
427
|
-
resource.dirty_attributes.clear
|
|
839
|
+
if query.limit || query.offset > 0 || query.links.any?
|
|
840
|
+
key = model.key(repository.name)
|
|
841
|
+
conditions = Query.target_conditions(self, key, key)
|
|
428
842
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
resource.dirty_attributes[property] = property.get(resource)
|
|
432
|
-
end
|
|
843
|
+
unless model.all(:repository => repository, :conditions => conditions).destroy!
|
|
844
|
+
return false
|
|
433
845
|
end
|
|
434
846
|
else
|
|
435
|
-
|
|
847
|
+
repository.delete(self)
|
|
848
|
+
mark_loaded
|
|
436
849
|
end
|
|
437
850
|
|
|
438
|
-
|
|
851
|
+
if loaded?
|
|
852
|
+
each { |resource| resource.reset }
|
|
853
|
+
clear
|
|
854
|
+
end
|
|
439
855
|
|
|
440
856
|
true
|
|
441
857
|
end
|
|
442
858
|
|
|
443
|
-
|
|
444
|
-
#
|
|
445
|
-
#
|
|
859
|
+
# Check to see if collection can respond to the method
|
|
860
|
+
#
|
|
861
|
+
# @param [Symbol] method
|
|
862
|
+
# method to check in the object
|
|
863
|
+
# @param [Boolean] include_private
|
|
864
|
+
# if set to true, collection will check private methods
|
|
865
|
+
#
|
|
866
|
+
# @return [Boolean]
|
|
867
|
+
# true if method can be responded to
|
|
868
|
+
#
|
|
869
|
+
# @api public
|
|
870
|
+
def respond_to?(method, include_private = false)
|
|
871
|
+
super || model.respond_to?(method) || relationships.key?(method)
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Checks if all the resources have no changes to save
|
|
875
|
+
#
|
|
876
|
+
# @return [Boolean]
|
|
877
|
+
# true if the resource may not be persisted
|
|
878
|
+
#
|
|
879
|
+
# @api public
|
|
880
|
+
def clean?
|
|
881
|
+
!dirty?
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
# Checks if any resources have unsaved changes
|
|
885
|
+
#
|
|
886
|
+
# @return [Boolean]
|
|
887
|
+
# true if a resource may be persisted
|
|
888
|
+
#
|
|
889
|
+
# @api public
|
|
890
|
+
def dirty?
|
|
891
|
+
loaded_entries.any? { |resource| resource.dirty? }
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# Gets a Human-readable representation of this collection,
|
|
895
|
+
# showing all elements contained in it
|
|
896
|
+
#
|
|
897
|
+
# @return [String]
|
|
898
|
+
# Human-readable representation of this collection, showing all elements
|
|
446
899
|
#
|
|
447
900
|
# @api public
|
|
901
|
+
def inspect
|
|
902
|
+
"[#{map { |resource| resource.inspect }.join(', ')}]"
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# Returns the PropertySet representing the fields in the Collection scope
|
|
906
|
+
#
|
|
907
|
+
# @return [PropertySet]
|
|
908
|
+
# The set of properties this Collection's query will retrieve
|
|
909
|
+
#
|
|
910
|
+
# @api semipublic
|
|
448
911
|
def properties
|
|
449
912
|
PropertySet.new(query.fields)
|
|
450
913
|
end
|
|
451
914
|
|
|
452
|
-
|
|
453
|
-
# @return [DataMapper::Relationship] The model's relationships
|
|
915
|
+
# Returns the Relationships for the Collection's Model
|
|
454
916
|
#
|
|
455
|
-
# @
|
|
917
|
+
# @return [Hash]
|
|
918
|
+
# The model's relationships, mapping the name to the
|
|
919
|
+
# Associations::Relationship object
|
|
920
|
+
#
|
|
921
|
+
# @api semipublic
|
|
456
922
|
def relationships
|
|
457
923
|
model.relationships(repository.name)
|
|
458
924
|
end
|
|
459
925
|
|
|
460
|
-
|
|
461
|
-
|
|
926
|
+
private
|
|
927
|
+
|
|
928
|
+
# Initializes a new Collection identified by the query
|
|
462
929
|
#
|
|
463
|
-
# @
|
|
930
|
+
# @param [Query] query
|
|
931
|
+
# Scope the results of the Collection
|
|
932
|
+
# @param [Enumerable] resources (optional)
|
|
933
|
+
# List of resources to initialize the Collection with
|
|
464
934
|
#
|
|
465
|
-
# @
|
|
935
|
+
# @return [self]
|
|
466
936
|
#
|
|
467
|
-
# @api
|
|
468
|
-
def
|
|
469
|
-
|
|
470
|
-
query.conditions.each do |tuple|
|
|
471
|
-
operator, property, bind_value = *tuple
|
|
937
|
+
# @api private
|
|
938
|
+
def initialize(query, resources = nil)
|
|
939
|
+
raise "#{self.class}#new with a block is deprecated" if block_given?
|
|
472
940
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
!@key_properties.include?(property)
|
|
941
|
+
@query = query
|
|
942
|
+
@identity_map = IdentityMap.new
|
|
943
|
+
@removed = Set.new
|
|
477
944
|
|
|
478
|
-
|
|
945
|
+
super()
|
|
946
|
+
|
|
947
|
+
# TODO: change LazyArray to not use a load proc at all
|
|
948
|
+
remove_instance_variable(:@load_with_proc)
|
|
949
|
+
|
|
950
|
+
if resources
|
|
951
|
+
replace(resources)
|
|
479
952
|
end
|
|
480
|
-
default_attributes
|
|
481
953
|
end
|
|
482
954
|
|
|
483
|
-
|
|
484
|
-
# check to see if collection can respond to the method
|
|
955
|
+
# Copies the original Collection state
|
|
485
956
|
#
|
|
486
|
-
# @param
|
|
487
|
-
#
|
|
488
|
-
# collection will check private methods
|
|
957
|
+
# @param [Collection] original
|
|
958
|
+
# the original collection to copy from
|
|
489
959
|
#
|
|
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
|
|
960
|
+
# @return [undefined]
|
|
493
961
|
#
|
|
494
|
-
# @api
|
|
495
|
-
def
|
|
496
|
-
super
|
|
962
|
+
# @api private
|
|
963
|
+
def initialize_copy(original)
|
|
964
|
+
super
|
|
965
|
+
@query = @query.dup
|
|
966
|
+
@identity_map = @identity_map.dup
|
|
967
|
+
@removed = @removed.dup
|
|
497
968
|
end
|
|
498
969
|
|
|
499
|
-
#
|
|
970
|
+
# Test if the collection is loaded between the offset and limit
|
|
971
|
+
#
|
|
972
|
+
# @param [Integer] offset
|
|
973
|
+
# the offset of the collection to test
|
|
974
|
+
# @param [Integer] limit
|
|
975
|
+
# optional limit for how many entries to be loaded
|
|
976
|
+
#
|
|
977
|
+
# @return [Boolean]
|
|
978
|
+
# true if the collection is loaded from the offset to the limit
|
|
979
|
+
#
|
|
500
980
|
# @api private
|
|
501
|
-
def
|
|
502
|
-
|
|
981
|
+
def partially_loaded?(offset, limit = 1)
|
|
982
|
+
if offset >= 0
|
|
983
|
+
lazy_possible?(head, offset + limit)
|
|
984
|
+
else
|
|
985
|
+
lazy_possible?(tail, offset.abs)
|
|
986
|
+
end
|
|
503
987
|
end
|
|
504
988
|
|
|
505
|
-
#
|
|
989
|
+
# Lazy loads a Collection
|
|
990
|
+
#
|
|
991
|
+
# @return [self]
|
|
992
|
+
#
|
|
506
993
|
# @api private
|
|
507
|
-
def
|
|
508
|
-
|
|
994
|
+
def lazy_load
|
|
995
|
+
if loaded?
|
|
996
|
+
return self
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
mark_loaded
|
|
1000
|
+
|
|
1001
|
+
resources = repository.read(query)
|
|
1002
|
+
|
|
1003
|
+
# remove already known results
|
|
1004
|
+
resources -= head if head.any?
|
|
1005
|
+
resources -= tail if tail.any?
|
|
1006
|
+
resources -= @removed.to_a if @removed.any?
|
|
1007
|
+
|
|
1008
|
+
query.add_reversed? ? unshift(*resources.reverse) : concat(resources)
|
|
509
1009
|
|
|
510
|
-
#
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
# to initialize an object. This should be fixed in the edge
|
|
514
|
-
# branch dkubb/dm-core
|
|
1010
|
+
# TODO: DRY this up with LazyArray
|
|
1011
|
+
@array.unshift(*head)
|
|
1012
|
+
@array.concat(tail)
|
|
515
1013
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
collection.instance_variable_set(:@cache, {})
|
|
522
|
-
collection
|
|
1014
|
+
@head = @tail = nil
|
|
1015
|
+
@reapers.each { |resource| @array.delete_if(&resource) } if @reapers
|
|
1016
|
+
@array.freeze if frozen?
|
|
1017
|
+
|
|
1018
|
+
self
|
|
523
1019
|
end
|
|
524
1020
|
|
|
525
|
-
|
|
1021
|
+
# Loaded Resources in the collection
|
|
1022
|
+
#
|
|
1023
|
+
# @return [Array<Resource>]
|
|
1024
|
+
# Resources in the collection
|
|
1025
|
+
#
|
|
1026
|
+
# @api private
|
|
1027
|
+
def loaded_entries
|
|
1028
|
+
loaded? ? self : head + tail
|
|
1029
|
+
end
|
|
526
1030
|
|
|
527
|
-
|
|
1031
|
+
# Initializes a new Collection
|
|
1032
|
+
#
|
|
1033
|
+
# @return [Collection]
|
|
1034
|
+
# A new Collection object
|
|
1035
|
+
#
|
|
528
1036
|
# @api private
|
|
529
|
-
def
|
|
530
|
-
|
|
1037
|
+
def new_collection(query, resources = nil, &block)
|
|
1038
|
+
if loaded?
|
|
1039
|
+
resources ||= filter(query)
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
# TOOD: figure out a way to pass not-yet-saved Resources to this newly
|
|
1043
|
+
# created Collection. If the new resource matches the conditions, then
|
|
1044
|
+
# it should be added to the collection (keep in mind limit/offset too)
|
|
1045
|
+
|
|
1046
|
+
self.class.new(query, resources, &block)
|
|
531
1047
|
end
|
|
532
1048
|
|
|
533
|
-
|
|
1049
|
+
# Creates a resource in the collection
|
|
1050
|
+
#
|
|
1051
|
+
# @param [Boolean] safe
|
|
1052
|
+
# Whether to use the safe or unsafe create
|
|
1053
|
+
# @param [Hash] attributes
|
|
1054
|
+
# Attributes with which to create the new resource
|
|
1055
|
+
#
|
|
1056
|
+
# @return [Resource]
|
|
1057
|
+
# a saved Resource
|
|
1058
|
+
#
|
|
1059
|
+
# @api private
|
|
1060
|
+
def _create(safe, attributes)
|
|
1061
|
+
resource = repository.scope { model.send(safe ? :create : :create!, default_attributes.merge(attributes)) }
|
|
1062
|
+
self << resource if resource.saved?
|
|
1063
|
+
resource
|
|
1064
|
+
end
|
|
534
1065
|
|
|
535
|
-
|
|
536
|
-
#
|
|
537
|
-
|
|
538
|
-
|
|
1066
|
+
# Updates a collection
|
|
1067
|
+
#
|
|
1068
|
+
# @return [Boolean]
|
|
1069
|
+
# Returns true if collection was updated
|
|
1070
|
+
#
|
|
1071
|
+
# @api private
|
|
1072
|
+
def _update(dirty_attributes)
|
|
1073
|
+
if query.limit || query.offset > 0 || query.links.any?
|
|
1074
|
+
attributes = dirty_attributes.map { |property, value| [ property.name, value ] }.to_hash
|
|
539
1075
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1076
|
+
key = model.key(repository.name)
|
|
1077
|
+
conditions = Query.target_conditions(self, key, key)
|
|
1078
|
+
|
|
1079
|
+
unless model.all(:repository => repository, :conditions => conditions).update!(attributes)
|
|
1080
|
+
return false
|
|
1081
|
+
end
|
|
1082
|
+
else
|
|
1083
|
+
repository.update(dirty_attributes, self)
|
|
543
1084
|
end
|
|
544
1085
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
@cache = {}
|
|
1086
|
+
true
|
|
1087
|
+
end
|
|
548
1088
|
|
|
549
|
-
|
|
1089
|
+
# Saves a collection
|
|
1090
|
+
#
|
|
1091
|
+
# @param [Symbol] method
|
|
1092
|
+
# The name of the Resource method to save the collection with
|
|
1093
|
+
#
|
|
1094
|
+
# @return [Boolean]
|
|
1095
|
+
# Returns true if collection was updated
|
|
1096
|
+
#
|
|
1097
|
+
# @api private
|
|
1098
|
+
def _save(safe)
|
|
1099
|
+
loaded_entries.each { |resource| set_default_attributes(resource) }
|
|
1100
|
+
@removed.clear
|
|
1101
|
+
loaded_entries.all? { |resource| resource.send(safe ? :save : :save!) }
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
# Returns default values to initialize new Resources in the Collection
|
|
1105
|
+
#
|
|
1106
|
+
# @return [Hash] The default attributes for new instances in this Collection
|
|
1107
|
+
#
|
|
1108
|
+
# @api private
|
|
1109
|
+
def default_attributes
|
|
1110
|
+
return @default_attributes if @default_attributes
|
|
1111
|
+
|
|
1112
|
+
default_attributes = {}
|
|
1113
|
+
|
|
1114
|
+
conditions = query.conditions
|
|
550
1115
|
|
|
551
|
-
|
|
1116
|
+
if conditions.kind_of?(Query::Conditions::AndOperation)
|
|
1117
|
+
repository_name = repository.name
|
|
1118
|
+
relationships = self.relationships.values
|
|
1119
|
+
properties = model.properties(repository_name)
|
|
1120
|
+
key = model.key(repository_name)
|
|
1121
|
+
|
|
1122
|
+
# if all the key properties are included in the conditions,
|
|
1123
|
+
# then do not allow them to be default attributes
|
|
1124
|
+
if query.condition_properties.to_set.superset?(key.to_set)
|
|
1125
|
+
properties -= key
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
conditions.each do |condition|
|
|
1129
|
+
if condition.kind_of?(Query::Conditions::EqualToComparison) &&
|
|
1130
|
+
(properties.include?(condition.subject) || (condition.relationship? && condition.subject.source_model == model))
|
|
1131
|
+
default_attributes[condition.subject] = condition.value
|
|
1132
|
+
end
|
|
1133
|
+
end
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
@default_attributes = default_attributes.freeze
|
|
552
1137
|
end
|
|
553
1138
|
|
|
554
|
-
|
|
1139
|
+
# Set the default attributes for a non-frozen resource
|
|
1140
|
+
#
|
|
1141
|
+
# @param [Resource] resource
|
|
1142
|
+
# the resource to set the default attributes for
|
|
1143
|
+
#
|
|
1144
|
+
# @return [undefined]
|
|
1145
|
+
#
|
|
555
1146
|
# @api private
|
|
556
|
-
def
|
|
557
|
-
|
|
558
|
-
|
|
1147
|
+
def set_default_attributes(resource)
|
|
1148
|
+
unless resource.frozen?
|
|
1149
|
+
resource.attributes = default_attributes
|
|
1150
|
+
end
|
|
559
1151
|
end
|
|
560
1152
|
|
|
561
|
-
|
|
1153
|
+
# Relates a Resource to the Collection
|
|
1154
|
+
#
|
|
1155
|
+
# This is used by SEL related code to reload a Resource and the
|
|
1156
|
+
# Collection it belongs to.
|
|
1157
|
+
#
|
|
1158
|
+
# @param [Resource] resource
|
|
1159
|
+
# The Resource to relate
|
|
1160
|
+
#
|
|
1161
|
+
# @return [Resource]
|
|
1162
|
+
# If Resource was successfully related
|
|
1163
|
+
# @return [nil]
|
|
1164
|
+
# If a nil resource was provided
|
|
1165
|
+
#
|
|
562
1166
|
# @api private
|
|
563
1167
|
def relate_resource(resource)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1168
|
+
unless resource.frozen?
|
|
1169
|
+
resource.collection = self
|
|
1170
|
+
end
|
|
1171
|
+
|
|
567
1172
|
resource
|
|
568
1173
|
end
|
|
569
1174
|
|
|
570
|
-
|
|
1175
|
+
# Orphans a Resource from the Collection
|
|
1176
|
+
#
|
|
1177
|
+
# Removes the association between the Resource and Collection so that
|
|
1178
|
+
# SEL related code will not load the Collection.
|
|
1179
|
+
#
|
|
1180
|
+
# @param [Resource] resource
|
|
1181
|
+
# The Resource to orphan
|
|
1182
|
+
#
|
|
1183
|
+
# @return [Resource]
|
|
1184
|
+
# The Resource that was orphaned
|
|
1185
|
+
# @return [nil]
|
|
1186
|
+
# If a nil resource was provided
|
|
1187
|
+
#
|
|
571
1188
|
# @api private
|
|
572
1189
|
def orphan_resource(resource)
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1190
|
+
if resource.collection.equal?(self) && !resource.frozen?
|
|
1191
|
+
resource.collection = nil
|
|
1192
|
+
end
|
|
1193
|
+
|
|
576
1194
|
resource
|
|
577
1195
|
end
|
|
578
1196
|
|
|
579
|
-
|
|
1197
|
+
# Track the added resource
|
|
1198
|
+
#
|
|
1199
|
+
# @param [Resource] resource
|
|
1200
|
+
# the resource that was added
|
|
1201
|
+
#
|
|
1202
|
+
# @return [Resource]
|
|
1203
|
+
# the resource that was added
|
|
1204
|
+
#
|
|
580
1205
|
# @api private
|
|
581
|
-
def
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1206
|
+
def resource_added(resource)
|
|
1207
|
+
if resource.saved?
|
|
1208
|
+
@identity_map[resource.key] = resource
|
|
1209
|
+
@removed.delete(resource)
|
|
1210
|
+
else
|
|
1211
|
+
set_default_attributes(resource)
|
|
1212
|
+
end
|
|
585
1213
|
|
|
586
|
-
|
|
1214
|
+
relate_resource(resource)
|
|
1215
|
+
end
|
|
587
1216
|
|
|
588
|
-
|
|
589
|
-
|
|
1217
|
+
# Track the added resources
|
|
1218
|
+
#
|
|
1219
|
+
# @param [Array<Resource>] resources
|
|
1220
|
+
# the resources that were added
|
|
1221
|
+
#
|
|
1222
|
+
# @return [Array<Resource>]
|
|
1223
|
+
# the resources that were added
|
|
1224
|
+
#
|
|
1225
|
+
# @api private
|
|
1226
|
+
def resources_added(resources)
|
|
1227
|
+
if resources.kind_of?(Enumerable)
|
|
1228
|
+
resources.each { |resource| resource_added(resource) }
|
|
590
1229
|
else
|
|
591
|
-
|
|
1230
|
+
resource_added(resources)
|
|
592
1231
|
end
|
|
1232
|
+
end
|
|
593
1233
|
|
|
594
|
-
|
|
595
|
-
|
|
1234
|
+
# Track the removed resource
|
|
1235
|
+
#
|
|
1236
|
+
# @param [Resource] resource
|
|
1237
|
+
# the resource that was removed
|
|
1238
|
+
#
|
|
1239
|
+
# @return [Resource]
|
|
1240
|
+
# the resource that was removed
|
|
1241
|
+
#
|
|
1242
|
+
# @api private
|
|
1243
|
+
def resource_removed(resource)
|
|
1244
|
+
if resource.saved?
|
|
1245
|
+
@identity_map.delete(resource.key)
|
|
1246
|
+
@removed << resource
|
|
596
1247
|
end
|
|
597
1248
|
|
|
598
|
-
|
|
1249
|
+
orphan_resource(resource)
|
|
599
1250
|
end
|
|
600
1251
|
|
|
601
|
-
|
|
1252
|
+
# Track the removed resources
|
|
1253
|
+
#
|
|
1254
|
+
# @param [Array<Resource>] resources
|
|
1255
|
+
# the resources that were removed
|
|
1256
|
+
#
|
|
1257
|
+
# @return [Array<Resource>]
|
|
1258
|
+
# the resources that were removed
|
|
1259
|
+
#
|
|
602
1260
|
# @api private
|
|
603
|
-
def
|
|
604
|
-
|
|
605
|
-
|
|
1261
|
+
def resources_removed(resources)
|
|
1262
|
+
if resources.kind_of?(Enumerable)
|
|
1263
|
+
resources.each { |resource| resource_removed(resource) }
|
|
1264
|
+
else
|
|
1265
|
+
resource_removed(resources)
|
|
1266
|
+
end
|
|
606
1267
|
end
|
|
607
1268
|
|
|
608
|
-
|
|
1269
|
+
# Filter resources in the collection based on a Query
|
|
1270
|
+
#
|
|
1271
|
+
# @param [Query] query
|
|
1272
|
+
# the query to match each resource in the collection
|
|
1273
|
+
#
|
|
1274
|
+
# @return [Array]
|
|
1275
|
+
# the resources that match the Query
|
|
1276
|
+
# @return [nil]
|
|
1277
|
+
# nil if no resources match the Query
|
|
1278
|
+
#
|
|
609
1279
|
# @api private
|
|
610
|
-
def
|
|
611
|
-
|
|
1280
|
+
def filter(query)
|
|
1281
|
+
fields = self.query.fields.to_set
|
|
1282
|
+
|
|
1283
|
+
if query.links.empty? &&
|
|
1284
|
+
(query.unique? || (!query.unique? && !self.query.unique?)) &&
|
|
1285
|
+
!query.reload? &&
|
|
1286
|
+
!query.raw? &&
|
|
1287
|
+
query.fields.to_set.subset?(fields) &&
|
|
1288
|
+
query.condition_properties.subset?(fields)
|
|
1289
|
+
then
|
|
1290
|
+
query.filter_records(to_a.dup)
|
|
1291
|
+
end
|
|
612
1292
|
end
|
|
613
1293
|
|
|
614
|
-
|
|
1294
|
+
# Return the absolute or relative scoped query
|
|
1295
|
+
#
|
|
1296
|
+
# @param [Query, Hash] query
|
|
1297
|
+
# the query to scope the collection with
|
|
1298
|
+
#
|
|
1299
|
+
# @return [Query]
|
|
1300
|
+
# the absolute or relative scoped query
|
|
1301
|
+
#
|
|
615
1302
|
# @api private
|
|
616
|
-
def
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
return if query.limit.nil? && self.query.limit.nil?
|
|
1303
|
+
def scoped_query(query)
|
|
1304
|
+
if query.kind_of?(Query)
|
|
1305
|
+
query.dup
|
|
1306
|
+
else
|
|
1307
|
+
self.query.relative(query)
|
|
622
1308
|
end
|
|
1309
|
+
end
|
|
623
1310
|
|
|
624
|
-
|
|
625
|
-
|
|
1311
|
+
# @api private
|
|
1312
|
+
def sliced_query(offset, limit)
|
|
1313
|
+
query = self.query
|
|
626
1314
|
|
|
627
|
-
if
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
end
|
|
1315
|
+
if offset >= 0
|
|
1316
|
+
query.slice(offset, limit)
|
|
1317
|
+
else
|
|
1318
|
+
query = query.slice((limit + offset).abs, limit).reverse!
|
|
632
1319
|
|
|
633
|
-
|
|
634
|
-
|
|
1320
|
+
# tell the Query to prepend each result from the adapter
|
|
1321
|
+
query.update(:add_reversed => !query.add_reversed?)
|
|
635
1322
|
end
|
|
636
|
-
|
|
637
|
-
query.update(:offset => first_pos)
|
|
638
|
-
query.update(:limit => last_pos - first_pos) if last_pos
|
|
639
1323
|
end
|
|
640
1324
|
|
|
641
|
-
|
|
642
|
-
#
|
|
1325
|
+
# Delegates to Model, Relationships or the superclass (LazyArray)
|
|
1326
|
+
#
|
|
1327
|
+
# When this receives a method that belongs to the Model the
|
|
1328
|
+
# Collection is scoped to, it will execute the method within the
|
|
1329
|
+
# same scope as the Collection and return the results.
|
|
1330
|
+
#
|
|
1331
|
+
# When this receives a method that is a relationship the Model has
|
|
1332
|
+
# defined, it will execute the association method within the same
|
|
1333
|
+
# scope as the Collection and return the results.
|
|
1334
|
+
#
|
|
1335
|
+
# Otherwise this method will delegate to a method in the superclass
|
|
1336
|
+
# (LazyArray) and return the results.
|
|
1337
|
+
#
|
|
1338
|
+
# @return [Object]
|
|
1339
|
+
# the return values of the delegated methods
|
|
1340
|
+
#
|
|
1341
|
+
# @api public
|
|
643
1342
|
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
|
|
1343
|
+
if model.model_method_defined?(method)
|
|
1344
|
+
delegate_to_model(method, *args, &block)
|
|
1345
|
+
elsif relationship = relationships[method] || relationships[method.to_s.singular.to_sym]
|
|
1346
|
+
delegate_to_relationship(relationship, *args)
|
|
1347
|
+
else
|
|
1348
|
+
super
|
|
1349
|
+
end
|
|
1350
|
+
end
|
|
653
1351
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1352
|
+
# Delegate the method to the Model
|
|
1353
|
+
#
|
|
1354
|
+
# @param [Symbol] method
|
|
1355
|
+
# the name of the method in the model to execute
|
|
1356
|
+
# @param [Array] *args
|
|
1357
|
+
# the arguments for the method
|
|
1358
|
+
#
|
|
1359
|
+
# @return [Object]
|
|
1360
|
+
# the return value of the model method
|
|
1361
|
+
#
|
|
1362
|
+
# @api private
|
|
1363
|
+
def delegate_to_model(method, *args, &block)
|
|
1364
|
+
model.__send__(:with_scope, query) do
|
|
1365
|
+
model.send(method, *args, &block)
|
|
1366
|
+
end
|
|
1367
|
+
end
|
|
658
1368
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1369
|
+
# Delegate the method to the Relationship
|
|
1370
|
+
#
|
|
1371
|
+
# @return [Collection]
|
|
1372
|
+
# the associated Resources
|
|
1373
|
+
#
|
|
1374
|
+
# @api private
|
|
1375
|
+
def delegate_to_relationship(relationship, query = nil)
|
|
1376
|
+
relationship.eager_load(self, query)
|
|
1377
|
+
end
|
|
663
1378
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1379
|
+
# Raises an exception if #update is performed on a dirty resource
|
|
1380
|
+
#
|
|
1381
|
+
# @raise [UpdateConflictError]
|
|
1382
|
+
# raise if the resource is dirty
|
|
1383
|
+
#
|
|
1384
|
+
# @return [undefined]
|
|
1385
|
+
#
|
|
1386
|
+
# @api private
|
|
1387
|
+
def assert_update_clean_only(method)
|
|
1388
|
+
if dirty?
|
|
1389
|
+
raise UpdateConflictError, "##{method} cannot be called on a dirty collection"
|
|
667
1390
|
end
|
|
668
1391
|
end
|
|
669
1392
|
end # class Collection
|