dm-core 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/CHANGELOG +144 -0
  2. data/FAQ +74 -0
  3. data/MIT-LICENSE +22 -0
  4. data/QUICKLINKS +12 -0
  5. data/README +143 -0
  6. data/lib/dm-core.rb +213 -0
  7. data/lib/dm-core/adapters.rb +4 -0
  8. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +701 -0
  10. data/lib/dm-core/adapters/mysql_adapter.rb +132 -0
  11. data/lib/dm-core/adapters/postgres_adapter.rb +179 -0
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  13. data/lib/dm-core/associations.rb +172 -0
  14. data/lib/dm-core/associations/many_to_many.rb +138 -0
  15. data/lib/dm-core/associations/many_to_one.rb +101 -0
  16. data/lib/dm-core/associations/one_to_many.rb +275 -0
  17. data/lib/dm-core/associations/one_to_one.rb +61 -0
  18. data/lib/dm-core/associations/relationship.rb +116 -0
  19. data/lib/dm-core/associations/relationship_chain.rb +74 -0
  20. data/lib/dm-core/auto_migrations.rb +64 -0
  21. data/lib/dm-core/collection.rb +604 -0
  22. data/lib/dm-core/hook.rb +11 -0
  23. data/lib/dm-core/identity_map.rb +45 -0
  24. data/lib/dm-core/is.rb +16 -0
  25. data/lib/dm-core/logger.rb +233 -0
  26. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  27. data/lib/dm-core/migrator.rb +29 -0
  28. data/lib/dm-core/model.rb +399 -0
  29. data/lib/dm-core/naming_conventions.rb +52 -0
  30. data/lib/dm-core/property.rb +611 -0
  31. data/lib/dm-core/property_set.rb +158 -0
  32. data/lib/dm-core/query.rb +590 -0
  33. data/lib/dm-core/repository.rb +159 -0
  34. data/lib/dm-core/resource.rb +618 -0
  35. data/lib/dm-core/scope.rb +35 -0
  36. data/lib/dm-core/support.rb +7 -0
  37. data/lib/dm-core/support/array.rb +13 -0
  38. data/lib/dm-core/support/assertions.rb +8 -0
  39. data/lib/dm-core/support/errors.rb +23 -0
  40. data/lib/dm-core/support/kernel.rb +7 -0
  41. data/lib/dm-core/support/symbol.rb +41 -0
  42. data/lib/dm-core/transaction.rb +267 -0
  43. data/lib/dm-core/type.rb +160 -0
  44. data/lib/dm-core/type_map.rb +80 -0
  45. data/lib/dm-core/types.rb +19 -0
  46. data/lib/dm-core/types/boolean.rb +7 -0
  47. data/lib/dm-core/types/discriminator.rb +32 -0
  48. data/lib/dm-core/types/object.rb +20 -0
  49. data/lib/dm-core/types/paranoid_boolean.rb +23 -0
  50. data/lib/dm-core/types/paranoid_datetime.rb +22 -0
  51. data/lib/dm-core/types/serial.rb +9 -0
  52. data/lib/dm-core/types/text.rb +10 -0
  53. data/spec/integration/association_spec.rb +1215 -0
  54. data/spec/integration/association_through_spec.rb +150 -0
  55. data/spec/integration/associations/many_to_many_spec.rb +171 -0
  56. data/spec/integration/associations/many_to_one_spec.rb +123 -0
  57. data/spec/integration/associations/one_to_many_spec.rb +66 -0
  58. data/spec/integration/auto_migrations_spec.rb +398 -0
  59. data/spec/integration/collection_spec.rb +1015 -0
  60. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  61. data/spec/integration/model_spec.rb +68 -0
  62. data/spec/integration/mysql_adapter_spec.rb +85 -0
  63. data/spec/integration/postgres_adapter_spec.rb +732 -0
  64. data/spec/integration/property_spec.rb +224 -0
  65. data/spec/integration/query_spec.rb +376 -0
  66. data/spec/integration/repository_spec.rb +57 -0
  67. data/spec/integration/resource_spec.rb +324 -0
  68. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  69. data/spec/integration/sti_spec.rb +185 -0
  70. data/spec/integration/transaction_spec.rb +75 -0
  71. data/spec/integration/type_spec.rb +149 -0
  72. data/spec/lib/mock_adapter.rb +27 -0
  73. data/spec/spec_helper.rb +112 -0
  74. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  75. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  76. data/spec/unit/adapters/data_objects_adapter_spec.rb +627 -0
  77. data/spec/unit/adapters/postgres_adapter_spec.rb +125 -0
  78. data/spec/unit/associations/many_to_many_spec.rb +14 -0
  79. data/spec/unit/associations/many_to_one_spec.rb +138 -0
  80. data/spec/unit/associations/one_to_many_spec.rb +385 -0
  81. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  82. data/spec/unit/associations/relationship_spec.rb +67 -0
  83. data/spec/unit/associations_spec.rb +205 -0
  84. data/spec/unit/auto_migrations_spec.rb +110 -0
  85. data/spec/unit/collection_spec.rb +174 -0
  86. data/spec/unit/data_mapper_spec.rb +21 -0
  87. data/spec/unit/identity_map_spec.rb +126 -0
  88. data/spec/unit/is_spec.rb +80 -0
  89. data/spec/unit/migrator_spec.rb +33 -0
  90. data/spec/unit/model_spec.rb +339 -0
  91. data/spec/unit/naming_conventions_spec.rb +28 -0
  92. data/spec/unit/property_set_spec.rb +96 -0
  93. data/spec/unit/property_spec.rb +447 -0
  94. data/spec/unit/query_spec.rb +485 -0
  95. data/spec/unit/repository_spec.rb +93 -0
  96. data/spec/unit/resource_spec.rb +557 -0
  97. data/spec/unit/scope_spec.rb +131 -0
  98. data/spec/unit/transaction_spec.rb +493 -0
  99. data/spec/unit/type_map_spec.rb +114 -0
  100. data/spec/unit/type_spec.rb +119 -0
  101. metadata +187 -0
