datamapper 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. data/CHANGELOG +5 -1
  2. data/FAQ +96 -0
  3. data/QUICKLINKS +12 -0
  4. data/README +57 -155
  5. data/environment.rb +61 -43
  6. data/example.rb +30 -12
  7. data/lib/data_mapper.rb +6 -1
  8. data/lib/data_mapper/adapters/abstract_adapter.rb +0 -57
  9. data/lib/data_mapper/adapters/data_object_adapter.rb +203 -97
  10. data/lib/data_mapper/adapters/mysql_adapter.rb +4 -0
  11. data/lib/data_mapper/adapters/postgresql_adapter.rb +7 -1
  12. data/lib/data_mapper/adapters/sql/coersion.rb +3 -2
  13. data/lib/data_mapper/adapters/sql/commands/load_command.rb +29 -10
  14. data/lib/data_mapper/adapters/sql/mappings/associations_set.rb +4 -0
  15. data/lib/data_mapper/adapters/sql/mappings/column.rb +13 -9
  16. data/lib/data_mapper/adapters/sql/mappings/conditions.rb +172 -0
  17. data/lib/data_mapper/adapters/sql/mappings/table.rb +43 -17
  18. data/lib/data_mapper/adapters/sqlite3_adapter.rb +9 -2
  19. data/lib/data_mapper/associations.rb +75 -3
  20. data/lib/data_mapper/associations/belongs_to_association.rb +70 -36
  21. data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +195 -86
  22. data/lib/data_mapper/associations/has_many_association.rb +168 -61
  23. data/lib/data_mapper/associations/has_n_association.rb +23 -3
  24. data/lib/data_mapper/attributes.rb +73 -0
  25. data/lib/data_mapper/auto_migrations.rb +2 -6
  26. data/lib/data_mapper/base.rb +5 -9
  27. data/lib/data_mapper/database.rb +4 -3
  28. data/lib/data_mapper/embedded_value.rb +66 -30
  29. data/lib/data_mapper/identity_map.rb +1 -3
  30. data/lib/data_mapper/is/tree.rb +121 -0
  31. data/lib/data_mapper/migration.rb +155 -0
  32. data/lib/data_mapper/persistence.rb +532 -218
  33. data/lib/data_mapper/property.rb +306 -0
  34. data/lib/data_mapper/query.rb +164 -0
  35. data/lib/data_mapper/support/blank.rb +2 -2
  36. data/lib/data_mapper/support/connection_pool.rb +5 -6
  37. data/lib/data_mapper/support/enumerable.rb +3 -3
  38. data/lib/data_mapper/support/errors.rb +10 -1
  39. data/lib/data_mapper/support/inflector.rb +174 -238
  40. data/lib/data_mapper/support/object.rb +54 -0
  41. data/lib/data_mapper/support/serialization.rb +19 -1
  42. data/lib/data_mapper/support/string.rb +7 -16
  43. data/lib/data_mapper/support/symbol.rb +3 -15
  44. data/lib/data_mapper/support/typed_set.rb +68 -0
  45. data/lib/data_mapper/types/base.rb +44 -0
  46. data/lib/data_mapper/types/string.rb +34 -0
  47. data/lib/data_mapper/validations/number_validator.rb +40 -0
  48. data/lib/data_mapper/validations/string_validator.rb +20 -0
  49. data/lib/data_mapper/validations/validator.rb +13 -0
  50. data/performance.rb +26 -1
  51. data/profile_data_mapper.rb +1 -1
  52. data/rakefile.rb +42 -2
  53. data/spec/acts_as_tree_spec.rb +11 -3
  54. data/spec/adapters/data_object_adapter_spec.rb +31 -0
  55. data/spec/associations/belongs_to_association_spec.rb +98 -0
  56. data/spec/associations/has_and_belongs_to_many_association_spec.rb +377 -0
  57. data/spec/associations/has_many_association_spec.rb +337 -0
  58. data/spec/attributes_spec.rb +23 -1
  59. data/spec/auto_migrations_spec.rb +86 -29
  60. data/spec/callbacks_spec.rb +107 -0
  61. data/spec/column_spec.rb +5 -2
  62. data/spec/count_command_spec.rb +33 -1
  63. data/spec/database_spec.rb +18 -0
  64. data/spec/dependency_spec.rb +4 -2
  65. data/spec/embedded_value_spec.rb +8 -8
  66. data/spec/fixtures/people.yaml +1 -1
  67. data/spec/fixtures/projects.yaml +10 -1
  68. data/spec/fixtures/tasks.yaml +6 -0
  69. data/spec/fixtures/tasks_tasks.yaml +2 -0
  70. data/spec/fixtures/tomatoes.yaml +1 -0
  71. data/spec/is_a_tree_spec.rb +149 -0
  72. data/spec/load_command_spec.rb +71 -9
  73. data/spec/magic_columns_spec.rb +17 -2
  74. data/spec/migration_spec.rb +267 -0
  75. data/spec/models/animal.rb +1 -1
  76. data/spec/models/candidate.rb +8 -0
  77. data/spec/models/career.rb +1 -1
  78. data/spec/models/chain.rb +8 -0
  79. data/spec/models/comment.rb +1 -1
  80. data/spec/models/exhibit.rb +1 -1
  81. data/spec/models/fence.rb +7 -0
  82. data/spec/models/fruit.rb +2 -2
  83. data/spec/models/job.rb +8 -0
  84. data/spec/models/person.rb +2 -3
  85. data/spec/models/post.rb +1 -1
  86. data/spec/models/project.rb +21 -1
  87. data/spec/models/section.rb +1 -1
  88. data/spec/models/serializer.rb +1 -1
  89. data/spec/models/task.rb +9 -0
  90. data/spec/models/tomato.rb +27 -0
  91. data/spec/models/user.rb +8 -2
  92. data/spec/models/zoo.rb +2 -7
  93. data/spec/paranoia_spec.rb +1 -1
  94. data/spec/{base_spec.rb → persistence_spec.rb} +207 -18
  95. data/spec/postgres_spec.rb +48 -6
  96. data/spec/property_spec.rb +90 -9
  97. data/spec/query_spec.rb +71 -5
  98. data/spec/save_command_spec.rb +11 -0
  99. data/spec/spec_helper.rb +14 -11
  100. data/spec/support/blank_spec.rb +8 -0
  101. data/spec/support/inflector_spec.rb +41 -0
  102. data/spec/support/object_spec.rb +9 -0
  103. data/spec/{serialization_spec.rb → support/serialization_spec.rb} +1 -1
  104. data/spec/support/silence_spec.rb +15 -0
  105. data/spec/{support_spec.rb → support/string_spec.rb} +3 -3
  106. data/spec/support/struct_spec.rb +12 -0
  107. data/spec/support/typed_set_spec.rb +66 -0
  108. data/spec/table_spec.rb +3 -3
  109. data/spec/types/string.rb +81 -0
  110. data/spec/validates_uniqueness_of_spec.rb +17 -0
  111. data/spec/validations/number_validator.rb +59 -0
  112. data/spec/validations/string_validator.rb +14 -0
  113. metadata +59 -17
  114. data/do_performance.rb +0 -153
  115. data/lib/data_mapper/support/active_record_impersonation.rb +0 -103
  116. data/lib/data_mapper/support/weak_hash.rb +0 -46
  117. data/spec/active_record_impersonation_spec.rb +0 -129
  118. data/spec/associations_spec.rb +0 -232
  119. data/spec/conditions_spec.rb +0 -49
  120. data/spec/has_many_association_spec.rb +0 -173
  121. data/spec/models/animals_exhibit.rb +0 -8
