rpbertp13-dm-core 0.9.11.1
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 +26 -0
- data/.gitignore +18 -0
- data/CONTRIBUTING +51 -0
- data/FAQ +92 -0
- data/History.txt +52 -0
- data/MIT-LICENSE +22 -0
- data/Manifest.txt +130 -0
- data/QUICKLINKS +11 -0
- data/README.txt +143 -0
- data/Rakefile +32 -0
- data/SPECS +62 -0
- data/TODO +1 -0
- data/dm-core.gemspec +40 -0
- data/lib/dm-core.rb +217 -0
- data/lib/dm-core/adapters.rb +16 -0
- data/lib/dm-core/adapters/abstract_adapter.rb +209 -0
- data/lib/dm-core/adapters/data_objects_adapter.rb +716 -0
- data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
- data/lib/dm-core/adapters/mysql_adapter.rb +138 -0
- data/lib/dm-core/adapters/postgres_adapter.rb +189 -0
- data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
- data/lib/dm-core/associations.rb +207 -0
- data/lib/dm-core/associations/many_to_many.rb +147 -0
- data/lib/dm-core/associations/many_to_one.rb +107 -0
- data/lib/dm-core/associations/one_to_many.rb +315 -0
- data/lib/dm-core/associations/one_to_one.rb +61 -0
- data/lib/dm-core/associations/relationship.rb +221 -0
- data/lib/dm-core/associations/relationship_chain.rb +81 -0
- data/lib/dm-core/auto_migrations.rb +105 -0
- data/lib/dm-core/collection.rb +670 -0
- data/lib/dm-core/dependency_queue.rb +32 -0
- data/lib/dm-core/hook.rb +11 -0
- data/lib/dm-core/identity_map.rb +42 -0
- data/lib/dm-core/is.rb +16 -0
- data/lib/dm-core/logger.rb +232 -0
- data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
- data/lib/dm-core/migrator.rb +29 -0
- data/lib/dm-core/model.rb +526 -0
- data/lib/dm-core/naming_conventions.rb +84 -0
- data/lib/dm-core/property.rb +676 -0
- data/lib/dm-core/property_set.rb +169 -0
- data/lib/dm-core/query.rb +676 -0
- data/lib/dm-core/repository.rb +167 -0
- data/lib/dm-core/resource.rb +671 -0
- data/lib/dm-core/scope.rb +58 -0
- data/lib/dm-core/support.rb +7 -0
- data/lib/dm-core/support/array.rb +13 -0
- data/lib/dm-core/support/assertions.rb +8 -0
- data/lib/dm-core/support/errors.rb +23 -0
- data/lib/dm-core/support/kernel.rb +11 -0
- data/lib/dm-core/support/symbol.rb +41 -0
- data/lib/dm-core/transaction.rb +252 -0
- data/lib/dm-core/type.rb +160 -0
- data/lib/dm-core/type_map.rb +80 -0
- data/lib/dm-core/types.rb +19 -0
- data/lib/dm-core/types/boolean.rb +7 -0
- data/lib/dm-core/types/discriminator.rb +34 -0
- data/lib/dm-core/types/object.rb +24 -0
- data/lib/dm-core/types/paranoid_boolean.rb +34 -0
- data/lib/dm-core/types/paranoid_datetime.rb +33 -0
- data/lib/dm-core/types/serial.rb +9 -0
- data/lib/dm-core/types/text.rb +10 -0
- data/lib/dm-core/version.rb +3 -0
- data/script/all +4 -0
- data/script/performance.rb +282 -0
- data/script/profile.rb +87 -0
- data/spec/integration/association_spec.rb +1382 -0
- data/spec/integration/association_through_spec.rb +203 -0
- data/spec/integration/associations/many_to_many_spec.rb +449 -0
- data/spec/integration/associations/many_to_one_spec.rb +163 -0
- data/spec/integration/associations/one_to_many_spec.rb +188 -0
- data/spec/integration/auto_migrations_spec.rb +413 -0
- data/spec/integration/collection_spec.rb +1073 -0
- data/spec/integration/data_objects_adapter_spec.rb +32 -0
- data/spec/integration/dependency_queue_spec.rb +46 -0
- data/spec/integration/model_spec.rb +197 -0
- data/spec/integration/mysql_adapter_spec.rb +85 -0
- data/spec/integration/postgres_adapter_spec.rb +731 -0
- data/spec/integration/property_spec.rb +253 -0
- data/spec/integration/query_spec.rb +514 -0
- data/spec/integration/repository_spec.rb +61 -0
- data/spec/integration/resource_spec.rb +513 -0
- data/spec/integration/sqlite3_adapter_spec.rb +352 -0
- data/spec/integration/sti_spec.rb +273 -0
- data/spec/integration/strategic_eager_loading_spec.rb +156 -0
- data/spec/integration/transaction_spec.rb +60 -0
- data/spec/integration/type_spec.rb +275 -0
- data/spec/lib/logging_helper.rb +18 -0
- data/spec/lib/mock_adapter.rb +27 -0
- data/spec/lib/model_loader.rb +100 -0
- data/spec/lib/publicize_methods.rb +28 -0
- data/spec/models/content.rb +16 -0
- data/spec/models/vehicles.rb +34 -0
- data/spec/models/zoo.rb +48 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +91 -0
- data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
- data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
- data/spec/unit/adapters/data_objects_adapter_spec.rb +632 -0
- data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
- data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
- data/spec/unit/associations/many_to_many_spec.rb +32 -0
- data/spec/unit/associations/many_to_one_spec.rb +159 -0
- data/spec/unit/associations/one_to_many_spec.rb +393 -0
- data/spec/unit/associations/one_to_one_spec.rb +7 -0
- data/spec/unit/associations/relationship_spec.rb +71 -0
- data/spec/unit/associations_spec.rb +242 -0
- data/spec/unit/auto_migrations_spec.rb +111 -0
- data/spec/unit/collection_spec.rb +182 -0
- data/spec/unit/data_mapper_spec.rb +35 -0
- data/spec/unit/identity_map_spec.rb +126 -0
- data/spec/unit/is_spec.rb +80 -0
- data/spec/unit/migrator_spec.rb +33 -0
- data/spec/unit/model_spec.rb +321 -0
- data/spec/unit/naming_conventions_spec.rb +36 -0
- data/spec/unit/property_set_spec.rb +90 -0
- data/spec/unit/property_spec.rb +753 -0
- data/spec/unit/query_spec.rb +571 -0
- data/spec/unit/repository_spec.rb +93 -0
- data/spec/unit/resource_spec.rb +649 -0
- data/spec/unit/scope_spec.rb +142 -0
- data/spec/unit/transaction_spec.rb +469 -0
- data/spec/unit/type_map_spec.rb +114 -0
- data/spec/unit/type_spec.rb +119 -0
- data/tasks/ci.rb +36 -0
- data/tasks/dm.rb +63 -0
- data/tasks/doc.rb +20 -0
- data/tasks/gemspec.rb +23 -0
- data/tasks/hoe.rb +46 -0
- data/tasks/install.rb +20 -0
- metadata +215 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Associations
|
3
|
+
class RelationshipChain < Relationship
|
4
|
+
OPTIONS = [
|
5
|
+
:repository_name, :near_relationship_name, :remote_relationship_name,
|
6
|
+
:child_model, :parent_model, :parent_key, :child_key,
|
7
|
+
:min, :max
|
8
|
+
]
|
9
|
+
|
10
|
+
undef_method :get_parent
|
11
|
+
undef_method :attach_parent
|
12
|
+
|
13
|
+
# @api private
|
14
|
+
def child_model
|
15
|
+
near_relationship.child_model
|
16
|
+
end
|
17
|
+
|
18
|
+
# @api private
|
19
|
+
def get_children(parent, options = {}, finder = :all, *args)
|
20
|
+
query = @query.merge(options).merge(child_key.to_query(parent_key.get(parent)))
|
21
|
+
|
22
|
+
query[:links] = links
|
23
|
+
query[:unique] = true
|
24
|
+
|
25
|
+
with_repository(parent) do
|
26
|
+
results = grandchild_model.send(finder, *(args << query))
|
27
|
+
# FIXME: remove the need for the uniq.freeze
|
28
|
+
finder == :all ? (@mutable ? results : results.freeze) : results
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# @api private
|
35
|
+
def initialize(options)
|
36
|
+
if (missing_options = OPTIONS - [ :min, :max ] - options.keys ).any?
|
37
|
+
raise ArgumentError, "The options #{missing_options * ', '} are required", caller
|
38
|
+
end
|
39
|
+
|
40
|
+
@repository_name = options.fetch(:repository_name)
|
41
|
+
@near_relationship_name = options.fetch(:near_relationship_name)
|
42
|
+
@remote_relationship_name = options.fetch(:remote_relationship_name)
|
43
|
+
@child_model = options.fetch(:child_model)
|
44
|
+
@parent_model = options.fetch(:parent_model)
|
45
|
+
@parent_properties = options.fetch(:parent_key)
|
46
|
+
@child_properties = options.fetch(:child_key)
|
47
|
+
@mutable = options.delete(:mutable) || false
|
48
|
+
|
49
|
+
@name = near_relationship.name
|
50
|
+
@query = options.reject{ |key,val| OPTIONS.include?(key) }
|
51
|
+
@extra_links = []
|
52
|
+
@options = options
|
53
|
+
end
|
54
|
+
|
55
|
+
# @api private
|
56
|
+
def near_relationship
|
57
|
+
parent_model.relationships[@near_relationship_name]
|
58
|
+
end
|
59
|
+
|
60
|
+
# @api private
|
61
|
+
def links
|
62
|
+
if remote_relationship.kind_of?(RelationshipChain)
|
63
|
+
remote_relationship.send(:links) + [ remote_relationship.send(:near_relationship) ]
|
64
|
+
else
|
65
|
+
[ remote_relationship ]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# @api private
|
70
|
+
def remote_relationship
|
71
|
+
near_relationship.child_model.relationships[@remote_relationship_name] ||
|
72
|
+
near_relationship.child_model.relationships[@remote_relationship_name.to_s.singularize.to_sym]
|
73
|
+
end
|
74
|
+
|
75
|
+
# @api private
|
76
|
+
def grandchild_model
|
77
|
+
Class === @child_model ? @child_model : (Class === @parent_model ? @parent_model.find_const(@child_model) : Object.find_const(@child_model))
|
78
|
+
end
|
79
|
+
end # class Relationship
|
80
|
+
end # module Associations
|
81
|
+
end # module DataMapper
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# TODO: move to dm-more/dm-migrations
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
class AutoMigrator
|
5
|
+
##
|
6
|
+
# Destructively automigrates the data-store to match the model.
|
7
|
+
# First migrates all models down and then up.
|
8
|
+
# REPEAT: THIS IS DESTRUCTIVE
|
9
|
+
#
|
10
|
+
# @param Symbol repository_name the repository to be migrated
|
11
|
+
def self.auto_migrate(repository_name = nil, *descendants)
|
12
|
+
auto_migrate_down(repository_name, *descendants)
|
13
|
+
auto_migrate_up(repository_name, *descendants)
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Destructively automigrates the data-store down
|
18
|
+
# REPEAT: THIS IS DESTRUCTIVE
|
19
|
+
#
|
20
|
+
# @param Symbol repository_name the repository to be migrated
|
21
|
+
# @calls DataMapper::Resource#auto_migrate_down!
|
22
|
+
# @api private
|
23
|
+
def self.auto_migrate_down(repository_name = nil, *descendants)
|
24
|
+
descendants = DataMapper::Resource.descendants.to_a if descendants.empty?
|
25
|
+
descendants.reverse.each do |model|
|
26
|
+
model.auto_migrate_down!(repository_name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Automigrates the data-store up
|
32
|
+
#
|
33
|
+
# @param Symbol repository_name the repository to be migrated
|
34
|
+
# @calls DataMapper::Resource#auto_migrate_up!
|
35
|
+
# @api private
|
36
|
+
def self.auto_migrate_up(repository_name = nil, *descendants)
|
37
|
+
descendants = DataMapper::Resource.descendants.to_a if descendants.empty?
|
38
|
+
descendants.each do |model|
|
39
|
+
model.auto_migrate_up!(repository_name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Safely migrates the data-store to match the model
|
45
|
+
# preserving data already in the data-store
|
46
|
+
#
|
47
|
+
# @param Symbol repository_name the repository to be migrated
|
48
|
+
# @calls DataMapper::Resource#auto_upgrade!
|
49
|
+
def self.auto_upgrade(repository_name = nil)
|
50
|
+
DataMapper::Resource.descendants.each do |model|
|
51
|
+
model.auto_upgrade!(repository_name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end # class AutoMigrator
|
55
|
+
|
56
|
+
module AutoMigrations
|
57
|
+
##
|
58
|
+
# Destructively automigrates the data-store to match the model
|
59
|
+
# REPEAT: THIS IS DESTRUCTIVE
|
60
|
+
#
|
61
|
+
# @param Symbol repository_name the repository to be migrated
|
62
|
+
def auto_migrate!(repository_name = self.repository_name)
|
63
|
+
auto_migrate_down!(repository_name)
|
64
|
+
auto_migrate_up!(repository_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Destructively migrates the data-store down, which basically
|
69
|
+
# deletes all the models.
|
70
|
+
# REPEAT: THIS IS DESTRUCTIVE
|
71
|
+
#
|
72
|
+
# @param Symbol repository_name the repository to be migrated
|
73
|
+
# @api private
|
74
|
+
def auto_migrate_down!(repository_name = self.repository_name)
|
75
|
+
# repository_name ||= default_repository_name
|
76
|
+
repository(repository_name) do |r|
|
77
|
+
r.adapter.destroy_model_storage(r, self.base_model)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Auto migrates the data-store to match the model
|
83
|
+
#
|
84
|
+
# @param Symbol repository_name the repository to be migrated
|
85
|
+
# @api private
|
86
|
+
def auto_migrate_up!(repository_name = self.repository_name)
|
87
|
+
repository(repository_name) do |r|
|
88
|
+
r.adapter.create_model_storage(r, self.base_model)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Safely migrates the data-store to match the model
|
94
|
+
# preserving data already in the data-store
|
95
|
+
#
|
96
|
+
# @param Symbol repository_name the repository to be migrated
|
97
|
+
def auto_upgrade!(repository_name = self.repository_name)
|
98
|
+
repository(repository_name) do |r|
|
99
|
+
r.adapter.upgrade_model_storage(r, self)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
Model.send(:include, self)
|
104
|
+
end # module AutoMigrations
|
105
|
+
end # module DataMapper
|
@@ -0,0 +1,670 @@
|
|
1
|
+
module DataMapper
|
2
|
+
class Collection < LazyArray
|
3
|
+
include Assertions
|
4
|
+
|
5
|
+
attr_reader :query
|
6
|
+
|
7
|
+
##
|
8
|
+
# @return [Repository] the repository the collection is
|
9
|
+
# associated with
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
def repository
|
13
|
+
query.repository
|
14
|
+
end
|
15
|
+
|
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.
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
def load(values)
|
23
|
+
add(model.load(values, query))
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# reloads the entries associated with this collection
|
28
|
+
#
|
29
|
+
# @param [DataMapper::Query] query (optional) additional query
|
30
|
+
# to scope by. Use this if you want to query a collections result
|
31
|
+
# set
|
32
|
+
#
|
33
|
+
# @see DataMapper::Collection#all
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def reload(query = {})
|
37
|
+
@query = scoped_query(query)
|
38
|
+
inheritance_property = model.base_model.inheritance_property
|
39
|
+
fields = (@key_properties | [inheritance_property]).compact
|
40
|
+
@query.update(:fields => @query.fields | fields)
|
41
|
+
replace(all(:reload => true))
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# retrieves an entry out of the collection's entry by key
|
46
|
+
#
|
47
|
+
# @param [DataMapper::Types::*, ...] key keys which uniquely
|
48
|
+
# identify a resource in the collection
|
49
|
+
#
|
50
|
+
# @return [DataMapper::Resource, NilClass] the resource which
|
51
|
+
# has the supplied keys
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
def get(*key)
|
55
|
+
key = model.typecast_key(key)
|
56
|
+
if loaded?
|
57
|
+
# find indexed resource (create index first if it does not exist)
|
58
|
+
each {|r| @cache[r.key] = r } if @cache.empty?
|
59
|
+
@cache[key]
|
60
|
+
elsif query.limit || query.offset > 0
|
61
|
+
# current query is exclusive, find resource within the set
|
62
|
+
|
63
|
+
# TODO: use a subquery to retrieve the collection and then match
|
64
|
+
# it up against the key. This will require some changes to
|
65
|
+
# how subqueries are generated, since the key may be a
|
66
|
+
# composite key. In the case of DO adapters, it means subselects
|
67
|
+
# like the form "(a, b) IN(SELECT a,b FROM ...)", which will
|
68
|
+
# require making it so the Query condition key can be a
|
69
|
+
# Property or an Array of Property objects
|
70
|
+
|
71
|
+
# use the brute force approach until subquery lookups work
|
72
|
+
lazy_load
|
73
|
+
get(*key)
|
74
|
+
else
|
75
|
+
# current query is all inclusive, lookup using normal approach
|
76
|
+
first(model.to_query(repository, key))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# retrieves an entry out of the collection's entry by key,
|
82
|
+
# raising an exception if the object cannot be found
|
83
|
+
#
|
84
|
+
# @param [DataMapper::Types::*, ...] key keys which uniquely
|
85
|
+
# identify a resource in the collection
|
86
|
+
#
|
87
|
+
# @calls DataMapper::Collection#get
|
88
|
+
#
|
89
|
+
# @raise [ObjectNotFoundError] "Could not find #{model.name} with key #{key.inspect} in collection"
|
90
|
+
#
|
91
|
+
# @api public
|
92
|
+
def get!(*key)
|
93
|
+
get(*key) || raise(ObjectNotFoundError, "Could not find #{model.name} with key #{key.inspect} in collection")
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Further refines a collection's conditions. #all provides an
|
98
|
+
# interface which simulates a database view.
|
99
|
+
#
|
100
|
+
# @param [Hash[Symbol, Object], DataMapper::Query] query parameters for
|
101
|
+
# an query within the results of the original query.
|
102
|
+
#
|
103
|
+
# @return [DataMapper::Collection] a collection whose query is the result
|
104
|
+
# of a merge
|
105
|
+
#
|
106
|
+
# @api public
|
107
|
+
def all(query = {})
|
108
|
+
# TODO: this shouldn't be a kicker if scoped_query() is called
|
109
|
+
return self if query.kind_of?(Hash) ? query.empty? : query == self.query
|
110
|
+
query = scoped_query(query)
|
111
|
+
query.repository.read_many(query)
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Simulates Array#first by returning the first entry (when
|
116
|
+
# there are no arguments), or transforms the collection's query
|
117
|
+
# by applying :limit => n when you supply an Integer. If you
|
118
|
+
# provide a conditions hash, or a Query object, the internal
|
119
|
+
# query is scoped and a new collection is returned
|
120
|
+
#
|
121
|
+
# @param [Integer, Hash[Symbol, Object], Query] args
|
122
|
+
#
|
123
|
+
# @return [DataMapper::Resource, DataMapper::Collection] The
|
124
|
+
# first resource in the entries of this collection, or
|
125
|
+
# a new collection whose query has been merged
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
def first(*args)
|
129
|
+
# TODO: this shouldn't be a kicker if scoped_query() is called
|
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
|
138
|
+
|
139
|
+
query = args.last.respond_to?(:merge) ? args.pop : {}
|
140
|
+
query = scoped_query(query.merge(:limit => args.first || 1))
|
141
|
+
|
142
|
+
if args.any?
|
143
|
+
query.repository.read_many(query)
|
144
|
+
else
|
145
|
+
query.repository.read_one(query)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# Simulates Array#last by returning the last entry (when
|
151
|
+
# there are no arguments), or transforming the collection's
|
152
|
+
# query by reversing the declared order, and applying
|
153
|
+
# :limit => n when you supply an Integer. If you
|
154
|
+
# supply a conditions hash, or a Query object, the
|
155
|
+
# internal query is scoped and a new collection is returned
|
156
|
+
#
|
157
|
+
# @calls Collection#first
|
158
|
+
#
|
159
|
+
# @api public
|
160
|
+
def last(*args)
|
161
|
+
return super if loaded? && args.empty?
|
162
|
+
|
163
|
+
reversed = reverse
|
164
|
+
|
165
|
+
# tell the collection to reverse the order of the
|
166
|
+
# results coming out of the adapter
|
167
|
+
reversed.query.add_reversed = !query.add_reversed?
|
168
|
+
|
169
|
+
reversed.first(*args)
|
170
|
+
end
|
171
|
+
|
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
|
176
|
+
#
|
177
|
+
# @calls Collection#first
|
178
|
+
# @calls Collection#last
|
179
|
+
#
|
180
|
+
# @api public
|
181
|
+
def at(offset)
|
182
|
+
return super if loaded?
|
183
|
+
offset >= 0 ? first(:offset => offset) : last(:offset => offset.abs - 1)
|
184
|
+
end
|
185
|
+
|
186
|
+
##
|
187
|
+
# Simulates Array#slice and returns a new Collection
|
188
|
+
# whose query has a new offset or limit according to the
|
189
|
+
# arguments provided.
|
190
|
+
#
|
191
|
+
# If you provide a range, the min is used as the offset
|
192
|
+
# and the max minues the offset is used as the limit.
|
193
|
+
#
|
194
|
+
# @param [Integer, Array(Integer), Range] args the offset,
|
195
|
+
# offset and limit, or range indicating offsets and limits
|
196
|
+
#
|
197
|
+
# @return [DataMapper::Resource, DataMapper::Collection]
|
198
|
+
# The entry which resides at that offset and limit,
|
199
|
+
# or a new Collection object with the set limits and offset
|
200
|
+
#
|
201
|
+
# @raise [ArgumentError] "arguments may be 1 or 2 Integers,
|
202
|
+
# or 1 Range object, was: #{args.inspect}"
|
203
|
+
#
|
204
|
+
# @alias []
|
205
|
+
#
|
206
|
+
# @api public
|
207
|
+
def slice(*args)
|
208
|
+
return at(args.first) if args.size == 1 && args.first.kind_of?(Integer)
|
209
|
+
|
210
|
+
if args.size == 2 && args.first.kind_of?(Integer) && args.last.kind_of?(Integer)
|
211
|
+
offset, limit = args
|
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
|
219
|
+
end
|
220
|
+
|
221
|
+
all(:offset => offset, :limit => limit)
|
222
|
+
end
|
223
|
+
|
224
|
+
alias [] slice
|
225
|
+
|
226
|
+
##
|
227
|
+
#
|
228
|
+
# @return [DataMapper::Collection] a new collection whose
|
229
|
+
# query is sorted in the reverse
|
230
|
+
#
|
231
|
+
# @see Array#reverse, DataMapper#all, DataMapper::Query#reverse
|
232
|
+
#
|
233
|
+
# @api public
|
234
|
+
def reverse
|
235
|
+
all(self.query.reverse)
|
236
|
+
end
|
237
|
+
|
238
|
+
##
|
239
|
+
# @see Array#<<
|
240
|
+
#
|
241
|
+
# @api public
|
242
|
+
def <<(resource)
|
243
|
+
super
|
244
|
+
relate_resource(resource)
|
245
|
+
self
|
246
|
+
end
|
247
|
+
|
248
|
+
##
|
249
|
+
# @see Array#push
|
250
|
+
#
|
251
|
+
# @api public
|
252
|
+
def push(*resources)
|
253
|
+
super
|
254
|
+
resources.each { |resource| relate_resource(resource) }
|
255
|
+
self
|
256
|
+
end
|
257
|
+
|
258
|
+
##
|
259
|
+
# @see Array#unshift
|
260
|
+
#
|
261
|
+
# @api public
|
262
|
+
def unshift(*resources)
|
263
|
+
super
|
264
|
+
resources.each { |resource| relate_resource(resource) }
|
265
|
+
self
|
266
|
+
end
|
267
|
+
|
268
|
+
##
|
269
|
+
# @see Array#replace
|
270
|
+
#
|
271
|
+
# @api public
|
272
|
+
def replace(other)
|
273
|
+
if loaded?
|
274
|
+
each { |resource| orphan_resource(resource) }
|
275
|
+
end
|
276
|
+
super
|
277
|
+
other.each { |resource| relate_resource(resource) }
|
278
|
+
self
|
279
|
+
end
|
280
|
+
|
281
|
+
##
|
282
|
+
# @see Array#pop
|
283
|
+
#
|
284
|
+
# @api public
|
285
|
+
def pop
|
286
|
+
orphan_resource(super)
|
287
|
+
end
|
288
|
+
|
289
|
+
##
|
290
|
+
# @see Array#shift
|
291
|
+
#
|
292
|
+
# @api public
|
293
|
+
def shift
|
294
|
+
orphan_resource(super)
|
295
|
+
end
|
296
|
+
|
297
|
+
##
|
298
|
+
# @see Array#delete
|
299
|
+
#
|
300
|
+
# @api public
|
301
|
+
def delete(resource)
|
302
|
+
orphan_resource(super)
|
303
|
+
end
|
304
|
+
|
305
|
+
##
|
306
|
+
# @see Array#delete_at
|
307
|
+
#
|
308
|
+
# @api public
|
309
|
+
def delete_at(index)
|
310
|
+
orphan_resource(super)
|
311
|
+
end
|
312
|
+
|
313
|
+
##
|
314
|
+
# @see Array#clear
|
315
|
+
#
|
316
|
+
# @api public
|
317
|
+
def clear
|
318
|
+
if loaded?
|
319
|
+
each { |resource| orphan_resource(resource) }
|
320
|
+
end
|
321
|
+
super
|
322
|
+
self
|
323
|
+
end
|
324
|
+
|
325
|
+
# builds a new resource and appends it to the collection
|
326
|
+
#
|
327
|
+
# @param Hash[Symbol => Object] attributes attributes which
|
328
|
+
# the new resource should have.
|
329
|
+
#
|
330
|
+
# @api public
|
331
|
+
def build(attributes = {})
|
332
|
+
repository.scope do
|
333
|
+
resource = model.new(default_attributes.merge(attributes))
|
334
|
+
self << resource
|
335
|
+
resource
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
##
|
340
|
+
# creates a new resource, saves it, and appends it to the collection
|
341
|
+
#
|
342
|
+
# @param Hash[Symbol => Object] attributes attributes which
|
343
|
+
# the new resource should have.
|
344
|
+
#
|
345
|
+
# @api public
|
346
|
+
def create(attributes = {})
|
347
|
+
repository.scope do
|
348
|
+
resource = model.create(default_attributes.merge(attributes))
|
349
|
+
self << resource unless resource.new_record?
|
350
|
+
resource
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def update(attributes = {}, preload = false)
|
355
|
+
raise NotImplementedError, 'update *with* validations has not be written yet, try update!'
|
356
|
+
end
|
357
|
+
|
358
|
+
##
|
359
|
+
# batch updates the entries belongs to this collection, and skip
|
360
|
+
# validations for all resources.
|
361
|
+
#
|
362
|
+
# @example Reached the Age of Alchohol Consumption
|
363
|
+
# Person.all(:age.gte => 21).update!(:allow_beer => true)
|
364
|
+
#
|
365
|
+
# @param attributes Hash[Symbol => Object] attributes to update
|
366
|
+
# @param reload [FalseClass, TrueClass] if set to true, collection
|
367
|
+
# will have loaded resources reflect updates.
|
368
|
+
#
|
369
|
+
# @return [TrueClass, FalseClass]
|
370
|
+
# TrueClass indicates that all entries were affected
|
371
|
+
# FalseClass indicates that some entries were affected
|
372
|
+
#
|
373
|
+
# @api public
|
374
|
+
def update!(attributes = {}, reload = false)
|
375
|
+
# TODO: delegate to Model.update
|
376
|
+
return true if attributes.empty?
|
377
|
+
|
378
|
+
dirty_attributes = {}
|
379
|
+
|
380
|
+
model.properties(repository.name).slice(*attributes.keys).each do |property|
|
381
|
+
dirty_attributes[property] = attributes[property.name] if property
|
382
|
+
end
|
383
|
+
|
384
|
+
# this should never be done on update! even if collection is loaded. or?
|
385
|
+
# each { |resource| resource.attributes = attributes } if loaded?
|
386
|
+
|
387
|
+
changes = repository.update(dirty_attributes, scoped_query)
|
388
|
+
|
389
|
+
# need to decide if this should be done in update!
|
390
|
+
query.update(attributes)
|
391
|
+
|
392
|
+
if identity_map.any? && reload
|
393
|
+
reload_query = @key_properties.zip(identity_map.keys.transpose).to_hash
|
394
|
+
model.all(reload_query.merge(attributes)).reload(:fields => attributes.keys)
|
395
|
+
end
|
396
|
+
|
397
|
+
# this should return true if there are any changes at all. as it skips validations
|
398
|
+
# the only way it could be fewer changes is if some resources already was updated.
|
399
|
+
# that should not return false? true = 'now all objects have these new values'
|
400
|
+
return loaded? ? changes == size : changes > 0
|
401
|
+
end
|
402
|
+
|
403
|
+
def destroy
|
404
|
+
raise NotImplementedError, 'destroy *with* validations has not be written yet, try destroy!'
|
405
|
+
end
|
406
|
+
|
407
|
+
##
|
408
|
+
# batch destroy the entries belongs to this collection, and skip
|
409
|
+
# validations for all resources.
|
410
|
+
#
|
411
|
+
# @example The War On Terror (if only it were this easy)
|
412
|
+
# Person.all(:terrorist => true).destroy() #
|
413
|
+
#
|
414
|
+
# @return [TrueClass, FalseClass]
|
415
|
+
# TrueClass indicates that all entries were affected
|
416
|
+
# FalseClass indicates that some entries were affected
|
417
|
+
#
|
418
|
+
# @api public
|
419
|
+
def destroy!
|
420
|
+
# TODO: delegate to Model.destroy
|
421
|
+
if loaded?
|
422
|
+
return false unless repository.delete(scoped_query) == size
|
423
|
+
|
424
|
+
each do |resource|
|
425
|
+
resource.instance_variable_set(:@new_record, true)
|
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
|
433
|
+
end
|
434
|
+
else
|
435
|
+
return false unless repository.delete(scoped_query) > 0
|
436
|
+
end
|
437
|
+
|
438
|
+
clear
|
439
|
+
|
440
|
+
true
|
441
|
+
end
|
442
|
+
|
443
|
+
##
|
444
|
+
# @return [DataMapper::PropertySet] The set of properties this
|
445
|
+
# query will be retrieving
|
446
|
+
#
|
447
|
+
# @api public
|
448
|
+
def properties
|
449
|
+
PropertySet.new(query.fields)
|
450
|
+
end
|
451
|
+
|
452
|
+
##
|
453
|
+
# @return [DataMapper::Relationship] The model's relationships
|
454
|
+
#
|
455
|
+
# @api public
|
456
|
+
def relationships
|
457
|
+
model.relationships(repository.name)
|
458
|
+
end
|
459
|
+
|
460
|
+
##
|
461
|
+
# default values to use when creating a Resource within the Collection
|
462
|
+
#
|
463
|
+
# @return [Hash] The default attributes for DataMapper::Collection#create
|
464
|
+
#
|
465
|
+
# @see DataMapper::Collection#create
|
466
|
+
#
|
467
|
+
# @api public
|
468
|
+
def default_attributes
|
469
|
+
default_attributes = {}
|
470
|
+
query.conditions.each do |tuple|
|
471
|
+
operator, property, bind_value = *tuple
|
472
|
+
|
473
|
+
next unless operator == :eql &&
|
474
|
+
property.kind_of?(DataMapper::Property) &&
|
475
|
+
![ Array, Range ].any? { |k| bind_value.kind_of?(k) }
|
476
|
+
!@key_properties.include?(property)
|
477
|
+
|
478
|
+
default_attributes[property.name] = bind_value
|
479
|
+
end
|
480
|
+
default_attributes
|
481
|
+
end
|
482
|
+
|
483
|
+
##
|
484
|
+
# check to see if collection can respond to the method
|
485
|
+
#
|
486
|
+
# @param method [Symbol] method to check in the object
|
487
|
+
# @param include_private [FalseClass, TrueClass] if set to true,
|
488
|
+
# collection will check private methods
|
489
|
+
#
|
490
|
+
# @return [TrueClass, FalseClass]
|
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
|
493
|
+
#
|
494
|
+
# @api public
|
495
|
+
def respond_to?(method, include_private = false)
|
496
|
+
super || model.public_methods(false).map { |m| m.to_s }.include?(method.to_s) || relationships.has_key?(method)
|
497
|
+
end
|
498
|
+
|
499
|
+
# TODO: add docs
|
500
|
+
# @api private
|
501
|
+
def _dump(*)
|
502
|
+
Marshal.dump([ query, to_a ])
|
503
|
+
end
|
504
|
+
|
505
|
+
# TODO: add docs
|
506
|
+
# @api private
|
507
|
+
def self._load(marshalled)
|
508
|
+
query, array = Marshal.load(marshalled)
|
509
|
+
|
510
|
+
# XXX: IMHO it is a code smell to be forced to use allocate
|
511
|
+
# and instance_variable_set to load an object. You should
|
512
|
+
# be able to use a constructor to provide all the info needed
|
513
|
+
# to initialize an object. This should be fixed in the edge
|
514
|
+
# branch dkubb/dm-core
|
515
|
+
|
516
|
+
collection = allocate
|
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
|
523
|
+
end
|
524
|
+
|
525
|
+
protected
|
526
|
+
|
527
|
+
##
|
528
|
+
# @api private
|
529
|
+
def model
|
530
|
+
query.model
|
531
|
+
end
|
532
|
+
|
533
|
+
private
|
534
|
+
|
535
|
+
##
|
536
|
+
# @api public
|
537
|
+
def initialize(query, &block)
|
538
|
+
assert_kind_of 'query', query, Query
|
539
|
+
|
540
|
+
unless block_given?
|
541
|
+
# It can be helpful (relationship.rb: 112-13, used for SEL) to have a non-lazy Collection.
|
542
|
+
block = lambda { |c| }
|
543
|
+
end
|
544
|
+
|
545
|
+
@query = query
|
546
|
+
@key_properties = model.key(repository.name)
|
547
|
+
@cache = {}
|
548
|
+
|
549
|
+
super()
|
550
|
+
|
551
|
+
load_with(&block)
|
552
|
+
end
|
553
|
+
|
554
|
+
##
|
555
|
+
# @api private
|
556
|
+
def add(resource)
|
557
|
+
query.add_reversed? ? unshift(resource) : push(resource)
|
558
|
+
resource
|
559
|
+
end
|
560
|
+
|
561
|
+
##
|
562
|
+
# @api private
|
563
|
+
def relate_resource(resource)
|
564
|
+
return unless resource
|
565
|
+
resource.collection = self
|
566
|
+
@cache[resource.key] = resource
|
567
|
+
resource
|
568
|
+
end
|
569
|
+
|
570
|
+
##
|
571
|
+
# @api private
|
572
|
+
def orphan_resource(resource)
|
573
|
+
return unless resource
|
574
|
+
resource.collection = nil if resource.collection.object_id == self.object_id
|
575
|
+
@cache.delete(resource.key)
|
576
|
+
resource
|
577
|
+
end
|
578
|
+
|
579
|
+
##
|
580
|
+
# @api private
|
581
|
+
def scoped_query(query = self.query)
|
582
|
+
assert_kind_of 'query', query, Query, Hash
|
583
|
+
|
584
|
+
query.update(keys) if loaded?
|
585
|
+
|
586
|
+
return self.query if query == self.query
|
587
|
+
|
588
|
+
query = if query.kind_of?(Hash)
|
589
|
+
Query.new(query.has_key?(:repository) ? query.delete(:repository) : self.repository, model, query)
|
590
|
+
else
|
591
|
+
query
|
592
|
+
end
|
593
|
+
|
594
|
+
if query.limit || query.offset > 0
|
595
|
+
set_relative_position(query)
|
596
|
+
end
|
597
|
+
|
598
|
+
self.query.merge(query)
|
599
|
+
end
|
600
|
+
|
601
|
+
##
|
602
|
+
# @api private
|
603
|
+
def keys
|
604
|
+
keys = map {|r| r.key }
|
605
|
+
keys.any? ? @key_properties.zip(keys.transpose).to_hash : {}
|
606
|
+
end
|
607
|
+
|
608
|
+
##
|
609
|
+
# @api private
|
610
|
+
def identity_map
|
611
|
+
repository.identity_map(model)
|
612
|
+
end
|
613
|
+
|
614
|
+
##
|
615
|
+
# @api private
|
616
|
+
def set_relative_position(query)
|
617
|
+
return if query == self.query
|
618
|
+
|
619
|
+
if query.offset == 0
|
620
|
+
return if !query.limit.nil? && !self.query.limit.nil? && query.limit <= self.query.limit
|
621
|
+
return if query.limit.nil? && self.query.limit.nil?
|
622
|
+
end
|
623
|
+
|
624
|
+
first_pos = self.query.offset + query.offset
|
625
|
+
last_pos = self.query.offset + self.query.limit if self.query.limit
|
626
|
+
|
627
|
+
if limit = query.limit
|
628
|
+
if last_pos.nil? || first_pos + limit < last_pos
|
629
|
+
last_pos = first_pos + limit
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
if last_pos && first_pos >= last_pos
|
634
|
+
raise 'outside range' # TODO: raise a proper exception object
|
635
|
+
end
|
636
|
+
|
637
|
+
query.update(:offset => first_pos)
|
638
|
+
query.update(:limit => last_pos - first_pos) if last_pos
|
639
|
+
end
|
640
|
+
|
641
|
+
##
|
642
|
+
# @api private
|
643
|
+
def method_missing(method, *args, &block)
|
644
|
+
if model.public_methods(false).map { |m| m.to_s }.include?(method.to_s)
|
645
|
+
model.send(:with_scope, query) do
|
646
|
+
model.send(method, *args, &block)
|
647
|
+
end
|
648
|
+
elsif relationship = relationships[method]
|
649
|
+
klass = model == relationship.child_model ? relationship.parent_model : relationship.child_model
|
650
|
+
|
651
|
+
# TODO: when self.query includes an offset/limit use it as a
|
652
|
+
# subquery to scope the results rather than a join
|
653
|
+
|
654
|
+
query = Query.new(repository, klass)
|
655
|
+
query.conditions.push(*self.query.conditions)
|
656
|
+
query.update(relationship.query)
|
657
|
+
query.update(args.pop) if args.last.kind_of?(Hash)
|
658
|
+
|
659
|
+
query.update(
|
660
|
+
:fields => klass.properties(repository.name).defaults,
|
661
|
+
:links => [ relationship ] + self.query.links
|
662
|
+
)
|
663
|
+
|
664
|
+
klass.all(query, &block)
|
665
|
+
else
|
666
|
+
super
|
667
|
+
end
|
668
|
+
end
|
669
|
+
end # class Collection
|
670
|
+
end # module DataMapper
|