bigrecord 0.0.5

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 (104) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +44 -0
  3. data/Rakefile +17 -0
  4. data/VERSION +1 -0
  5. data/doc/bigrecord_specs.rdoc +36 -0
  6. data/doc/getting_started.rdoc +157 -0
  7. data/examples/bigrecord.yml +25 -0
  8. data/generators/bigrecord/bigrecord_generator.rb +17 -0
  9. data/generators/bigrecord/templates/bigrecord.rake +47 -0
  10. data/generators/bigrecord_migration/bigrecord_migration_generator.rb +13 -0
  11. data/generators/bigrecord_migration/templates/migration.rb +9 -0
  12. data/generators/bigrecord_model/bigrecord_model_generator.rb +28 -0
  13. data/generators/bigrecord_model/templates/migration.rb +13 -0
  14. data/generators/bigrecord_model/templates/model.rb +7 -0
  15. data/generators/bigrecord_model/templates/model_spec.rb +12 -0
  16. data/init.rb +9 -0
  17. data/install.rb +22 -0
  18. data/lib/big_record/abstract_base.rb +1088 -0
  19. data/lib/big_record/action_view_extensions.rb +266 -0
  20. data/lib/big_record/ar_associations/association_collection.rb +194 -0
  21. data/lib/big_record/ar_associations/association_proxy.rb +158 -0
  22. data/lib/big_record/ar_associations/belongs_to_association.rb +57 -0
  23. data/lib/big_record/ar_associations/belongs_to_many_association.rb +57 -0
  24. data/lib/big_record/ar_associations/has_and_belongs_to_many_association.rb +164 -0
  25. data/lib/big_record/ar_associations/has_many_association.rb +191 -0
  26. data/lib/big_record/ar_associations/has_one_association.rb +80 -0
  27. data/lib/big_record/ar_associations.rb +1608 -0
  28. data/lib/big_record/ar_reflection.rb +223 -0
  29. data/lib/big_record/attribute_methods.rb +75 -0
  30. data/lib/big_record/base.rb +618 -0
  31. data/lib/big_record/br_associations/association_collection.rb +194 -0
  32. data/lib/big_record/br_associations/association_proxy.rb +153 -0
  33. data/lib/big_record/br_associations/belongs_to_association.rb +52 -0
  34. data/lib/big_record/br_associations/belongs_to_many_association.rb +293 -0
  35. data/lib/big_record/br_associations/cached_item_proxy.rb +194 -0
  36. data/lib/big_record/br_associations/cached_item_proxy_factory.rb +62 -0
  37. data/lib/big_record/br_associations/has_and_belongs_to_many_association.rb +168 -0
  38. data/lib/big_record/br_associations/has_one_association.rb +80 -0
  39. data/lib/big_record/br_associations.rb +978 -0
  40. data/lib/big_record/br_reflection.rb +151 -0
  41. data/lib/big_record/callbacks.rb +367 -0
  42. data/lib/big_record/connection_adapters/abstract/connection_specification.rb +279 -0
  43. data/lib/big_record/connection_adapters/abstract/database_statements.rb +175 -0
  44. data/lib/big_record/connection_adapters/abstract/quoting.rb +58 -0
  45. data/lib/big_record/connection_adapters/abstract_adapter.rb +190 -0
  46. data/lib/big_record/connection_adapters/column.rb +491 -0
  47. data/lib/big_record/connection_adapters/hbase_adapter.rb +432 -0
  48. data/lib/big_record/connection_adapters/view.rb +27 -0
  49. data/lib/big_record/connection_adapters.rb +10 -0
  50. data/lib/big_record/deletion.rb +73 -0
  51. data/lib/big_record/dynamic_schema.rb +92 -0
  52. data/lib/big_record/embedded.rb +71 -0
  53. data/lib/big_record/embedded_associations/association_proxy.rb +148 -0
  54. data/lib/big_record/family_span_columns.rb +89 -0
  55. data/lib/big_record/fixtures.rb +1025 -0
  56. data/lib/big_record/migration.rb +380 -0
  57. data/lib/big_record/routing_ext.rb +65 -0
  58. data/lib/big_record/timestamp.rb +51 -0
  59. data/lib/big_record/validations.rb +830 -0
  60. data/lib/big_record.rb +125 -0
  61. data/lib/bigrecord.rb +1 -0
  62. data/rails/init.rb +9 -0
  63. data/spec/connections/bigrecord.yml +13 -0
  64. data/spec/connections/cassandra/connection.rb +2 -0
  65. data/spec/connections/hbase/connection.rb +2 -0
  66. data/spec/debug.log +281 -0
  67. data/spec/integration/br_associations_spec.rb +80 -0
  68. data/spec/lib/animal.rb +12 -0
  69. data/spec/lib/book.rb +10 -0
  70. data/spec/lib/broken_migrations/duplicate_name/20090706182535_add_animals_table.rb +14 -0
  71. data/spec/lib/broken_migrations/duplicate_name/20090706193019_add_animals_table.rb +9 -0
  72. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_books_table.rb +9 -0
  73. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_companies_table.rb +9 -0
  74. data/spec/lib/company.rb +14 -0
  75. data/spec/lib/embedded/web_link.rb +12 -0
  76. data/spec/lib/employee.rb +33 -0
  77. data/spec/lib/migrations/20090706182535_add_animals_table.rb +13 -0
  78. data/spec/lib/migrations/20090706190623_add_books_table.rb +15 -0
  79. data/spec/lib/migrations/20090706193019_add_companies_table.rb +14 -0
  80. data/spec/lib/migrations/20090706194512_add_employees_table.rb +13 -0
  81. data/spec/lib/migrations/20090706195741_add_zoos_table.rb +13 -0
  82. data/spec/lib/novel.rb +5 -0
  83. data/spec/lib/zoo.rb +17 -0
  84. data/spec/spec.opts +4 -0
  85. data/spec/spec_helper.rb +55 -0
  86. data/spec/unit/abstract_base_spec.rb +287 -0
  87. data/spec/unit/adapters/abstract_adapter_spec.rb +56 -0
  88. data/spec/unit/adapters/adapter_shared_spec.rb +51 -0
  89. data/spec/unit/adapters/hbase_adapter_spec.rb +15 -0
  90. data/spec/unit/ar_associations_spec.rb +8 -0
  91. data/spec/unit/base_spec.rb +6 -0
  92. data/spec/unit/br_associations_spec.rb +58 -0
  93. data/spec/unit/embedded_spec.rb +43 -0
  94. data/spec/unit/find_spec.rb +34 -0
  95. data/spec/unit/hash_helper_spec.rb +44 -0
  96. data/spec/unit/migration_spec.rb +144 -0
  97. data/spec/unit/model_spec.rb +315 -0
  98. data/spec/unit/validations_spec.rb +182 -0
  99. data/tasks/bigrecord_tasks.rake +47 -0
  100. data/tasks/data_store.rb +46 -0
  101. data/tasks/gem.rb +22 -0
  102. data/tasks/rdoc.rb +8 -0
  103. data/tasks/spec.rb +34 -0
  104. metadata +189 -0