@@ -1,5 +1,5 @@
1
1
  require 'data_mapper/property'
2
- require 'data_mapper/support/active_record_impersonation'
2
+ require 'data_mapper/attributes'
3
3
  require 'data_mapper/support/serialization'
4
4
  require 'data_mapper/validations'
5
5
  require 'data_mapper/associations'
@@ -12,129 +12,471 @@ require 'data_mapper/support/struct'
12
12
  module DataMapper
13
13
  # See DataMapper::Persistence::ClassMethods for DataMapper's DSL documentation.
14
14
  module Persistence
15
+
16
+ class IncompleteModelDefinitionError < StandardError
17
+ end
18
+
15
19
  # This probably needs to be protected
16
20
  attr_accessor :loaded_set
17
-
21
+
22
+ include Comparable
23
+
18
24
  def self.included(klass)
25
+
19
26
  klass.extend(ClassMethods)
27
+ klass.extend(ConvenienceMethods::ClassMethods)
20
28
 
29
+ klass.send(:include, ConvenienceMethods::InstanceMethods)
30
+ klass.send(:include, Attributes)
21
31
  klass.send(:include, Associations)
22
32
  klass.send(:include, Validations)
23
33
  klass.send(:include, CallbacksHelper)
24
- klass.send(:include, Support::ActiveRecordImpersonation)
25
34
  klass.send(:include, Support::Serialization)
26
35
 
27
- prepare_for_persistence(klass)
28
- end
29
-
30
- def self.prepare_for_persistence(klass)
31
36
  klass.instance_variable_set('@properties', [])
32
-
37
+
33
38
  klass.send :extend, AutoMigrations
34
39
  klass.subclasses
35
40
  DataMapper::Persistence::subclasses << klass unless klass == DataMapper::Base
36
41
  klass.send(:undef_method, :id) if method_defined?(:id)
37
42
 
38
- return if klass == DataMapper::Base
39
-
40
43
  # When this class is sub-classed, copy the declared columns.
41
44
  klass.class_eval do
42
45
  def self.subclasses
43
- @subclasses || (@subclasses = [])
46
+ @subclasses || (@subclasses = Support::TypedSet.new(Class))
44
47
  end
45
-
48
+
46
49
  def self.inherited(subclass)
47
50
  super_table = database.table(self)
48
-
51
+
49
52
  if super_table.type_column.nil?
50
53
  super_table.add_column(:type, :class, {})
51
54
  end
52
-
55
+
56
+ subclass.instance_variable_set('@properties', self.instance_variable_get("@properties").dup)
53
57
  subclass.instance_variable_set("@callbacks", self.callbacks.dup)
54
-
58
+
55
59
  self::subclasses << subclass
56
60
  end
57
-
61
+
58
62
  def self.persistent?
59
63
  true
60
64
  end
61
65
  end
62
66
  end
63
67
 
68
+ # Migrates the database schema based on the properties defined within
69
+ # models. This includes removing fields no longer listed in models and
70
+ # adding new ones.
71
+ #
72
+ # This is destructive. Any data stored in the database will be destroyed
73
+ # when this method is called.
74
+ #
75
+ # ==== Returns
76
+ # True:: successfully automigrated database
77
+ # False:: an error occured when automigrating the database
78
+ #
79
+ # @public
64
80
  def self.auto_migrate!
65
81
  subclasses.each do |subclass|
66
82
  subclass.auto_migrate!
67
83
  end
68
84
  end
69
-
85
+
86
+
87
+ # Drops all tables known by the schema
88
+ #
89
+ # ==== Returns
90
+ # True:: successfully automigrated database
91
+ # False:: an error occured when automigrating the database
92
+ #
93
+ # @public
94
+ def self.drop_all_tables!
95
+ database.adapter.schema.each do |table|
96
+ table.drop!
97
+ end
98
+ end
99
+
70
100
  # Track classes that include this module.