@@ -0,0 +1,61 @@
1
+ module DataMapper
2
+ module Associations
3
+ module OneToOne
4
+ extend Assertions
5
+
6
+ # Setup one to one relationship between two models
7
+ # -
8
+ # @api private
9
+ def self.setup(name, model, options = {})
10
+ assert_kind_of 'name', name, Symbol
11
+ assert_kind_of 'model', model, Model
12
+ assert_kind_of 'options', options, Hash
13
+
14
+ repository_name = model.repository.name
15
+
16
+ model.class_eval <<-EOS, __FILE__, __LINE__
17
+ def #{name}
18
+ #{name}_association.first
19
+ end
20
+
21
+ def #{name}=(child_resource)
22
+ #{name}_association.replace(child_resource.nil? ? [] : [ child_resource ])
23
+ end
24
+
25
+ private
26
+
27
+ def #{name}_association
28
+ @#{name}_association ||= begin
29
+ unless relationship = model.relationships(#{repository_name.inspect})[:#{name}]
30
+ raise ArgumentError, 'Relationship #{name.inspect} does not exist'
31
+ end
32
+ association = Associations::OneToMany::Proxy.new(relationship, self)
33
+ parent_associations << association
34
+ association
35
+ end
36
+ end
37
+ EOS
38
+
39
+ model.relationships(repository_name)[name] = if options.has_key?(:through)
40
+ RelationshipChain.new(
41
+ :child_model => options.fetch(:class_name, Extlib::Inflection.classify(name)),
42
+ :parent_model => model.name,
43
+ :repository_name => repository_name,
44
+ :near_relationship_name => options[:through],
45
+ :remote_relationship_name => options.fetch(:remote_name, name),
46
+ :parent_key => options[:parent_key],
47
+ :child_key => options[:child_key]
48
+ )
49
+ else
50
+ Relationship.new(
51
+ Extlib::Inflection.underscore(Extlib::Inflection.demodulize(model.name)).to_sym,
52
+ repository_name,
53
+ options.fetch(:class_name, Extlib::Inflection.classify(name)),
54
+ model.name,
55
+ options
56
+ )
57
+ end
58
+ end
59
+ end # module HasOne
60
+ end # module Associations
61
+ end # module DataMapper
@@ -0,0 +1,116 @@
1
+ module DataMapper
2
+ module Associations
3
+ class Relationship
4
+ include Assertions
5
+
6
+ OPTIONS = [ :class_name, :child_key, :parent_key, :min, :max, :through ]
7
+
8
+ attr_reader :name, :repository_name, :options, :query
9
+
10
+ def child_key
11
+ @child_key ||= begin
12
+ model_properties = child_model.properties(repository_name)
13
+
14
+ child_key = parent_key.zip(@child_properties || []).map do |parent_property,property_name|
15
+ # TODO: use something similar to DM::NamingConventions to determine the property name
16
+ property_name ||= "#{name}_#{parent_property.name}".to_sym
17
+
18
+ model_properties[property_name] || DataMapper.repository(repository_name) do
19
+ attributes = {}
20
+
21
+ [ :length, :precision, :scale ].each do |attribute|
22
+ attributes[attribute] = parent_property.send(attribute)
23
+ end
24
+
25
+ # NOTE: hack to make each many to many child_key a true key,
26
+ # until I can figure out a better place for this check
27
+ if child_model.respond_to?(:many_to_many)
28
+ attributes[:key] = true
29
+ end
30
+
31
+ child_model.property(property_name, parent_property.primitive, attributes)
32
+ end
33
+ end
34
+ PropertySet.new(child_key)
35
+ end
36
+ end
37
+
38
+ def parent_key
39
+ @parent_key ||= begin
40
+ parent_key = if @parent_properties
41
+ parent_model.properties(repository_name).slice(*@parent_properties)
42
+ else
43
+ parent_model.key(repository_name)
44
+ end
45
+
46
+ PropertySet.new(parent_key)
47
+ end
48
+ end
49
+
50
+ def parent_model
51
+ Class === @parent_model ? @parent_model : self.class.find_const(@parent_model)
52
+ end
53
+
54
+ def child_model
55
+ Class === @child_model ? @child_model : self.class.find_const(@child_model)
56
+ end
57
+
58
+ # @api private
59
+ def get_children(parent, options = {}, finder = :all, *args)
60
+ bind_values = parent_key.get(parent)
61
+ return [] if bind_values.any? { |bind_value| bind_value.nil? }
62
+ query = child_key.to_query(bind_values)
63
+
64
+ DataMapper.repository(repository_name) do
65
+ child_model.send(finder, *(args << @query.merge(options).merge(query)))
66
+ end
67
+ end
68
+
69
+ # @api private
70
+ def get_parent(child)
71
+ bind_values = child_key.get(child)
72
+ return nil if bind_values.any? { |bind_value| bind_value.nil? }
73
+ query = parent_key.to_query(bind_values)
74
+
75
+ DataMapper.repository(repository_name) do
76
+ parent_model.first(@query.merge(query))
77
+ end
78
+ end
79
+
80
+ # @api private
81
+ def attach_parent(child, parent)
82
+ child_key.set(child, parent && parent_key.get(parent))
83
+ end
84
+
85
+ private
86
+
87
+ # +child_model_name and child_properties refers to the FK, parent_model_name
88
+ # and parent_properties refer to the PK. For more information:
89
+ # http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html
90
+ # I wash my hands of it!
91
+ def initialize(name, repository_name, child_model, parent_model, options = {})
92
+ assert_kind_of 'name', name, Symbol
93
+ assert_kind_of 'repository_name', repository_name, Symbol
94
+ assert_kind_of 'child_model', child_model, String, Class
95
+ assert_kind_of 'parent_model', parent_model, String, Class
96
+
97
+ if child_properties = options[:child_key]
98
+ assert_kind_of 'options[:child_key]', child_properties, Array
99
+ end
100
+
101
+ if parent_properties = options[:parent_key]
102
+ assert_kind_of 'options[:parent_key]', parent_properties, Array
103
+ end
104
+
105
+ @name = name
106
+ @repository_name = repository_name
107
+ @child_model = child_model
108
+ @child_properties = child_properties # may be nil
109
+ @query = options.reject { |k,v| OPTIONS.include?(k) }
110
+ @parent_model = parent_model
111
+ @parent_properties = parent_properties # may be nil
112
+ @options = options
113
+ end
114
+ end # class Relationship
115
+ end # module Associations
116
+ end # module DataMapper
@@ -0,0 +1,74 @@
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
+ def child_model
14
+ near_relationship.child_model
15
+ end
16
+
17
+ # @api private
18
+ def get_children(parent, options = {}, finder = :all, *args)
19
+ query = @query.merge(options).merge(child_key.to_query(parent_key.get(parent)))
20
+
21
+ query[:links] = links
22
+
23
+ DataMapper.repository(parent.repository.name) do
24
+ results = grandchild_model.send(finder, *(args << query))
25
+ # FIXME: remove the need for the uniq.freeze
26
+ finder == :all ? (@mutable ? results.uniq : results.uniq.freeze) : results
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def near_relationship
33
+ parent_model.relationships[@near_relationship_name]
34
+ end
35
+
36
+ def links
37
+ if remote_relationship.kind_of?(RelationshipChain)
38
+ remote_relationship.instance_eval { links } + [remote_relationship.instance_eval { near_relationship } ]
39
+ else
40
+ [ remote_relationship ]
41
+ end
42
+ end
43
+
44
+ def remote_relationship
45
+ near_relationship.child_model.relationships[@remote_relationship_name] ||
46
+ near_relationship.child_model.relationships[@remote_relationship_name.to_s.singularize.to_sym]
47
+ end
48
+
49
+ def grandchild_model
50
+ Class === @child_model ? @child_model : self.class.find_const(@child_model)
51
+ end
52
+
53
+ def initialize(options)
54
+ if (missing_options = OPTIONS - [ :min, :max ] - options.keys ).any?
55
+ raise ArgumentError, "The options #{missing_options * ', '} are required", caller
56
+ end
57
+
58
+ @repository_name = options.fetch(:repository_name)
59
+ @near_relationship_name = options.fetch(:near_relationship_name)
60
+ @remote_relationship_name = options.fetch(:remote_relationship_name)
61
+ @child_model = options.fetch(:child_model)
62
+ @parent_model = options.fetch(:parent_model)
63
+ @parent_properties = options.fetch(:parent_key)
64
+ @child_properties = options.fetch(:child_key)
65
+ @mutable = options.delete(:mutable) || false
66
+
67
+ @name = near_relationship.name
68
+ @query = options.reject{ |key,val| OPTIONS.include?(key) }
69
+ @extra_links = []
70
+ @options = options
71
+ end
72
+ end # class Relationship
73
+ end # module Associations
74
+ end # module DataMapper
@@ -0,0 +1,64 @@
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
+ # REPEAT: THIS IS DESTRUCTIVE
8
+ #
9
+ # @param Symbol repository_name the repository to be migrated
10
+ # @calls DataMapper::Resource#auto_migrate!
11
+ def self.auto_migrate(repository_name = nil)
12
+ DataMapper::Resource.descendants.each do |model|
13
+ model.auto_migrate!(repository_name)
14
+ end
15
+ end
16
+
17
+ ##
18
+ # Safely migrates the data-store to match the model
19
+ # preserving data already in the data-store
20
+ #
21
+ # @param Symbol repository_name the repository to be migrated
22
+ # @calls DataMapper::Resource#auto_upgrade!
23
+ def self.auto_upgrade(repository_name = nil)
24
+ DataMapper::Resource.descendants.each do |model|
25
+ model.auto_upgrade!(repository_name)
26
+ end
27
+ end
28
+ end # class AutoMigrator
29
+
30
+ module AutoMigrations
31
+ ##
32
+ # Destructively automigrates the data-store to match the model
33
+ # REPEAT: THIS IS DESTRUCTIVE
34
+ #
35
+ # @param Symbol repository_name the repository to be migrated
36
+ def auto_migrate!(repository_name = nil)
37
+ if self.superclass != Object
38
+ self.superclass.auto_migrate!(repository_name)
39
+ else
40
+ repository_name ||= default_repository_name
41
+ repository(repository_name) do |r|
42
+ (relationships(r.name)||{}).each_value { |relationship| relationship.child_key }
43
+ r.adapter.destroy_model_storage(r, self)
44
+ r.adapter.create_model_storage(r, self)
45
+ end
46
+ end
47
+ end
48
+
49
+ ##
50
+ # Safely migrates the data-store to match the model
51
+ # preserving data already in the data-store
52
+ #
53
+ # @param Symbol repository_name the repository to be migrated
54
+ def auto_upgrade!(repository_name = nil)
55
+ repository_name ||= default_repository_name
56
+ repository(repository_name) do |r|
57
+ (relationships(r.name)||{}).each_value { |relationship| relationship.child_key }
58
+ r.adapter.upgrade_model_storage(r, self)
59
+ end
60
+ end
61
+
62
+ Model.send(:include, self)
63
+ end # module AutoMigrations
64
+ end # module DataMapper
@@ -0,0 +1,604 @@
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
+ @query.update(:fields => @query.fields | @key_properties)
39
+ replace(all(:reload => true))
40
+ end
41
+
42
+ ##
43
+ # retrieves an entry out of the collection's entry by key
44
+ #
45
+ # @param [DataMapper::Types::*, ...] key keys which uniquely
46
+ # identify a resource in the collection
47
+ #
48
+ # @return [DataMapper::Resource, NilClass] the resource which
49
+ # has the supplied keys
50
+ #
51
+ # @api public
52
+ def get(*key)
53
+ if loaded?
54
+ # loop over the collection to find the matching resource
55
+ detect { |resource| resource.key == key }
56
+ elsif query.limit || query.offset > 0
57
+ # current query is exclusive, find resource within the set
58
+
59
+ # TODO: use a subquery to retrieve the collection and then match
60
+ # it up against the key. This will require some changes to
61
+ # how subqueries are generated, since the key may be a
62
+ # composite key. In the case of DO adapters, it means subselects
63
+ # like the form "(a, b) IN(SELECT a,b FROM ...)", which will
64
+ # require making it so the Query condition key can be a
65
+ # Property or an Array of Property objects
66
+
67
+ # use the brute force approach until subquery lookups work
68
+ lazy_load
69
+ get(*key)
70
+ else
71
+ # current query is all inclusive, lookup using normal approach
72
+ first(model.to_query(repository, key))
73
+ end
74
+ end
75
+
76
+ ##
77
+ # retrieves an entry out of the collection's entry by key,
78
+ # raising an exception if the object cannot be found
79
+ #
80
+ # @param [DataMapper::Types::*, ...] key keys which uniquely
81
+ # identify a resource in the collection
82
+ #
83
+ # @calls DataMapper::Collection#get
84
+ #
85
+ # @raise [ObjectNotFoundError] "Could not find #{model.name} with key #{key.inspect} in collection"
86
+ #
87
+ # @api public
88
+ def get!(*key)
89
+ get(*key) || raise(ObjectNotFoundError, "Could not find #{model.name} with key #{key.inspect} in collection")
90
+ end
91
+
92
+ ##
93
+ # Further refines a collection's conditions. #all provides an
94
+ # interface which simulates a database view.
95
+ #
96
+ # @param [Hash[Symbol, Object], DataMapper::Query] query parameters for
97
+ # an query within the results of the original query.
98
+ #
99
+ # @return [DataMapper::Collection] a collection whose query is the result
100
+ # of a merge
101
+ #
102
+ # @api public
103
+ def all(query = {})
104
+ # TODO: this shouldn't be a kicker if scoped_query() is called
105
+ return self if query.kind_of?(Hash) ? query.empty? : query == self.query
106
+ query = scoped_query(query)
107
+ query.repository.read_many(query)
108
+ end
109
+
110
+ ##
111
+ # Simulates Array#first by returning the first entry (when
112
+ # there are no arguments), or transforms the collection's query
113
+ # by applying :limit => n when you supply an Integer. If you
114
+ # provide a conditions hash, or a Query object, the internal
115
+ # query is scoped and a new collection is returned
116
+ #
117
+ # @param [Integer, Hash[Symbol, Object], Query] args
118
+ #
119
+ # @return [DataMapper::Resource, DataMapper::Collection] The
120
+ # first resource in the entries of this collection, or
121
+ # a new collection whose query has been merged
122
+ #
123
+ # @api public
124
+ def first(*args)
125
+ # TODO: this shouldn't be a kicker if scoped_query() is called
126
+ if loaded?
127
+ if args.empty?
128
+ return super
129
+ elsif args.size == 1 && args.first.kind_of?(Integer)
130
+ limit = args.shift
131
+ return self.class.new(scoped_query(:limit => limit)) { |c| c.replace(super(limit)) }
132
+ end
133
+ end
134
+
135
+ query = args.last.respond_to?(:merge) ? args.pop : {}
136
+ query = scoped_query(query.merge(:limit => args.first || 1))
137
+
138
+ if args.any?
139
+ query.repository.read_many(query)
140
+ else
141
+ query.repository.read_one(query)
142
+ end
143
+ end
144
+
145
+ ##
146
+ # Simulates Array#last by returning the last entry (when
147
+ # there are no arguments), or transforming the collection's
148
+ # query by reversing the declared order, and applying
149
+ # :limit => n when you supply an Integer. If you
150
+ # supply a conditions hash, or a Query object, the
151
+ # internal query is scoped and a new collection is returned
152
+ #
153
+ # @calls Collection#first
154
+ #
155
+ # @api public
156
+ def last(*args)
157
+ return super if loaded? && args.empty?
158
+
159
+ reversed = reverse
160
+
161
+ # tell the collection to reverse the order of the
162
+ # results coming out of the adapter
163
+ reversed.query.add_reversed = !query.add_reversed?
164
+
165
+ reversed.first(*args)
166
+ end
167
+
168
+ ##
169
+ # Simulates Array#at and returns the entrie at that index.
170
+ # Also accepts negative indexes and appropriate reverses
171
+ # the order of the query
172
+ #
173
+ # @calls Collection#first
174
+ # @calls Collection#last
175
+ #
176
+ # @api public
177
+ def at(offset)
178
+ return super if loaded?
179
+ offset >= 0 ? first(:offset => offset) : last(:offset => offset.abs - 1)
180
+ end
181
+
182
+ ##
183
+ # Simulates Array#slice and returns a new Collection
184
+ # whose query has a new offset or limit according to the
185
+ # arguments provided.
186
+ #
187
+ # If you provide a range, the min is used as the offset
188
+ # and the max minues the offset is used as the limit.
189
+ #
190
+ # @param [Integer, Array(Integer), Range] args the offset,
191
+ # offset and limit, or range indicating offsets and limits
192
+ #
193
+ # @return [DataMapper::Resource, DataMapper::Collection]
194
+ # The entry which resides at that offset and limit,
195
+ # or a new Collection object with the set limits and offset
196
+ #
197
+ # @raise [ArgumentError] "arguments may be 1 or 2 Integers,
198
+ # or 1 Range object, was: #{args.inspect}"
199
+ #
200
+ # @alias []
201
+ #
202
+ # @api public
203
+ def slice(*args)
204
+ return at(args.first) if args.size == 1 && args.first.kind_of?(Integer)
205
+
206
+ if args.size == 2 && args.first.kind_of?(Integer) && args.last.kind_of?(Integer)
207
+ offset, limit = args
208
+ elsif args.size == 1 && args.first.kind_of?(Range)
209
+ range = args.first
210
+ offset = range.first
211
+ limit = range.last - offset
212
+ limit += 1 unless range.exclude_end?
213
+ else
214
+ raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}", caller
215
+ end
216
+
217
+ all(:offset => offset, :limit => limit)
218
+ end
219
+
220
+ alias [] slice
221
+
222
+ ##
223
+ #
224
+ # @return [DataMapper::Collection] a new collection whose
225
+ # query is sorted in the reverse
226
+ #
227
+ # @see Array#reverse, DataMapper#all, DataMapper::Query#reverse
228
+ #
229
+ # @api public
230
+ def reverse
231
+ all(self.query.reverse)
232
+ end
233
+
234
+ ##
235
+ # @see Array#<<
236
+ #
237
+ # @api public
238
+ def <<(resource)
239
+ super
240
+ relate_resource(resource)
241
+ self
242
+ end
243
+
244
+ ##
245
+ # @see Array#push
246
+ #
247
+ # @api public
248
+ def push(*resources)
249
+ super
250
+ resources.each { |resource| relate_resource(resource) }
251
+ self
252
+ end
253
+
254
+ ##
255
+ # @see Array#unshift
256
+ #
257
+ # @api public
258
+ def unshift(*resources)
259
+ super
260
+ resources.each { |resource| relate_resource(resource) }
261
+ self
262
+ end
263
+
264
+ ##
265
+ # @see Array#replace
266
+ #
267
+ # @api public
268
+ def replace(other)
269
+ if loaded?
270
+ each { |resource| orphan_resource(resource) }
271
+ end
272
+ super
273
+ other.each { |resource| relate_resource(resource) }
274
+ self
275
+ end
276
+
277
+ ##
278
+ # @see Array#pop
279
+ #
280
+ # @api public
281
+ def pop
282
+ orphan_resource(super)
283
+ end
284
+
285
+ ##
286
+ # @see Array#shift
287
+ #
288
+ # @api public
289
+ def shift
290
+ orphan_resource(super)
291
+ end
292
+
293
+ ##
294
+ # @see Array#delete
295
+ #
296
+ # @api public
297
+ def delete(resource, &block)
298
+ orphan_resource(super)
299
+ end
300
+
301
+ ##
302
+ # @see Array#delete_at
303
+ #
304
+ # @api public
305
+ def delete_at(index)
306
+ orphan_resource(super)
307
+ end
308
+
309
+ ##
310
+ # @see Array#clear
311
+ #
312
+ # @api public
313
+ def clear
314
+ if loaded?
315
+ each { |resource| orphan_resource(resource) }
316
+ end
317
+ super
318
+ self
319
+ end
320
+
321
+ ##
322
+ # creates a new array, saves it, and << it onto the collection
323
+ #
324
+ # @param Hash[Symbol => Object] attributes attributes which
325
+ # the new resource should have.
326
+ #
327
+ # @api public
328
+ def create(attributes = {})
329
+ repository.scope do
330
+ resource = model.create(default_attributes.merge(attributes))
331
+ self << resource unless resource.new_record?
332
+ resource
333
+ end
334
+ end
335
+
336
+ def update(attributes = {}, preload = false)
337
+ raise NotImplementedError, 'update *with* validations has not be written yet, try update!'
338
+ end
339
+
340
+ ##
341
+ # batch updates the entries belongs to this collection, and skip
342
+ # validations for all resources.
343
+ #
344
+ # @example Reached the Age of Alchohol Consumption
345
+ # Person.all(:age.gte => 21).update!(:allow_beer => true)
346
+ #
347
+ # @param attributes Hash[Symbol => Object] attributes to update
348
+ # @param reload [FalseClass, TrueClass] if set to true, collection
349
+ # will have loaded resources reflect updates.
350
+ #
351
+ # @return [TrueClass, FalseClass]
352
+ # TrueClass indicates that all entries were affected
353
+ # FalseClass indicates that some entries were affected
354
+ #
355
+ # @api public
356
+ def update!(attributes = {}, reload = false)
357
+ # TODO: delegate to Model.update
358
+ return true if attributes.empty?
359
+
360
+ dirty_attributes = {}
361
+
362
+ model.properties(repository.name).slice(*attributes.keys).each do |property|
363
+ dirty_attributes[property] = attributes[property.name] if property
364
+ end
365
+
366
+ # this should never be done on update! even if collection is loaded. or?
367
+ # each { |resource| resource.attributes = attributes } if loaded?
368
+
369
+ changes = repository.update(dirty_attributes, scoped_query)
370
+
371
+ # need to decide if this should be done in update!
372
+ query.update(attributes)
373
+
374
+ if identity_map.any? && reload
375
+ reload_query = @key_properties.zip(identity_map.keys.transpose).to_hash
376
+ model.all(reload_query.merge(attributes)).reload(:fields => attributes.keys)
377
+ end
378
+
379
+ # this should return true if there are any changes at all. as it skips validations
380
+ # the only way it could be fewer changes is if some resources already was updated.
381
+ # that should not return false? true = 'now all objects have these new values'
382
+ return loaded? ? changes == size : changes > 0
383
+ end
384
+
385
+ def destroy
386
+ raise NotImplementedError, 'destroy *with* validations has not be written yet, try destroy!'
387
+ end
388
+
389
+ ##
390
+ # batch destroy the entries belongs to this collection, and skip
391
+ # validations for all resources.
392
+ #
393
+ # @example The War On Terror (if only it were this easy)
394
+ # Person.all(:terrorist => true).destroy() #
395
+ #
396
+ # @return [TrueClass, FalseClass]
397
+ # TrueClass indicates that all entries were affected
398
+ # FalseClass indicates that some entries were affected
399
+ #
400
+ # @api public
401
+ def destroy!
402
+ # TODO: delegate to Model.destroy
403
+ if loaded?
404
+ return false unless repository.delete(scoped_query) == size
405
+
406
+ each do |resource|
407
+ resource.instance_variable_set(:@new_record, true)
408
+ identity_map.delete(resource.key)
409
+ resource.dirty_attributes.clear
410
+
411
+ model.properties(repository.name).each do |property|
412
+ next unless resource.attribute_loaded?(property.name)
413
+ resource.dirty_attributes[property] = property.get(resource)
414
+ end
415
+ end
416
+ else
417
+ return false unless repository.delete(scoped_query) > 0
418
+ end
419
+
420
+ clear
421
+
422
+ true
423
+ end
424
+
425
+ ##
426
+ # @return [DataMapper::PropertySet] The set of properties this
427
+ # query will be retrieving
428
+ #
429
+ # @api public
430
+ def properties
431
+ PropertySet.new(query.fields)
432
+ end
433
+
434
+ ##
435
+ # @return [DataMapper::Relationship] The model's relationships
436
+ #
437
+ # @api public
438
+ def relationships
439
+ model.relationships(repository.name)
440
+ end
441
+
442
+ ##
443
+ # default values to use when creating a Resource within the Collection
444
+ #
445
+ # @return [Hash] The default attributes for DataMapper::Collection#create
446
+ #
447
+ # @see DataMapper::Collection#create
448
+ #
449
+ # @api public
450
+ def default_attributes
451
+ default_attributes = {}
452
+ query.conditions.each do |tuple|
453
+ operator, property, bind_value = *tuple
454
+
455
+ next unless operator == :eql &&
456
+ property.kind_of?(DataMapper::Property) &&
457
+ ![ Array, Range ].any? { |k| bind_value.kind_of?(k) }
458
+ !@key_properties.include?(property)
459
+
460
+ default_attributes[property.name] = bind_value
461
+ end
462
+ default_attributes
463
+ end
464
+
465
+ protected
466
+
467
+ ##
468
+ # @api private
469
+ #
470
+ # @api public
471
+ def model
472
+ query.model
473
+ end
474
+
475
+ private
476
+
477
+ ##
478
+ # @api public
479
+ def initialize(query, &block)
480
+ assert_kind_of 'query', query, Query
481
+
482
+ unless block_given?
483
+ raise ArgumentError, 'a block must be supplied for lazy loading results', caller
484
+ end
485
+
486
+ @query = query
487
+ @key_properties = model.key(repository.name)
488
+
489
+ super()
490
+
491
+ load_with(&block)
492
+ end
493
+
494
+ ##
495
+ # @api private
496
+ def add(resource)
497
+ query.add_reversed? ? unshift(resource) : push(resource)
498
+ resource
499
+ end
500
+
501
+ ##
502
+ # @api private
503
+ def relate_resource(resource)
504
+ return unless resource
505
+ resource.collection = self
506
+ resource
507
+ end
508
+
509
+ ##
510
+ # @api private
511
+ def orphan_resource(resource)
512
+ return unless resource
513
+ resource.collection = nil if resource.collection == self
514
+ resource
515
+ end
516
+
517
+ ##
518
+ # @api private
519
+ def scoped_query(query = self.query)
520
+ assert_kind_of 'query', query, Query, Hash
521
+
522
+ query.update(keys) if loaded?
523
+
524
+ return self.query if query == self.query
525
+
526
+ query = if query.kind_of?(Hash)
527
+ Query.new(query.has_key?(:repository) ? query.delete(:repository) : self.repository, model, query)
528
+ else
529
+ query
530
+ end
531
+
532
+ if query.limit || query.offset > 0
533
+ set_relative_position(query)
534
+ end
535
+
536
+ self.query.merge(query)
537
+ end
538
+
539
+ ##
540
+ # @api private
541
+ def keys
542
+ keys = map {|r| r.key }
543
+ keys.any? ? @key_properties.zip(keys.transpose).to_hash : {}
544
+ end
545
+
546
+ ##
547
+ # @api private
548
+ def identity_map
549
+ repository.identity_map(model)
550
+ end
551
+
552
+ ##
553
+ # @api private
554
+ def set_relative_position(query)
555
+ return if query == self.query
556
+
557
+ if query.offset == 0
558
+ return if !query.limit.nil? && !self.query.limit.nil? && query.limit <= self.query.limit
559
+ return if query.limit.nil? && self.query.limit.nil?
560
+ end
561
+
562
+ first_pos = self.query.offset + query.offset
563
+ last_pos = self.query.offset + self.query.limit if self.query.limit
564
+
565
+ if limit = query.limit
566
+ if last_pos.nil? || first_pos + limit < last_pos
567
+ last_pos = first_pos + limit
568
+ end
569
+ end
570
+
571
+ if last_pos && first_pos >= last_pos
572
+ raise 'outside range' # TODO: raise a proper exception object
573
+ end
574
+
575
+ query.update(:offset => first_pos)
576
+ query.update(:limit => last_pos - first_pos) if last_pos
577
+ end
578
+
579
+ ##
580
+ # @api private
581
+ def method_missing(method, *args, &block)
582
+ if relationship = relationships[method]
583
+ klass = model == relationship.child_model ? relationship.parent_model : relationship.child_model
584
+
585
+ # TODO: when self.query includes an offset/limit use it as a
586
+ # subquery to scope the results rather than a join
587
+
588
+ query = Query.new(repository, klass)
589
+ query.conditions.push(*self.query.conditions)
590
+ query.update(relationship.query)
591
+ query.update(args.pop) if args.last.kind_of?(Hash)
592
+
593
+ query.update(
594
+ :fields => klass.properties(repository.name).defaults,
595
+ :links => self.query.links + [ relationship ]
596
+ )
597
+
598
+ return klass.all(query, &block)
599
+ end
600
+
601
+ super
602
+ end
603
+ end # class Collection
604
+ end # module DataMapper