datamapper 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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