101
+ # ==== Returns
102
+ # Support::TypedSet::
103
+ # contains classes that include or inherit from this module
104
+ #
105
+ # @semipublic
71
106
  def self.subclasses
72
- @subclasses || (@subclasses = [])
107
+ @subclasses || (@subclasses = Support::TypedSet.new(Class))
73
108
  end
74
-
109
+
110
+ # Track classes that include this module.
111
+ # ==== Returns
112
+ # Support::TypedSet::
113
+ # contains classes that include or inherit from this module
114
+ #
115
+ # @semipublic
75
116
  def self.dependencies
76
- @dependency_queue || (@dependency_queue = DependencyQueue.new)
117
+ @dependency_queue || (@dependency_queue = DependencyQueue.new)
77
118
  end
78
119
 
79
120
  def initialize(details = nil)
121
+ check_for_properties!
122
+ if details
123
+ initialize_with_attributes(details)
124
+ end
125
+ end
126
+
127
+ def initialize_with_attributes(details)
80
128
  case details
81
129
  when Hash then self.attributes = details
82
- when details.respond_to?(:persistent?) then self.unsafe_attributes = details.attributes
83
- when Struct then self.unsafe_attributes = details.attributes
84
- when NilClass then nil
130
+ when details.respond_to?(:persistent?) then self.private_attributes = details.attributes
131
+ when Struct then self.private_attributes = details.attributes
85
132
  end
133
+ end
134
+
135
+ def check_for_properties!
136
+ raise IncompleteModelDefinitionError.new("Models must have at least one property to be initialized.") if self.class.properties.empty?
86
137
  end