@@ -0,0 +1,1088 @@
1
+ ## The sub-classes of this class must implement the abstract method 'column_names' of this class.
2
+ module BigRecord
3
+ class BigRecordError < StandardError #:nodoc:
4
+ end
5
+ class SubclassNotFound < BigRecordError #:nodoc:
6
+ end
7
+ class AssociationTypeMismatch < BigRecordError #:nodoc:
8
+ end
9
+ class WrongAttributeDataType < BigRecordError #:nodoc:
10
+ end
11
+ class AttributeMissing < BigRecordError #:nodoc:
12
+ end
13
+ class UnknownAttribute < BigRecordError #:nodoc:
14
+ end
15
+ class AdapterNotSpecified < BigRecordError # :nodoc:
16
+ end
17
+ class AdapterNotFound < BigRecordError # :nodoc:
18
+ end
19
+ class ConnectionNotEstablished < BigRecordError #:nodoc:
20
+ end
21
+ class ConnectionFailed < BigRecordError #:nodoc:
22
+ end
23
+ class RecordNotFound < BigRecordError #:nodoc:
24
+ end
25
+ class RecordNotSaved < BigRecordError #:nodoc:
26
+ end
27
+ class StatementInvalid < BigRecordError #:nodoc:
28
+ end
29
+ class PreparedStatementInvalid < BigRecordError #:nodoc:
30
+ end
31
+ class StaleObjectError < BigRecordError #:nodoc:
32
+ end
33
+ class ConfigurationError < StandardError #:nodoc:
34
+ end
35
+ class ReadOnlyRecord < StandardError #:nodoc:
36
+ end
37
+ class NotImplemented < BigRecordError #:nodoc:
38
+ end
39
+ class ColumnNotFound < BigRecordError #:nodoc:
40
+ end
41
+ class AttributeAssignmentError < BigRecordError #:nodoc:
42
+ attr_reader :exception, :attribute
43
+ def initialize(message, exception, attribute)
44
+ @exception = exception
45
+ @attribute = attribute
46
+ @message = message
47
+ end
48
+ end
49
+ class MultiparameterAssignmentErrors < BigRecordError #:nodoc:
50
+ attr_reader :errors
51
+ def initialize(errors)
52
+ @errors = errors
53
+ end
54
+ end
55
+
56
+ class AbstractBase
57
+ require 'rubygems'
58
+ require 'uuidtools'
59
+
60
+ # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
61
+ # on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
62
+ cattr_accessor :logger, :instance_writer => false
63
+
64
+ # Constants for special characters in generated IDs. An ID might then look
65
+ # like this: 'United_States-Hawaii-Oahu-Honolulu-b9cef848-a4e0-11dc-a7ba-0018f3137ea8'
66
+ ID_FIELD_SEPARATOR = '-'
67
+ ID_WHITE_SPACE_CHAR = '_'
68
+
69
+ def self.inherited(child) #:nodoc:
70
+ @@subclasses[self] ||= []
71
+ @@subclasses[self] << child
72
+ child.set_table_name child.name.tableize if child.superclass == BigRecord::Base
73
+ super
74
+ end
75
+
76
+ def self.reset_subclasses #:nodoc:
77
+ nonreloadables = []
78
+ subclasses.each do |klass|
79
+ unless Dependencies.autoloaded? klass
80
+ nonreloadables << klass
81
+ next
82
+ end
83
+ klass.instance_variables.each { |var| klass.send(:remove_instance_variable, var) }
84
+ klass.instance_methods(false).each { |m| klass.send :undef_method, m }
85
+ end
86
+ @@subclasses = {}
87
+ nonreloadables.each { |klass| (@@subclasses[klass.superclass] ||= []) << klass }
88
+ end
89
+
90
+ @@subclasses = {}
91
+
92
+ def self.store_primary_key?
93
+ false
94
+ end
95
+
96
+ cattr_accessor :configurations, :instance_writer => false
97
+ @@configurations = {}
98
+
99
+ # Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
100
+ # table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient way of creating a namespace
101
+ # for tables in a shared database. By default, the prefix is the empty string.
102
+ cattr_accessor :table_name_prefix, :instance_writer => false
103
+ @@table_name_prefix = ""
104
+
105
+ # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
106
+ # "people_basecamp"). By default, the suffix is the empty string.
107
+ cattr_accessor :table_name_suffix, :instance_writer => false
108
+ @@table_name_suffix = ""
109
+
110
+ # Indicates whether table names should be the pluralized versions of the corresponding class names.
111
+ # If true, the default table name for a +Product+ class will be +products+. If false, it would just be +product+.
112
+ # See table_name for the full rules on table/class naming. This is true, by default.
113
+ cattr_accessor :pluralize_table_names, :instance_writer => false
114
+ @@pluralize_table_names = true
115
+
116
+ # Determines whether or not to use ANSI codes to colorize the logging statements committed by the connection adapter. These colors
117
+ # make it much easier to overview things during debugging (when used through a reader like +tail+ and on a black background), but
118
+ # may complicate matters if you use software like syslog. This is true, by default.
119
+ cattr_accessor :colorize_logging, :instance_writer => false
120
+ @@colorize_logging = true
121
+
122
+ # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database.
123
+ # This is set to :local by default.
124
+ cattr_accessor :default_timezone, :instance_writer => false
125
+ @@default_timezone = :local
126
+
127
+ # Determines whether to speed up access by generating optimized reader
128
+ # methods to avoid expensive calls to method_missing when accessing
129
+ # attributes by name. You might want to set this to false in development
130
+ # mode, because the methods would be regenerated on each request.
131
+ cattr_accessor :generate_read_methods, :instance_writer => false
132
+ @@generate_read_methods = false
133
+
134
+ # Determines whether or not to use a connection for each thread, or a single shared connection for all threads.
135
+ # Defaults to false. Set to true if you're writing a threaded application.
136
+ cattr_accessor :allow_concurrency, :instance_writer => false
137
+ @@allow_concurrency = false
138
+
139
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
140
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
141
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
142
+ # hence you can't have attributes that aren't part of the table columns.
143
+ def initialize(attrs = nil)
144
+ preinitialize(attrs)
145
+ @attributes = attributes_from_column_definition
146
+ self.attributes = attrs unless attrs.nil?
147
+ end
148
+
149
+ # Callback method meant to be overriden by subclasses if they need to preload some
150
+ # attributes before initializing the record. (usefull when using a meta model where
151
+ # the list of columns depends on the value of an attribute)
152
+ def preinitialize(attrs = nil)
153
+ @attributes = {}
154
+ end
155
+
156
+ def deserialize(attrs = nil)
157
+ end
158
+
159
+ # Safe version of attributes= so that objects can be instantiated even
160
+ # if columns are removed.
161
+ def safe_attributes=(new_attributes, guard_protected_attributes = true)
162
+ return if new_attributes.nil?
163
+ attributes = new_attributes.dup
164
+ attributes.stringify_keys!
165
+
166
+ multi_parameter_attributes = []
167
+ attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
168
+
169
+ attributes.each do |k, v|
170
+ begin
171
+ k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
172
+ rescue
173
+ logger.debug "#{__FILE__}:#{__LINE__} Warning! Ignoring attribute '#{k}' because it doesn't exist anymore"
174
+ end
175
+ end
176
+
177
+ begin
178
+ assign_multiparameter_attributes(multi_parameter_attributes)
179
+ rescue
180
+ logger.debug "#{__FILE__}:#{__LINE__} Warning! Ignoring multiparameter attributes because some don't exist anymore"
181
+ end
182
+ end
183
+
184
+ # A model instance's primary key is always available as model.id
185
+ # whether you name it the default 'id' or set it to something else.
186
+ def id
187
+ attr_name = self.class.primary_key
188
+ c = column_for_attribute(attr_name)
189
+ define_read_method(:id, attr_name, c) if self.class.generate_read_methods
190
+ read_attribute(attr_name)
191
+ end
192
+
193
+ def to_s
194
+ id
195
+ end
196
+
197
+ def attributes()
198
+ @attributes.dup
199
+ end
200
+
201
+ # Reloads the attributes of this object from the database.
202
+ # The optional options argument is passed to find when reloading so you
203
+ # may do e.g. record.reload(:lock => true) to reload the same record with
204
+ # an exclusive row lock.
205
+ def reload(options = nil)
206
+ @attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
207
+ self
208
+ end
209
+
210
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
211
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
212
+ # (Alias for the protected read_attribute method).
213
+ def [](attr_name)
214
+ read_attribute(attr_name)
215
+ end
216
+
217
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
218
+ # (Alias for the protected write_attribute method).
219
+ def []=(attr_name, value)
220
+ write_attribute(attr_name, value)
221
+ end
222
+
223
+ # Allows you to set all the attributes at once by passing in a hash with keys
224
+ # matching the attribute names (which again matches the column names). Sensitive attributes can be protected
225
+ # from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively
226
+ # specify which attributes *can* be accessed in with the +attr_accessible+ macro. Then all the
227
+ # attributes not included in that won't be allowed to be mass-assigned.
228
+ def attributes=(new_attributes, guard_protected_attributes = true)
229
+ return if new_attributes.nil?
230
+ attributes = new_attributes.dup
231
+ attributes.stringify_keys!
232
+
233
+ multi_parameter_attributes = []
234
+ attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
235
+
236
+ attributes.each do |k, v|
237
+ k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
238
+ end
239
+
240
+ assign_multiparameter_attributes(multi_parameter_attributes)
241
+ end
242
+
243
+ def all_attributes_loaded=(loaded)
244
+ @all_attributes_loaded = loaded
245
+ end
246
+
247
+ def all_attributes_loaded?
248
+ @all_attributes_loaded
249
+ end
250
+
251
+ # Format attributes nicely for inspect.
252
+ def attribute_for_inspect(attr_name)
253
+ value = read_attribute(attr_name)
254
+
255
+ if value.is_a?(String) && value.length > 50
256
+ "#{value[0..50]}...".inspect
257
+ elsif value.is_a?(Date) || value.is_a?(Time)
258
+ %("#{value.to_s(:db)}")
259
+ else
260
+ value.inspect
261
+ end
262
+ end
263
+
264
+ # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
265
+ # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
266
+ def attribute_present?(attribute)
267
+ value = read_attribute(attribute)
268
+ !value.blank? or value == 0
269
+ end
270
+
271
+ # Returns true if the given attribute is in the attributes hash
272
+ def has_attribute?(attr_name)
273
+ @attributes.has_key?(attr_name.to_s)
274
+ end
275
+
276
+ # Returns an array of names for the attributes available on this object sorted alphabetically.
277
+ def attribute_names
278
+ @attributes.keys.sort
279
+ end
280
+
281
+ def human_attribute_name(attribute_key)
282
+ self.class.human_attribute_name(attribute_key)
283
+ end
284
+
285
+ # Overridden by FamilySpanColumns
286
+ # Returns the column object for the named attribute.
287
+ def column_for_attribute(name)
288
+ self.class.columns_hash[name.to_s]
289
+ end
290
+
291
+ # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
292
+ def ==(comparison_object)
293
+ comparison_object.equal?(self) ||
294
+ (comparison_object.instance_of?(self.class) &&
295
+ comparison_object.id == id &&
296
+ !comparison_object.new_record?)
297
+ end
298
+
299
+ # Delegates to ==
300
+ def eql?(comparison_object)
301
+ self == (comparison_object)
302
+ end
303
+
304
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
305
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
306
+ def hash
307
+ id.hash
308
+ end
309
+
310
+ # For checking respond_to? without searching the attributes (which is faster).
311
+ alias_method :respond_to_without_attributes?, :respond_to?
312
+
313
+ # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
314
+ # person.respond_to?("name?") which will all return true.
315
+ def respond_to?(method, include_priv = false)
316
+ if @attributes.nil?
317
+ return super
318
+ elsif attr_name = self.class.column_methods_hash[method.to_sym]
319
+ return true if @attributes.include?(attr_name) || attr_name == self.class.primary_key
320
+ return false if self.class.read_methods.include?(attr_name)
321
+ elsif @attributes.include?(method.to_s)
322
+ return true
323
+ elsif md = self.class.match_attribute_method?(method.to_s)
324
+ return true if @attributes.include?(md.pre_match)
325
+ end
326
+ # super must be called at the end of the method, because the inherited respond_to?
327
+ # would return true for generated readers, even if the attribute wasn't present
328
+ super
329
+ end
330
+
331
+ # Just freeze the attributes hash, such that associations are still accessible even on destroyed records.
332
+ def freeze
333
+ @attributes.freeze; self
334
+ end
335
+
336
+ def frozen?
337
+ @attributes.frozen?
338
+ end
339
+
340
+ def quoted_id #:nodoc:
341
+ quote_value(id, column_for_attribute(self.class.primary_key))
342
+ end
343
+
344
+ # Sets the primary ID.
345
+ def id=(value)
346
+ write_attribute(self.class.primary_key, value)
347
+ end
348
+
349
+ # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
350
+ def new_record?
351
+ false
352
+ end
353
+
354
+ # * No record exists: Creates a new record with values matching those of the object attributes.
355
+ # * A record does exist: Updates the record with values matching those of the object attributes.
356
+ def save
357
+ raise NotImplemented
358
+ end
359
+
360
+ # Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a
361
+ # RecordNotSaved exception
362
+ def save!
363
+ raise NotImplemented
364
+ end
365
+
366
+ # Deletes the record in the database and freezes this instance to reflect that no changes should
367
+ # be made (since they can't be persisted).
368
+ def destroy
369
+ raise NotImplemented
370
+ end
371
+
372
+ # Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
373
+ # Note: This method is overwritten by the Validation module that'll make sure that updates made with this method
374
+ # doesn't get subjected to validation checks. Hence, attributes can be updated even if the full object isn't valid.
375
+ def update_attribute(name, value)
376
+ raise NotImplemented
377
+ end
378
+
379
+ # Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will
380
+ # fail and false will be returned.
381
+ def update_attributes(attributes)
382
+ raise NotImplemented
383
+ end
384
+
385
+ # Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid.
386
+ def update_attributes!(attributes)
387
+ raise NotImplemented
388
+ end
389
+
390
+ def connection
391
+ self.class.connection
392
+ end
393
+
394
+ # Records loaded through joins with piggy-back attributes will be marked as read only as they cannot be saved and return true to this query.
395
+ def readonly?
396
+ @readonly == true
397
+ end
398
+
399
+ def readonly! #:nodoc:
400
+ @readonly = true
401
+ end
402
+
403
+ # Returns the contents of the record as a nicely formatted string.
404
+ def inspect
405
+ attributes_as_nice_string = self.class.column_names.collect { |name|
406
+ if has_attribute?(name) || new_record?
407
+ "#{name}: #{attribute_for_inspect(name)}"
408
+ end
409
+ }.compact.join(", ")
410
+ "#<#{self.class} #{attributes_as_nice_string}>"
411
+ end
412
+
413
+ protected
414
+ def clone_in_persistence_format
415
+ validate_attributes_schema
416
+
417
+ data = {}
418
+
419
+ # normalized attributes without the id
420
+ @attributes.keys.each do |key|
421
+ next if !self.class.store_primary_key? and (key == self.class.primary_key)
422
+ value = read_attribute(key)
423
+ if value.kind_of?(Embedded)
424
+ data[key] = value.clone_in_persistence_format
425
+ elsif value.is_a?(Array)
426
+ data[key] = value.collect do |e|
427
+ if e.kind_of?(Embedded)
428
+ e.clone_in_persistence_format
429
+ else
430
+ e
431
+ end
432
+ end
433
+ else
434
+ data[key] = value
435
+ end
436
+ end
437
+ data
438
+ end
439
+
440
+ # Validate the type of the values in the attributes hash
441
+ def validate_attributes_schema
442
+ @attributes.keys.each do |key|
443
+ value = read_attribute(key)
444
+ next unless value
445
+ # type validation
446
+ if (column = column_for_attribute(key)) and !key.ends_with?(":")
447
+ if column.collection?
448
+ unless value.is_a?(Array)
449
+ raise WrongAttributeDataType, "#{human_attribute_name(column.name)} has the wrong type. Expected collection of #{column.klass}. Record is #{value.class}"
450
+ end
451
+ value.each do |v|
452
+ validate_attribute_type(v, column)
453
+ end
454
+ else
455
+ validate_attribute_type(value, column)
456
+ end
457
+ else
458
+ # Don't save attributes set in a previous schema version
459
+ @attributes.delete(key)
460
+ end
461
+ end
462
+ end
463
+
464
+ def validate_attribute_type(value, column)
465
+ unless (value == nil) or value.kind_of?(column.klass)
466
+ raise WrongAttributeDataType, "#{human_attribute_name(column.name)} has the wrong type. Expected #{column.klass}. Record is #{value.class}"
467
+ end
468
+ end
469
+
470
+ # Generate a new id. Override this to use custom ids.
471
+ def generate_new_id
472
+ UUIDTools::UUID.random_create.to_s
473
+ end
474
+
475
+ def self.random_id #not necessarily unique! -- this is strictly for 'stumbling', not for assigning to new entities
476
+ [8,4,4,4,12].map{|l| "%0#{l}x" % rand(1 << l*4) }.join('-')
477
+ end
478
+
479
+ # Initializes the attributes array with keys matching the columns from the linked table and
480
+ # the values matching the corresponding default value of that column, so
481
+ # that a new instance, or one populated from a passed-in Hash, still has all the attributes
482
+ # that instances loaded from the database would.
483
+ def attributes_from_column_definition
484
+ self.class.columns.inject({}) do |attributes, column|
485
+ unless column.name == self.class.primary_key
486
+ attributes[column.name] = column.default
487
+ end
488
+ attributes
489
+ end
490
+ end
491
+
492
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
493
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
494
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
495
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
496
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float,
497
+ # s for String, and a for Array. If all the values for a given attribute are empty, the attribute will be set to nil.
498
+ def assign_multiparameter_attributes(pairs)
499
+ execute_callstack_for_multiparameter_attributes(
500
+ extract_callstack_for_multiparameter_attributes(pairs)
501
+ )
502
+ end
503
+
504
+ # Includes an ugly hack for Time.local instead of Time.new because the latter is reserved by Time itself.
505
+ def execute_callstack_for_multiparameter_attributes(callstack)
506
+ errors = []
507
+ callstack.each do |name, values|
508
+ # TODO: handle aggregation reflections
509
+ # klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
510
+ column = column_for_attribute(name)
511
+ if column
512
+ klass = column.klass
513
+
514
+ # Ugly fix for time selectors so that when any value is invalid the value is considered invalid, hence nil
515
+ if values.empty? or (column.type == :time and !values[-2..-1].all?) or ([:date, :datetime].include?(column.type) and !values.all?)
516
+ send(name + "=", nil)
517
+ else
518
+ # End of the ugly time fix...
519
+ values = [2000, 1, 1, values[-2], values[-1]] if column.type == :time and !values[0..2].all?
520
+ begin
521
+ send(name + "=", Time == klass ? (@@default_timezone == :utc ? klass.utc(*values) : klass.local(*values)) : klass.new(*values))
522
+ rescue => ex
523
+ errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
524
+ end
525
+ end
526
+ end
527
+ end
528
+ unless errors.empty?
529
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
530
+ end
531
+ end
532
+
533
+ def extract_callstack_for_multiparameter_attributes(pairs)
534
+ attributes = { }
535
+
536
+ for pair in pairs
537
+ multiparameter_name, value = pair
538
+ attribute_name = multiparameter_name.split("(").first
539
+ attributes[attribute_name] = [] unless attributes.include?(attribute_name)
540
+
541
+ position = find_parameter_position(multiparameter_name)
542
+ value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
543
+ attributes[attribute_name] << [position, value]
544
+ end
545
+ attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
546
+ end
547
+
548
+ def type_cast_attribute_value(multiparameter_name, value)
549
+ multiparameter_name =~ /\([0-9]*([a-z])\)/ ? value.send("to_" + $1) : value
550
+ end
551
+
552
+ def find_parameter_position(multiparameter_name)
553
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
554
+ end
555
+
556
+ # Quote strings appropriately for SQL statements.
557
+ def quote_value(value, column = nil)
558
+ self.class.connection.quote(value, column)
559
+ end
560
+
561
+ def create_or_update
562
+ raise NotImplemented
563
+ end
564
+
565
+ # Creates a record with values matching those of the instance attributes
566
+ # and returns its id. Generate a UUID as the row key.
567
+ def create
568
+ raise NotImplemented
569
+ end
570
+
571
+ # Updates the associated record with values matching those of the instance attributes.
572
+ # Returns the number of affected rows.
573
+ def update
574
+ raise NotImplemented
575
+ end
576
+
577
+ # Update this record in hbase. Cannot be directly in the method 'update' because it would trigger callbacks and
578
+ # therefore weird behaviors.
579
+ def update_bigrecord
580
+ timestamp = self.respond_to?(:updated_at) ? self.updated_at.to_bigrecord_timestamp : Time.now.to_bigrecord_timestamp
581
+ connection.update(self.class.table_name, id, clone_in_persistence_format, timestamp)
582
+ end
583
+
584
+ # Allows access to the object attributes, which are held in the @attributes hash, as were
585
+ # they first-class methods. So a Person class with a name attribute can use Person#name and
586
+ # Person#name= and never directly use the attributes hash -- except for multiple assigns with
587
+ # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
588
+ # the completed attribute is not nil or 0.
589
+ #
590
+ # It's also possible to instantiate related objects, so a Client class belonging to the clients
591
+ # table with a master_id foreign key can instantiate master through Client#master.
592
+ def method_missing(method_id, *args, &block)
593
+ method_name = method_id.to_s
594
+ if column_for_attribute(method_name) or
595
+ ((md = /\?$/.match(method_name)) and
596
+ column_for_attribute(query_method_name = md.pre_match) and
597
+ method_name = query_method_name)
598
+ define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods
599
+ md ? query_attribute(method_name) : read_attribute(method_name)
600
+ elsif self.class.primary_key.to_s == method_name
601
+ id
602
+ elsif (md = self.class.match_attribute_method?(method_name))
603
+ attribute_name, method_type = md.pre_match, md.to_s
604
+ if column_for_attribute(attribute_name)
605
+ __send__("attribute#{method_type}", attribute_name, *args, &block)
606
+ else
607
+ super
608
+ end
609
+ else
610
+ super
611
+ end
612
+ end
613
+
614
+ def remove_attributes_protected_from_mass_assignment(attributes)
615
+ safe_attributes =
616
+ if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
617
+ attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
618
+ elsif self.class.protected_attributes.nil?
619
+ attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
620
+ elsif self.class.accessible_attributes.nil?
621
+ attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
622
+ else
623
+ raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
624
+ end
625
+
626
+ if !self.new_record? && !self.class.create_accessible_attributes.nil?
627
+ safe_attributes = safe_attributes.delete_if{ |key, value| self.class.create_accessible_attributes.include?(key.gsub(/\(.+/,"")) }
628
+ end
629
+
630
+ removed_attributes = attributes.keys - safe_attributes.keys
631
+
632
+ if removed_attributes.any?
633
+ log_protected_attribute_removal(removed_attributes)
634
+ end
635
+
636
+ safe_attributes
637
+ end
638
+
639
+ # Removes attributes which have been marked as readonly.
640
+ def remove_readonly_attributes(attributes)
641
+ unless self.class.readonly_attributes.nil?
642
+ attributes.delete_if { |key, value| self.class.readonly_attributes.include?(key.gsub(/\(.+/,"")) }
643
+ else
644
+ attributes
645
+ end
646
+ end
647
+
648
+ def log_protected_attribute_removal(*attributes)
649
+ logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}"
650
+ end
651
+
652
+ # The primary key and inheritance column can never be set by mass-assignment for security reasons.
653
+ def attributes_protected_by_default
654
+ # default = [ self.class.primary_key, self.class.inheritance_column ]
655
+ # default << 'id' unless self.class.primary_key.eql? 'id'
656
+ # default
657
+ []
658
+ end
659
+
660
+ protected
661
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
662
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
663
+ def read_attribute(attr_name)
664
+ attr_name = attr_name.to_s
665
+ if !(value = @attributes[attr_name]).nil?
666
+ if column = column_for_attribute(attr_name)
667
+ write_attribute(attr_name, column.type_cast(value))
668
+ else
669
+ value
670
+ end
671
+ else
672
+ nil
673
+ end
674
+ end
675
+
676
+ def read_attribute_before_type_cast(attr_name)
677
+ @attributes[attr_name.to_s]
678
+ end
679
+
680
+ private
681
+ # Called on first read access to any given column and generates reader
682
+ # methods for all columns in the columns_hash if
683
+ # ActiveRecord::Base.generate_read_methods is set to true.
684
+ def define_read_methods
685
+ self.class.columns_hash.each do |name, column|
686
+ unless respond_to_without_attributes?(name)
687
+ define_read_method(name.to_sym, name, column)
688
+ end
689
+
690
+ unless respond_to_without_attributes?("#{name}?")
691
+ define_question_method(name)
692
+ end
693
+ end
694
+ end
695
+
696
+ # Define an attribute reader method. Cope with nil column.
697
+ def define_read_method(symbol, attr_name, column)
698
+ cast_code = column.type_cast_code('v') if column
699
+ access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
700
+
701
+ unless attr_name.to_s == self.class.primary_key.to_s
702
+ access_code = access_code.insert(0, "raise NoMethodError, 'missing attribute: #{attr_name}', caller unless @attributes.has_key?('#{attr_name}'); ")
703
+ self.class.read_methods << attr_name
704
+ end
705
+
706
+ evaluate_read_method attr_name, "def #{symbol}; #{access_code}; end"
707
+ end
708
+
709
+ # Define an attribute ? method.
710
+ def define_question_method(attr_name)
711
+ unless attr_name.to_s == self.class.primary_key.to_s
712
+ self.class.read_methods << "#{attr_name}?"
713
+ end
714
+
715
+ evaluate_read_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end"
716
+ end
717
+
718
+ # Evaluate the definition for an attribute reader or ? method
719
+ def evaluate_read_method(attr_name, method_definition)
720
+ begin
721
+ self.class.class_eval(method_definition)
722
+ rescue SyntaxError => err
723
+ self.class.read_methods.delete(attr_name)
724
+ if logger
725
+ logger.warn "Exception occurred during reader method compilation."
726
+ logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
727
+ logger.warn "#{err.message}"
728
+ end
729
+ end
730
+ end
731
+
732
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
733
+ # columns are turned into nil.
734
+ def write_attribute(attr_name, value)
735
+ attr_name = attr_name.to_s
736
+ column = column_for_attribute(attr_name)
737
+
738
+ raise "Invalid column for this bigrecord object (e.g., you tried to set a predicate value for an entity that is out of the predicate scope)" if column == nil
739
+
740
+ if column.number?
741
+ @attributes[attr_name] = convert_number_column_value(value)
742
+ else
743
+ @attributes[attr_name] = value
744
+ end
745
+ end
746
+
747
+ def convert_number_column_value(value)
748
+ case value
749
+ when FalseClass then 0
750
+ when TrueClass then 1
751
+ when '' then nil
752
+ else value
753
+ end
754
+ end
755
+
756
+ public
757
+ class << self
758
+
759
+ # Evaluate the name of the column of the primary key only once
760
+ def primary_key
761
+ raise NotImplemented
762
+ end
763
+
764
+ # # Returns a string like 'Post id:integer, title:string, body:text'
765
+ # def inspect
766
+ # if self == Base
767
+ # super
768
+ # elsif abstract_class?
769
+ # "#{super}(abstract)"
770
+ # elsif table_exists?
771
+ # attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
772
+ # "#{super}(#{attr_list})"
773
+ # else
774
+ # "#{super}(Table doesn't exist)"
775
+ # end
776
+ # end
777
+
778
+ # Log and benchmark multiple statements in a single block. Example:
779
+ #
780
+ # Project.benchmark("Creating project") do
781
+ # project = Project.create("name" => "stuff")
782
+ # project.create_manager("name" => "David")
783
+ # project.milestones << Milestone.find(:all)
784
+ # end
785
+ #
786
+ # The benchmark is only recorded if the current level of the logger matches the <tt>log_level</tt>, which makes it
787
+ # easy to include benchmarking statements in production software that will remain inexpensive because the benchmark
788
+ # will only be conducted if the log level is low enough.
789
+ #
790
+ # The logging of the multiple statements is turned off unless <tt>use_silence</tt> is set to false.
791
+ def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
792
+ if logger && logger.level == log_level
793
+ result = nil
794
+ seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
795
+ logger.add(log_level, "#{title} (#{'%.5f' % seconds})")
796
+ result
797
+ else
798
+ yield
799
+ end
800
+ end
801
+
802
+ # Silences the logger for the duration of the block.
803
+ def silence
804
+ old_logger_level, logger.level = logger.level, Logger::ERROR if logger
805
+ yield
806
+ ensure
807
+ logger.level = old_logger_level if logger
808
+ end
809
+
810
+ # Overwrite the default class equality method to provide support for association proxies.
811
+ def ===(object)
812
+ object.is_a?(self)
813
+ end
814
+
815
+ def base_class
816
+ raise NotImplemented
817
+ end
818
+
819
+ # Override this method in the subclasses to add new columns. This is different from ActiveRecord because
820
+ # the number of columns in a Hbase table is variable.
821
+ def columns
822
+ @columns = columns_hash.values
823
+ end
824
+
825
+ # Returns a hash of column objects for the table associated with this class.
826
+ def columns_hash
827
+ unless @all_columns_hash
828
+ # add default hbase columns
829
+ @all_columns_hash =
830
+ if self == base_class
831
+ if @columns_hash
832
+ default_columns.merge(@columns_hash)
833
+ else
834
+ @columns_hash = default_columns
835
+ end
836
+ else
837
+ if @columns_hash
838
+ superclass.columns_hash.merge(@columns_hash)
839
+ else
840
+ superclass.columns_hash
841
+ end
842
+ end
843
+ end
844
+ @all_columns_hash
845
+ end
846
+
847
+ # Returns an array of column names as strings.
848
+ def column_names
849
+ @column_names = columns_hash.keys
850
+ end
851
+
852
+ # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
853
+ # and columns used for single table inheritance have been removed.
854
+ def content_columns
855
+ @content_columns ||= columns.reject{|c| c.primary || "id"}
856
+ end
857
+
858
+ # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
859
+ # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
860
+ # is available.
861
+ def column_methods_hash #:nodoc:
862
+ @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
863
+ attr_name = attr.to_s
864
+ methods[attr.to_sym] = attr_name
865
+ methods["#{attr}=".to_sym] = attr_name
866
+ methods["#{attr}?".to_sym] = attr_name
867
+ methods["#{attr}_before_type_cast".to_sym] = attr_name
868
+ methods
869
+ end
870
+ end
871
+
872
+ def column(name, type, options={})
873
+ name = name.to_s
874
+
875
+ @columns_hash = default_columns unless @columns_hash
876
+
877
+ # The other variables that are cached and depend on @columns_hash need to be reloaded
878
+ invalidate_columns
879
+
880
+ c = create_column(name, type, options)
881
+ @columns_hash[c.name] = c
882
+
883
+ alias_attribute c.alias, c.name if c.alias
884
+
885
+ c
886
+ end
887
+
888
+ def create_column(name, type, options)
889
+ ConnectionAdapters::Column.new(name, type, options)
890
+ end
891
+
892
+ # Define aliases to the fully qualified attributes
893
+ def alias_attribute(alias_name, fully_qualified_name)
894
+ self.class_eval <<-EOF
895
+ def #{alias_name}
896
+ read_attribute("#{fully_qualified_name}")
897
+ end
898
+ def #{alias_name}=(value)
899
+ write_attribute("#{fully_qualified_name}", value)
900
+ end
901
+ EOF
902
+ end
903
+
904
+ # Contains the names of the generated reader methods.
905
+ def read_methods #:nodoc:
906
+ @read_methods ||= Set.new
907
+ end
908
+
909
+ # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
910
+ # Person.human_attribute_name("first_name") # => "First name"
911
+ # Deprecated in favor of just calling "first_name".humanize
912
+ def human_attribute_name(attribute_key_name) #:nodoc:
913
+ attribute_key_name.humanize
914
+ end
915
+
916
+ def quote_value(value, c = nil) #:nodoc:
917
+ connection.quote(value,c)
918
+ end
919
+
920
+ # Finder methods must instantiate through this method.
921
+ def instantiate(raw_record)
922
+ record = self.allocate
923
+ record.deserialize(raw_record)
924
+ record.preinitialize(raw_record)
925
+ record.instance_variable_set(:@new_record, false)
926
+ record.send("safe_attributes=", raw_record, false)
927
+ record
928
+ end
929
+
930
+ # Attributes named in this macro are protected from mass-assignment,
931
+ # such as <tt>new(attributes)</tt>,
932
+ # <tt>update_attributes(attributes)</tt>, or
933
+ # <tt>attributes=(attributes)</tt>.
934
+ #
935
+ # Mass-assignment to these attributes will simply be ignored, to assign
936
+ # to them you can use direct writer methods. This is meant to protect
937
+ # sensitive attributes from being overwritten by malicious users
938
+ # tampering with URLs or forms.
939
+ #
940
+ # class Customer < ActiveRecord::Base
941
+ # attr_protected :credit_rating
942
+ # end
943
+ #
944
+ # customer = Customer.new("name" => David, "credit_rating" => "Excellent")
945
+ # customer.credit_rating # => nil
946
+ # customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
947
+ # customer.credit_rating # => nil
948
+ #
949
+ # customer.credit_rating = "Average"
950
+ # customer.credit_rating # => "Average"
951
+ #
952
+ # To start from an all-closed default and enable attributes as needed,
953
+ # have a look at +attr_accessible+.
954
+ def attr_protected(*attributes)
955
+ write_inheritable_attribute(:attr_protected, Set.new(attributes.map {|a| a.to_s}) + (protected_attributes || []))
956
+ end
957
+
958
+ # Returns an array of all the attributes that have been protected from mass-assignment.
959
+ def protected_attributes # :nodoc:
960
+ read_inheritable_attribute(:attr_protected)
961
+ end
962
+
963
+ # Specifies a white list of model attributes that can be set via
964
+ # mass-assignment, such as <tt>new(attributes)</tt>,
965
+ # <tt>update_attributes(attributes)</tt>, or
966
+ # <tt>attributes=(attributes)</tt>
967
+ #
968
+ # This is the opposite of the +attr_protected+ macro: Mass-assignment
969
+ # will only set attributes in this list, to assign to the rest of
970
+ # attributes you can use direct writer methods. This is meant to protect
971
+ # sensitive attributes from being overwritten by malicious users
972
+ # tampering with URLs or forms. If you'd rather start from an all-open
973
+ # default and restrict attributes as needed, have a look at
974
+ # +attr_protected+.
975
+ #
976
+ # class Customer < ActiveRecord::Base
977
+ # attr_accessible :name, :nickname
978
+ # end
979
+ #
980
+ # customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent")
981
+ # customer.credit_rating # => nil
982
+ # customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" }
983
+ # customer.credit_rating # => nil
984
+ #
985
+ # customer.credit_rating = "Average"
986
+ # customer.credit_rating # => "Average"
987
+ def attr_accessible(*attributes)
988
+ write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
989
+ end
990
+
991
+ # Returns an array of all the attributes that have been made accessible to mass-assignment.
992
+ def accessible_attributes # :nodoc:
993
+ read_inheritable_attribute(:attr_accessible)
994
+ end
995
+
996
+ # Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards.
997
+ def attr_readonly(*attributes)
998
+ write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
999
+ end
1000
+
1001
+ # Returns an array of all the attributes that have been specified as readonly.
1002
+ def readonly_attributes
1003
+ read_inheritable_attribute(:attr_readonly)
1004
+ end
1005
+
1006
+ # Attributes listed as create_accessible work with mass assignment ONLY on creation. After that, any updates
1007
+ # to that attribute will be protected from mass assignment. This differs from attr_readonly since that macro
1008
+ # prevents attributes from ever being changed (even with the explicit setters) after the record is created.
1009
+ #
1010
+ # class Customer < BigRecord::Base
1011
+ # attr_create_accessible :name
1012
+ # end
1013
+ #
1014
+ # customer = Customer.new(:name => "Greg")
1015
+ # customer.name # => "Greg"
1016
+ # customer.save # => true
1017
+ #
1018
+ # customer.attributes = { :name => "Nerd" }
1019
+ # customer.name # => "Greg"
1020
+ # customer.name = "Nerd"
1021
+ # customer.name # => "Nerd"
1022
+ #
1023
+ def attr_create_accessible(*attributes)
1024
+ write_inheritable_attribute(:attr_create_accessible, Set.new(attributes.map(&:to_s)) + (create_accessible_attributes || []))
1025
+ end
1026
+
1027
+ # Returns an array of all the attributes that have been specified as create_accessible.
1028
+ def create_accessible_attributes
1029
+ read_inheritable_attribute(:attr_create_accessible)
1030
+ end
1031
+
1032
+ protected
1033
+ def invalidate_views
1034
+ @views = nil
1035
+ @view_names = nil
1036
+ end
1037
+
1038
+ def invalidate_columns
1039
+ @columns = nil
1040
+ @column_names = nil
1041
+ @content_columns = nil
1042
+ end
1043
+
1044
+ def default_columns
1045
+ raise NotImplemented
1046
+ end
1047
+
1048
+ def default_views
1049
+ {:all=>ConnectionAdapters::View.new('all', nil, self), :default=>ConnectionAdapters::View.new('default', nil, self)}
1050
+ end
1051
+
1052
+ def subclasses #:nodoc:
1053
+ @@subclasses[self] ||= []
1054
+ @@subclasses[self] + @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses }
1055
+ end
1056
+
1057
+ # Returns the class type of the record using the current module as a prefix. So descendents of
1058
+ # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
1059
+ def compute_type(type_name)
1060
+ type_name.constantize
1061
+ end
1062
+
1063
+ public
1064
+ # Guesses the table name, but does not decorate it with prefix and suffix information.
1065
+ def undecorated_table_name(class_name = base_class.name)
1066
+ table_name = Inflector.underscore(Inflector.demodulize(class_name))
1067
+ table_name = Inflector.pluralize(table_name) if pluralize_table_names
1068
+ table_name
1069
+ end
1070
+ end
1071
+
1072
+ protected
1073
+ # Handle *? for method_missing.
1074
+ def attribute?(attribute_name)
1075
+ query_attribute(attribute_name)
1076
+ end
1077
+
1078
+ # Handle *= for method_missing.
1079
+ def attribute=(attribute_name, value)
1080
+ write_attribute(attribute_name, value)
1081
+ end
1082
+
1083
+ # Handle *_before_type_cast for method_missing.
1084
+ def attribute_before_type_cast(attribute_name)
1085
+ read_attribute_before_type_cast(attribute_name)
1086
+ end
1087
+ end
1088
+ end