87
-
138
+
139
+ module ConvenienceMethods
140
+ module InstanceMethods
141
+
142
+ # Save updated properties to the database.
143
+ #
144
+ # ==== Returns
145
+ # True:: successfully saved the object to the database
146
+ # False:: an error occured when saving the object to the database.
147
+ # check valid? to see if validation error occured
148
+ #
149
+ # @public
150
+ def save
151
+ database_context.save(self)
152
+ end
153
+
154
+ # This behaves in the same way as save, but raises a ValidationError
155
+ # if the model is invalid. Successful saves return true.
156
+ #
157
+ # ==== Returns
158
+ # True:: successfully saved the object to the database
159
+ #
160
+ # ==== Raises
161
+ # ValidationError::
162
+ # The object could not be saved to the database due to validation
163
+ # errors
164
+ # @public
165
+ def save!
166
+ raise ValidationError.new(errors) unless save
167
+ return true
168
+ end
169
+
170
+ # Reloads a model's properties from the database. This also includes
171
+ # data for any associated models that have been loaded from the
172
+ # database.
173
+ #
174
+ # You can limit the properties being reloaded by passing in an array
175
+ # of symbols.
176
+ def reload!(cols = nil)
177
+ database_context.first(self.class, key, :select => ([self.class.table.key.to_sym] + (cols || original_values.keys)).uniq, :reload => true)
178
+ self.loaded_associations.each { |association| association.reload! }
179
+ self
180
+ end
181
+ alias reload reload!
182
+
183
+ # Deletes the model from the database and de-activates associations
184
+ def destroy!
185
+ database_context.destroy(self)
186
+ end
187
+ end
188
+
189
+ module ClassMethods
190
+
191
+ # Attempts to find an object using options passed as
192
+ # search_attributes, and falls back to creating the object if it
193
+ # can't find it.
194
+ #
195
+ # ==== Parameters
196
+ # search_attributes <hash>::
197
+ # attributes used to perform the search, and which can be later
198
+ # merged with create_attributes when creating a record
199
+ # create_attributes <hash>::
200
+ # attributes which are merged into the search_attributes when a
201
+ # record is unfound and needs to be created
202
+ #
203
+ # ==== Returns
204
+ # Object:: the found or created object from the database
205
+ #
206
+ # ==== Raises
207
+ # ValidationError::
208
+ # An object was not found, and could not be created due to errors
209
+ # in validation.
210
+ # DataObject::QueryError::
211
+ # The database threw an error
212
+ # -
213
+ # @public
214
+ def find_or_create(search_attributes, create_attributes = {})
215
+ first(search_attributes) || create(search_attributes.merge(create_attributes))
216
+ end
217
+
218
+ # returns an array of objects matching <tt>options</tt>.
219
+ #
220
+ # ==== Parameters
221
+ # options <hash>::
222
+ # hash of parameters to search by
223
+ #
224
+ # ==== Returns
225
+ # Array:: contains all matched objects from the database, or an
226
+ # empty set
227
+ #
228
+ # ==== Options
229
+ # Basics:
230
+ # Widget.all # => no conditions
231
+ # Widget.all :order => 'created_at desc' # => ORDER BY created_at desc
232
+ # Widget.all :limit => 10 # => LIMIT 10
233
+ # Widget.all :offset => 100 # => OFFSET 100
234
+ # Widget.all :include => [:gadgets] # => performs the JOIN according to
235
+ # its association with Gadgets
236
+ #
237
+ # Any non-standard options are assumed to be column names and are ANDed together:
238
+ # Widget.all :age => 10 # => WHERE age = 10
239
+ # Widget.all :age => 10, :title => 'Toy' # => WHERE age = 10 AND title = 'Toy'
240
+ #
241
+ # Using Symbol Operators[link:classes/DataMapper/Support/Symbol/Operator.html]:
242
+ # Widget.all :age.gt => 20 # => WHERE age > 10
243
+ # Widget.all :age.gte => 20, :name.like => '%Toy%' # => WHERE age >= 10 and name like '%Toy%'
244
+ #
245
+ # Variations of syntax include the :conditions => {} as well as interpolated arrays
246
+ # Widget.all :conditions => {:age => 10} # => WHERE age = 10
247
+ # Widget.all :conditions => ["age = ?", 10] # => WHERE age = 10
248
+ #
249
+ # Syntaxes can be mixed-and-matched as well
250
+ # Widget.all :conditions => ["age = ?", 10], :title => 'Toy'
251
+ # # => WHERE age = 10 AND title = 'Toy'
252
+ #
253
+ # ==== Raises
254
+ # DataMapper::Adapters::Sql::Commands::LoadCommand::ConditionsError::
255
+ # A query could not be constructed from the hash passed in as
256
+ # <tt>options</tt>
257
+ # DataObject::QueryError::
258
+ # The database threw an error
259
+ # -
260
+ # @public
261
+ def all(options = {})
262
+ database.all(self, options)
263
+ end
264
+
265
+ # Allows you to iterate over a collection of matching records. The
266
+ # first argument is the find options. The second is a block that will
267
+ # be called for every matching record.
268
+ #
269
+ # The valid options are the same as those documented in #all,
270
+ # except the <tt>:offset</tt> option, which is not allowed.
271
+ def each(options = {}, &b)
272
+ raise ArgumentError.new(":offset is not supported with the #each method") if options.has_key?(:offset)
273
+
274
+ offset = 0
275
+ limit = options[:limit] || (self::const_defined?('DEFAULT_LIMIT') ? self::DEFAULT_LIMIT : 500)
276
+
277
+ until (results = all(options.merge(:limit => limit, :offset => offset))).empty?
278
+ results.each(&b)
279
+ offset += limit
280
+ end
281
+ end
282
+
283
+ # Returns the first object which matches the query generated from the arguments
284
+ #
285
+ # ==== Parameters
286
+ # see all()
287
+ #
288
+ # ==== Returns
289
+ # Object:: first object from the database which matches the query
290
+ # nil:: no object could be found which matches the query
291
+ #
292
+ # ==== Raises
293
+ # DataMapper::Adapters::Sql::Commands::LoadCommand::ConditionsError::
294
+ # A query could not be generated from the arguments passed in
295
+ # DataObject::QueryError::
296
+ # The database threw an error
297
+ # -
298
+ # @public
299
+ def first(*args)
300
+ database.first(self, *args)
301
+ end
302
+
303
+ # returns the count of rows that match the given options hash. See
304
+ # all() for a list of possible arguments.
305
+ # NOTE: discards <tt>:offset</tt>, <tt>:limit</tt>, <tt>:order</tt>
306
+ #
307
+ # ==== Parameters
308
+ # see all().
309
+ #
310
+ # ==== Returns
311
+ # Integer:: number of rows matching query
312
+ #
313
+ # ==== Raises
314
+ # DataMapper::Adapters::Sql::Commands::LoadCommand::ConditionsError::
315
+ # A query could not be generated from the arguments passed in
316
+ # DataObject::QueryError::
317
+ # The database threw an error
318
+ # -
319
+ # @public
320
+ def count(*args)
321
+ database.count(self, *args)
322
+ end
323
+
324
+ # Does what it says. Deletes all records in a model's table.
325
+ # before_destroy and after_destroy callbacks are called and
326
+ # paranoia is respected.
327
+ #
328
+ # ==== Returns
329
+ # nil:: successfully deleted all rows
330
+ #
331
+ # ==== Raises
332
+ # DataObject::QueryError::
333
+ # The database threw an error
334
+ # -
335
+ # @public
336
+ def delete_all
337
+ database.delete_all(self)
338
+ end
339
+
340
+ def truncate!
341
+ database.truncate(self)
342
+ end
343
+
344
+ # This method allows for ActiveRecord style queries. The first
345
+ # argument is a symbol indicating a search for a single record or a
346
+ # collection — <tt>:first</tt> and <tt>:all</tt> respectively. The
347
+ # second argument is the hash of options for your query. For a list
348
+ # of valid options, please refer to the #all method.
349
+ #
350
+ # Widget.find(:all, :active => true) # => An array of active widgets
351
+ # Widget.find(:first, :active => true) # => The first active widget found
352
+ def find(type_or_id, options = {})
353
+ case type_or_id
354
+ when :first then first(options)
355
+ when :all then all(options)
356
+ else first(type_or_id, options)
357
+ end
358
+ end
359
+
360
+ # supply this method with the full SQL you wish to search on, and it
361
+ # will return an array of Structs with your results set in them.
362
+ #
363
+ # NOTE: this does NOT return objects of a specific type, but rather
364
+ # Struct objects with as many attributes as what you requested in
365
+ # your full SQL query. These structs are read-only.
366
+ #
367
+ # If you only indicate you want 1 specific column, Datamapper and
368
+ # DataObjects will do their best to type-cast the result as best they
369
+ # can, rather than supplying you with an array of length 1 containing
370
+ # Structs with 1 attribute.
371
+ def find_by_sql(*args)
372
+ DataMapper::database.query(*args)
373
+ end
374
+
375
+ # finds a single row from the database by it's primary key.
376
+ # If you declared a property with <tt>:key => true</tt>, it's safe to
377
+ # use here.
378
+ # Example:
379
+ # Widget.get(100) # => widget with the primary key of 100
380
+ # Widget.get('Toy') # => widget with the primary natural key of 'Toy'
381
+ def get(*keys)
382
+ database.get(self, keys)
383
+ end
384
+
385
+
386
+ # synonym for get()
387
+ # ==== Parameters
388
+ # keys <any>:: keys which which to look up objects in the table.
389
+ #
390
+ # ==== Returns
391
+ # object :: object matching the request
392
+ #
393
+ # ==== Raises
394
+ # DataMapper::ObjectNotFoundError
395
+ # could not find the object requested
396
+ # -
397
+ # @public
398
+ def [](*keys)
399
+ # Eventually this ArgumentError should be removed. It's only here
400
+ # to help
401
+ # migrate users away from the [options_hash] syntax, which is no
402
+ # longer supported.
403
+ raise ArgumentError.new('Hash is not a valid key') if keys.size == 1 && keys.first.is_a?(Hash)
404
+ instance = database.get(self, keys)
405
+ raise ObjectNotFoundError.new() unless instance
406
+ return instance
407
+ end
408
+
409
+ # creates (and saves) a new instance of the object.
410
+ def create(attributes)
411
+ instance = self.new_with_attributes(attributes)
412
+ instance.save
413
+ instance
414
+ end
415
+
416
+ # the same as create(), though will raise an ObjectNotFoundError if
417
+ # the instance could not be saved
418
+ def create!(attributes)
419
+ instance = create(attributes)
420
+ raise ObjectNotFoundError.new(instance) if instance.new_record?
421
+ instance
422
+ end
423
+ end
424
+ end
425
+
88
426
  module ClassMethods
427
+
428
+ def new_with_attributes(details)
429
+ instance = allocate
430
+ instance.initialize_with_attributes(details)
431
+ instance
432
+ end
89
433
 
90
434
  # Track classes that include this module.
91
435
  def subclasses
92
436
  @subclasses || (@subclasses = [])
93
437
  end
94
-
438
+
95
439
  def logger
96
440
  database.logger
97
441
  end
98
-
442
+
99
443
  def transaction
100
444
  yield
101
445
  end
102
-
446
+
447
+ # The foreign key for a model. It is based on the lowercased and
448
+ # underscored name of the class, suffixed with <tt>_id</tt>.
449
+ #
450
+ # Widget.foreign_key # => "widget_id"
451
+ # NewsItem.foreign_key # => "news_item_id"
103
452
  def foreign_key
104
453
  Inflector.underscore(self.name) + "_id"
105
454
  end
106
455
 
107
456
  def extended(klass)
108
- unless klass == DataMapper::Base
109
- klass.class_eval do
110
- def persistent?
111
- true
112
- end
113
- end
114
- end
115
457
  end
116
-
458
+
117
459
  def table
118
460
  database.table(self)
119
461
  end
120
-
121
- # NOTE: check is only for psql, so maybe the postgres adapter should define
122
- # its own property options. currently it will produce a warning tho since
123
- # PROPERTY_OPTIONS is a constant
124
- PROPERTY_OPTIONS = [
125
- :public, :protected, :private, :accessor, :reader, :writer,
126
- :lazy, :default, :nullable, :key, :serial, :column, :size, :length,
127
- :index, :check
128
- ]
129
-
130
- # Adds property accessors for a field that you'd like to be able to modify. The DataMapper doesn't
131
- # use the table schema to infer accessors, you must explicity call #property to add field accessors
132
- # to your model.
462
+
463
+ # Adds property accessors for a field that you'd like to be able to
464
+ # modify. The DataMapper doesn't
465
+ # use the table schema to infer accessors, you must explicity call
466
+ # #property to add field accessors
467
+ # to your model.
468
+ #
469
+ # Can accept an unlimited amount of property names. Optionally, you may
470
+ # pass the property names as an
471
+ # array.
472
+ #
473
+ # For more documentation, see Property.
133
474
  #
134
475
  # EXAMPLE:
135
476
  # class CellProvider
136
477
  # property :name, :string
137
- # property :rating, :integer
478
+ # property :rating_number, :rating_percent, :integer # will create two properties with same type and text
479
+ # property [:bill_to, :ship_to, :mail_to], :text, :lazy => false # will create three properties all with same type and text
138
480
  # end
139
481
  #
140
482
  # att = CellProvider.new(:name => 'AT&T')
@@ -146,46 +488,33 @@ module DataMapper
146
488
  #
147
489
  # OPTIONS:
148
490
  # * <tt>lazy</tt>: Lazy load the specified property (:lazy => true). False by default.
149
- # * <tt>accessor</tt>: Set method visibility for the property accessors. Affects both
150
- # reader and writer. Allowable values are :public, :protected, :private. Defaults to
491
+ # * <tt>accessor</tt>: Set method visibility for the property accessors. Affects both
492
+ # reader and writer. Allowable values are :public, :protected, :private. Defaults to
151
493
  # :public
152
494
  # * <tt>reader</tt>: Like the accessor option but affects only the property reader.
153
495
  # * <tt>writer</tt>: Like the accessor option but affects only the property writer.
154
496
  # * <tt>protected</tt>: Alias for :reader => :public, :writer => :protected
155
497
  # * <tt>private</tt>: Alias for :reader => :public, :writer => :private
156
- def property(name, type, options = {})
157
498
 
158
- options.each_pair do |k,v|
159
- raise ArgumentError.new("#{k.inspect} is not a supported option in DataMapper::Base::PROPERTY_OPTIONS") unless PROPERTY_OPTIONS.include?(k)
160
- end
499
+ def property(*columns_and_options)
500
+ columns, options = columns_and_options.partition {|item| not item.is_a?(Hash)}
501
+ options = (options.empty? ? {} : options[0])
502
+ type = columns.pop
161
503
 
162
- visibility_options = [:public, :protected, :private]
163
- reader_visibility = options[:reader] || options[:accessor] || :public
164
- writer_visibility = options[:writer] || options[:accessor] || :public
165
- writer_visibility = :protected if options[:protected]
166
- writer_visibility = :private if options[:private]
167
-
168
- raise(ArgumentError.new, "property visibility must be :public, :protected, or :private") unless visibility_options.include?(reader_visibility) && visibility_options.include?(writer_visibility)
169
-
170
- mapping = database.schema[self].add_column(name.to_s.sub(/\?$/, '').to_sym, type, options)
171
-
172
- property_getter(mapping, reader_visibility)
173
- property_setter(mapping, writer_visibility)
504
+ @properties ||= []
505
+ new_properties = []
174
506
 
175
- if MAGIC_PROPERTIES.has_key?(name)
176
- class_eval(&MAGIC_PROPERTIES[name])
507
+ columns.flatten.each do |name|
508
+ property = DataMapper::Property.new(self, name, type, options)
509
+ new_properties << property
510
+ @properties << property
177
511
  end
178
-
179
- return name
512
+
513
+ return (new_properties.length == 1 ? new_properties[0] : new_properties)
180
514
  end
181
-
182
- MAGIC_PROPERTIES = {
183
- :updated_at => lambda { before_save { |x| x.updated_at = Time::now } },
184
- :updated_on => lambda { before_save { |x| x.updated_on = Date::today } },
185
- :created_at => lambda { before_create { |x| x.created_at = Time::now } },
186
- :created_on => lambda { before_create { |x| x.created_on = Date::today } }
187
- }
188
-
515
+
516
+ # TODO: Figure out how to make EmbeddedValue work with new property
517
+ # code. EV relies on these next two methods.
189
518
  def property_getter(mapping, visibility = :public)
190
519
  if mapping.lazy?
191
520
  class_eval <<-EOS
@@ -201,15 +530,15 @@ module DataMapper
201
530
  else
202
531
  class_eval("#{visibility.to_s}; def #{mapping.name}; #{mapping.instance_variable_name} end") unless [ :public, :private, :protected ].include?(mapping.name)
203
532
  end
204
-
533
+
205
534
  if mapping.type == :boolean
206
535
  class_eval("#{visibility.to_s}; def #{mapping.name.to_s.ensure_ends_with('?')}; #{mapping.instance_variable_name} end")
207
536
  end
208
-
537
+
209
538
  rescue SyntaxError
210
539
  raise SyntaxError.new(mapping)
211
540
  end
212
-
541
+
213
542
  def property_setter(mapping, visibility = :public)
214
543
  if mapping.lazy?
215
544
  class_eval <<-EOS
@@ -227,7 +556,7 @@ module DataMapper
227
556
  rescue SyntaxError
228
557
  raise SyntaxError.new(mapping)
229
558
  end
230
-
559
+
231
560
  # Allows you to override the table name for a model.
232
561
  # EXAMPLE:
233
562
  # class WorkItem
@@ -236,10 +565,13 @@ module DataMapper
236
565
  def set_table_name(value)
237
566
  database.table(self).name = value
238
567
  end
239
- # An embedded value maps the values of an object to fields in the record of the object's owner.
240
- # #embed takes a symbol to define the embedded class, options, and an optional block. See
568
+
569
+ # An embedded value maps the values of an object to fields in the
570
+ # record of the object's owner.
571
+ # #embed takes a symbol to define the embedded class, options, and
572
+ # an optional block. See
241
573
  # examples for use cases.
242
- #
574
+ #
243
575
  # EXAMPLE:
244
576
  # class CellPhone < DataMapper::Base
245
577
  # property :number, :string
@@ -257,30 +589,42 @@ module DataMapper
257
589
  # => Nick
258
590
  #
259
591
  # OPTIONS:
260
- # * <tt>prefix</tt>: define a column prefix, so instead of mapping :address to an 'address'
261
- # column, it would map to 'owner_address' in the example above. If :prefix => true is
262
- # specified, the prefix will be the name of the symbol given as the first parameter. If the
263
- # prefix is a string the specified string will be used for the prefix.
264
- # * <tt>lazy</tt>: lazy-load all embedded values at the same time. :lazy => true to enable.
265
- # Disabled (false) by default.
266
- # * <tt>accessor</tt>: Set method visibility for all embedded properties. Affects both
267
- # reader and writer. Allowable values are :public, :protected, :private. Defaults to :public
268
- # * <tt>reader</tt>: Like the accessor option but affects only embedded property readers.
269
- # * <tt>writer</tt>: Like the accessor option but affects only embedded property writers.
270
- # * <tt>protected</tt>: Alias for :reader => :public, :writer => :protected
271
- # * <tt>private</tt>: Alias for :reader => :public, :writer => :private
592
+ # * <tt>prefix</tt>: define a column prefix, so instead of mapping
593
+ # :address to an 'address' column, it would map to
594
+ # 'owner_address' in the example above. If
595
+ # :prefix => true is specified, the prefix will
596
+ # be the name of the symbol given as the first
597
+ # parameter. If the prefix is a string the
598
+ # specified
599
+ # string will be used for the prefix.
600
+ # * <tt>lazy</tt>: lazy-load all embedded values at the same time.
601
+ # :lazy => true to enable. Disabled (false) by
602
+ # default.
603
+ # * <tt>accessor</tt>: Set method visibility for all embedded
604
+ # properties. Affects both reader and writer.
605
+ # Allowable values are :public, :protected,
606
+ # :private. Defaults to :public
607
+ # * <tt>reader</tt>: Like the accessor option but affects only
608
+ # embedded property readers.
609
+ # * <tt>writer</tt>: Like the accessor option but affects only
610
+ # embedded property writers.
611
+ # * <tt>protected</tt>: Alias for :reader => :public,
612
+ # :writer => :protected
613
+ # * <tt>private</tt>: Alias for :reader => :public, :writer => :private
272
614
  #
273
615
  def embed(name, options = {}, &block)
274
616
  EmbeddedValue::define(self, name, options, &block)
275
- end
276
-
617
+ end
618
+
619
+ # Returns the hash of properties for this model.
277
620
  def properties
278
621
  @properties
279
622
  end
280
623
 
281
- # Creates a composite index for an arbitrary number of database columns. Note that
282
- # it also is possible to specify single indexes directly for each property.
283
- #
624
+ # Creates a composite index for an arbitrary number of database columns.
625
+ # Note that it also is possible to specify single indexes directly for
626
+ # each property.
627
+ #
284
628
  # === EXAMPLE WITH COMPOSITE INDEX:
285
629
  # class Person < DataMapper::Base
286
630
  # property :server_id, :integer
@@ -301,21 +645,23 @@ module DataMapper
301
645
  # * property :name, :index => true
302
646
  # * property :name, :index => :unique
303
647
  def index(indexes, unique = false)
304
- if indexes.kind_of?(Array) # if given an index of multiple columns
648
+ if indexes.kind_of?(Array) # if given an index of multiple columns
305
649
  database.schema[self].add_composite_index(indexes, unique)
306
650
  else
307
651
  raise ArgumentError.new("You must supply an array for the composite index")
308
652
  end
309
653
  end
310
-
654
+
311
655
  end
312
-
313
- # Lazy-loads the attributes for a loaded_set, then overwrites the accessors
314
- # for the named methods so that the lazy_loading is skipped the second time.
656
+
657
+ # Lazy-loads the attributes for a loaded_set, then overwrites the
658
+ # accessors
659
+ # for the named methods so that the lazy_loading is skipped the second
660
+ # time.
315
661
  def lazy_load!(*names)
316
-
662
+
317
663
  names = names.map { |name| name.to_sym }.reject { |name| lazy_loaded_attributes.include?(name) }
318
-
664
+
319
665
  reset_attribute = lambda do |instance|
320
666
  singleton_class = (class << instance; self end)
321
667
  names.each do |name|
@@ -323,14 +669,14 @@ module DataMapper
323
669
  singleton_class.send(:attr_accessor, name)
324
670
  end
325
671
  end
326
-
672
+
327
673
  unless names.empty? || new_record? || loaded_set.nil?
328
-
674
+
329
675
  key = database_context.table(self.class).key.to_sym
330
676
  keys_to_select = loaded_set.map do |instance|
331
677
  instance.send(key)
332
678
  end
333
-
679
+
334
680
  database_context.all(
335
681
  self.class,
336
682
  :select => ([key] + names),
@@ -342,25 +688,10 @@ module DataMapper
342
688
  end
343
689
  end
344
690
 
345
- # Mass-assign mapped fields.
346
- def attributes=(values_hash)
347
- table = database_context.table(self.class)
348
-
349
- values_hash.delete_if do |key, value|
350
- !self.class.public_method_defined?("#{key}=")
351
- end.each_pair do |key, value|
352
- if respond_to?(key)
353
- send("#{key}=", value)
354
- elsif column = table[key]
355
- instance_variable_set(column.instance_variable_name, value)
356
- end
357
- end
358
- end
359
-
360
691
  def database_context
361
692
  @database_context || ( @database_context = database )
362
693
  end
363
-
694
+
364
695
  def database_context=(value)
365
696
  @database_context = value
366
697
  end
@@ -368,77 +699,32 @@ module DataMapper
368
699
  def logger
369
700
  self.class.logger
370
701
  end
371
-
702
+
703
+ # Returns <tt>true</tt> if this model hasn't been saved to the
704
+ # database, <tt>false</tt> otherwise.
372
705
  def new_record?
373
706
  @new_record.nil? || @new_record
374
707
  end
375
708
 
376
- def ==(other)
377
- other.is_a?(self.class) && private_attributes == other.send(:private_attributes)
378
- end
379
-
380
- # Returns the difference between two objects, in terms of their attributes.
381
- def ^(other)
382
- results = {}
383
-
384
- self_attributes, other_attributes = attributes, other.attributes
385
-
386
- self_attributes.each_pair do |k,v|
387
- other_value = other_attributes[k]
388
- unless v == other_value
389
- results[k] = [v, other_value]
390
- end
391
- end
392
-
393
- results
394
- end
395
-
709
+ # Returns a Set containing the properties that have had their
710
+ # <tt>:lazy</tt> option set to true, or are lazily loaded by
711
+ # default — i.e. text fields.
396
712
  def lazy_loaded_attributes
397
713
  @lazy_loaded_attributes || @lazy_loaded_attributes = Set.new
398
714
  end
399
-
400
- def loaded_attributes
401
- pairs = {}
402
-
403
- database_context.table(self).columns.each do |column|
404
- pairs[column.name] = instance_variable_get(column.instance_variable_name)
405
- end
406
-
407
- pairs
408
- end
409
-
715
+
716
+ # Accepts a hash of properties and values to be updated and then calls #save
410
717
  def update_attributes(update_hash)
411
718
  self.attributes = update_hash
412
719
  self.save
413
720
  end
414
-
415
- def attributes
416
- pairs = {}
417
-
418
- database_context.table(self).columns.each do |column|
419
- if self.class.public_method_defined?(column.name)
420
- lazy_load!(column.name) if column.lazy?
421
- value = instance_variable_get(column.instance_variable_name)
422
- pairs[column.name] = column.type == :class ? value.to_s : value
423
- end
424
- end
425
-
426
- pairs
427
- end
428
-
429
- def unsafe_attributes=(values_hash)
430
- table = database_context.table(self.class)
431
-
432
- values_hash.each_pair do |key, value|
433
- if respond_to?(key)
434
- send("#{key}=", value)
435
- elsif column = table[key]
436
- instance_variable_set(column.instance_variable_name, value)
437
- end
438
- end
439
- end
440
-
441
- def dirty?
721
+
722
+ # Returns <tt>true</tt> if the unsaved model has had properties changed
723
+ # since it was loaded from the database. Returns <tt>false</tt> otherwise.
724
+ def dirty?(cleared = Set.new)
725
+ return false if cleared.include?(self)
726
+ cleared << self
727
+
442
728
  result = database_context.table(self).columns.any? do |column|
443
729
  if column.type == :object
444
730
  Marshal.dump(self.instance_variable_get(column.instance_variable_name)) != original_values[column.name]
@@ -446,72 +732,74 @@ module DataMapper
446
732
  self.instance_variable_get(column.instance_variable_name) != original_values[column.name]
447
733
  end
448
734
  end
449
-
735
+
450
736
  return true if result
451
-
737
+
452
738
  loaded_associations.any? do |loaded_association|
453
- loaded_association.dirty?
739
+ loaded_association.dirty?(cleared)
454
740
  end
455
741
  end
456
742
 
743
+ # For unsaved models, returns a hash of properties that have had their
744
+ # values changed since it was loaded from the database.
457
745
  def dirty_attributes
458
746
  pairs = {}
459
-
747
+
460
748
  database_context.table(self).columns.each do |column|
461
749
  value = instance_variable_get(column.instance_variable_name)
462
750
  if value != original_values[column.name] && (!new_record? || !column.serial?)
463
751
  pairs[column.name] = column.type != :object ? value : YAML.dump(value)
464
752
  end
465
753
  end
466
-
754
+
467
755
  pairs
468
756
  end
469
-
757
+
470
758
  def original_values=(values)
471
759
  values.each_pair do |k,v|
472
760
  original_values[k] = case v
473
- when String, Date, Time then v.dup
474
- # when column.type == :object then Marshal.dump(v)
475
- else v
761
+ when String, Date, Time then v.dup
762
+ # when column.type == :object then Marshal.dump(v)
763
+ else v
476
764
  end
477
765
  end
478
766
  end
479
-
767
+
480
768
  def original_values
481
769
  class << self
482
770
  attr_reader :original_values
483
771
  end
484
-
772
+
485
773
  @original_values = {}
486
774
  end
487
-
775
+
488
776
  def loaded_set=(value)
489
777
  value << self
490
778
  @loaded_set = value
491
779
  end
492
-
780
+
493
781
  def inspect
494
782
  inspected_attributes = attributes.map { |k,v| "@#{k}=#{v.inspect}" }
495
-
783
+
496
784
  instance_variables.each do |name|
497
785
  if instance_variable_get(name).kind_of?(Associations::HasManyAssociation)
498
786
  inspected_attributes << "#{name}=#{instance_variable_get(name).inspect}"
499
787
  end
500
788
  end
501
-
789
+
502
790
  "#<%s:0x%x @new_record=%s, %s>" % [self.class.name, (object_id * 2), new_record?, inspected_attributes.join(', ')]
503
791
  end
504
-
792
+
505
793
  def loaded_associations
506
794
  @loaded_associations || @loaded_associations = []
507
795
  end
508
-
796
+
509
797
  def key=(value)
510
798
  key_column = database_context.table(self.class).key
511
799
  @__key = key_column.type_cast_value(value)
512
800
  instance_variable_set(key_column.instance_variable_name, @__key)
513
801
  end
514
-
802
+
515
803
  def key
516
804
  @__key || @__key = begin
517
805
  key_column = database_context.table(self.class).key
@@ -519,20 +807,46 @@ module DataMapper
519
807
  end
520
808
  end
521
809
 
522
- private
810
+ def keys
811
+ self.class.table.keys.map do |column|
812
+ column.type_cast_value(instance_variable_get(column.instance_variable_name))
813
+ end.compact
814
+ end
523
815
 
524
- # return all attributes, regardless of their visibility
525
- def private_attributes
526
- pairs = {}
816
+ def <=>(other)
817
+ keys <=> other.keys
818
+ end
527
819
 
528
- database_context.table(self).columns.each do |column|
529
- lazy_load!(column.name) if column.lazy?
530
- value = instance_variable_get(column.instance_variable_name)
531
- pairs[column.name] = column.type == :class ? value.to_s : value
820
+ # Look to ::included for __hash alias
821
+ def hash
822
+ @__hash || @__hash = keys.empty? ? super : keys.hash
823
+ end
824
+
825
+ def eql?(other)
826
+ return false unless other.is_a?(self.class) || self.is_a?(other.class)
827
+ comparator = keys.empty? ? :private_attributes : :keys
828
+ send(comparator) == other.send(comparator)
829
+ end
830
+
831
+ def ==(other)
832
+ eql?(other)
833
+ end
834
+
835
+ # Returns the difference between two objects, in terms of their
836
+ # attributes.
837
+ def ^(other)
838
+ results = {}
839
+
840
+ self_attributes, other_attributes = attributes, other.attributes
841
+
842
+ self_attributes.each_pair do |k,v|
843
+ other_value = other_attributes[k]
844
+ unless v == other_value
845
+ results[k] = [v, other_value]
846
+ end
532
847
  end
533
848
 
534
- pairs
849
+ results
535
850
  end
536
-
537
851
  end
538
- end
852
+ end