bigrecord 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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,618 @@
1
+ ## The sub-classes of this class must implement the abstract method 'column_names' of this class.
2
+ module BigRecord
3
+
4
+ class Base < AbstractBase
5
+
6
+ attr_accessor :modified_attributes
7
+
8
+
9
+ def self.inherited(child) #:nodoc:
10
+ @@subclasses[self] ||= []
11
+ @@subclasses[self] << child
12
+ child.set_table_name child.name.tableize if child.superclass == BigRecord::Base
13
+ super
14
+ end
15
+
16
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
17
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
18
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
19
+ # hence you can't have attributes that aren't part of the table columns.
20
+ def initialize(attrs = nil)
21
+ @new_record = true
22
+ super
23
+ attrs.keys.each{ |k| set_loaded(k) } if attrs
24
+ end
25
+
26
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
27
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
28
+ # (Alias for the protected read_attribute method).
29
+ def [](attr_name)
30
+ if attr_name.ends_with?(":")
31
+ read_family_attributes(attr_name)
32
+ else
33
+ read_attribute(attr_name)
34
+ end
35
+ end
36
+
37
+ # protected
38
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
39
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
40
+ def read_attribute(attr_name, options={})
41
+ attr_name = attr_name.to_s
42
+ column = column_for_attribute(attr_name)
43
+ if column
44
+ # First check if the attribute is already in the attributes hash
45
+ if @attributes.has_key?(attr_name) and options.blank?
46
+ super(attr_name)
47
+ # Elsif the column exist, we try to lazy load it
48
+ elsif !(is_loaded?(attr_name)) and attr_name != self.class.primary_key and !new_record?
49
+ unless self.all_attributes_loaded? and attr_name =~ /\A#{self.class.default_family}:/
50
+ if options.blank?
51
+ # Normal behavior
52
+
53
+ # Retrieve the version of the attribute matching the current record version
54
+ options[:timestamp] = self.updated_at.to_bigrecord_timestamp if self.has_attribute?("#{self.class.default_family}:updated_at") and self.updated_at
55
+
56
+ # get the content of the cell
57
+ value = connection.get(self.class.table_name, self.id, attr_name, options)
58
+
59
+ set_loaded(attr_name)
60
+ write_attribute(attr_name, column.type_cast(value))
61
+ else
62
+ # Special request... don't keep it in the attributes hash
63
+ options[:timestamp] ||= self.updated_at.to_bigrecord_timestamp if self.has_attribute?("#{self.class.default_family}:updated_at") and self.updated_at
64
+
65
+ # get the content of the cell
66
+ value = connection.get(self.class.table_name, self.id, attr_name, options)
67
+
68
+ if options[:versions] and options[:versions] > 1
69
+ value.collect{ |v| column.type_cast(v) }
70
+ else
71
+ column.type_cast(value)
72
+ end
73
+ end
74
+ else
75
+ write_attribute(attr_name, column.default)
76
+ end
77
+ else
78
+ write_attribute(attr_name, column.default)
79
+ end
80
+ else
81
+ nil
82
+ end
83
+ end
84
+
85
+ def read_family_attributes(attr_name)
86
+ attr_name = attr_name.to_s
87
+ column = column_for_attribute(attr_name)
88
+ if column
89
+ # First check if the attribute is already in the attributes hash
90
+ if @attributes.has_key?(attr_name)
91
+ if (values = @attributes[attr_name]) and values.is_a?(Hash)
92
+ values.delete(self.class.primary_key)
93
+ casted_values = {}
94
+ values.each{|k,v| casted_values[k] = column.type_cast(v)}
95
+ write_attribute(attr_name, casted_values)
96
+ else
97
+ write_attribute(attr_name, {})
98
+ end
99
+
100
+ # Elsif the column exist, we try to lazy load it
101
+ elsif !(is_loaded?(attr_name)) and attr_name != self.class.primary_key and !new_record?
102
+ unless self.all_attributes_loaded? and attr_name =~ /\A#{self.class.default_family}:/
103
+ options = {}
104
+ # Retrieve the version of the attribute matching the current record version
105
+ options[:timestamp] = self.updated_at.to_bigrecord_timestamp if self.has_attribute?("#{self.class.default_family}:updated_at") and self.updated_at
106
+
107
+ # get the content of the whole family
108
+ values = connection.get_columns(self.class.table_name, self.id, [attr_name], options)
109
+ if values
110
+ values.delete(self.class.primary_key)
111
+ casted_values = {}
112
+ values.each do |k,v|
113
+ short_name = k.split(":")[1]
114
+ casted_values[short_name] = column.type_cast(v) if short_name
115
+ set_loaded(k)
116
+ write_attribute(k, casted_values[short_name]) if short_name
117
+ end
118
+ write_attribute(attr_name, casted_values)
119
+ else
120
+ set_loaded(attr_name)
121
+ write_attribute(attr_name, {})
122
+ end
123
+ else
124
+ write_attribute(attr_name, column.default)
125
+ end
126
+ else
127
+ write_attribute(attr_name, column.default)
128
+ end
129
+ else
130
+ nil
131
+ end
132
+ end
133
+
134
+ def set_loaded(name)
135
+ @loaded_columns ||= []
136
+ @loaded_columns << name
137
+ end
138
+
139
+ def is_loaded?(name)
140
+ @loaded_columns ||= []
141
+ @loaded_columns.include?(name)
142
+ end
143
+
144
+ public
145
+ # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
146
+ def new_record?
147
+ @new_record
148
+ end
149
+
150
+ # * No record exists: Creates a new record with values matching those of the object attributes.
151
+ # * A record does exist: Updates the record with values matching those of the object attributes.
152
+ def save
153
+ create_or_update
154
+ end
155
+
156
+ # Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a
157
+ # RecordNotSaved exception
158
+ def save!
159
+ create_or_update || raise(RecordNotSaved)
160
+ end
161
+
162
+ # Deletes the record in the database and freezes this instance to reflect that no changes should
163
+ # be made (since they can't be persisted).
164
+ def destroy
165
+ unless new_record?
166
+ connection.delete(self.class.table_name, self.id)
167
+ end
168
+
169
+ # FIXME: this currently doesn't work because we write the attributes everytime we read them
170
+ # which means that we cannot read the attributes of a deleted record... it's bad
171
+ # freeze
172
+ end
173
+
174
+ # Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
175
+ # Note: This method is overwritten by the Validation module that'll make sure that updates made with this method
176
+ # doesn't get subjected to validation checks. Hence, attributes can be updated even if the full object isn't valid.
177
+ def update_attribute(name, value)
178
+ send(name.to_s + '=', value)
179
+ save
180
+ end
181
+
182
+ # Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will
183
+ # fail and false will be returned.
184
+ def update_attributes(attributes)
185
+ self.attributes = attributes
186
+ save
187
+ end
188
+
189
+ # Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid.
190
+ def update_attributes!(attributes)
191
+ self.attributes = attributes
192
+ save!
193
+ end
194
+
195
+ def connection
196
+ self.class.connection
197
+ end
198
+
199
+ protected
200
+
201
+ def create_or_update
202
+ raise ReadOnlyRecord if readonly?
203
+ result = new_record? ? create : update
204
+ result != false
205
+ end
206
+
207
+ # Creates a record with values matching those of the instance attributes
208
+ # and returns its id. Generate a UUID as the row key.
209
+ def create
210
+ self.id = generate_new_id unless self.id
211
+ @new_record = false
212
+ update_bigrecord
213
+ end
214
+
215
+ # Updates the associated record with values matching those of the instance attributes.
216
+ # Returns the number of affected rows.
217
+ def update
218
+ update_bigrecord
219
+ end
220
+
221
+ # Update this record in hbase. Cannot be directly in the method 'update' because it would trigger callbacks and
222
+ # therefore weird behaviors.
223
+ def update_bigrecord
224
+ timestamp = self.respond_to?(:updated_at) ? self.updated_at.to_bigrecord_timestamp : Time.now.to_bigrecord_timestamp
225
+
226
+ data = clone_in_persistence_format
227
+
228
+ connection.update(self.class.table_name, id, data, timestamp)
229
+ end
230
+
231
+ public
232
+ class << self
233
+
234
+ # Replaced with: class_inheritable_accessor :default_family (line 46)
235
+ # def default_family
236
+ # "attribute"
237
+ # end
238
+
239
+ def primary_key
240
+ @primary_key ||= "id"
241
+ end
242
+
243
+ # Return the list of families for this class
244
+ def families
245
+ columns.collect(&:family).uniq
246
+ end
247
+
248
+ # HBase scanner utility -- scans the table and executes code on each record
249
+ # Example:
250
+ # Entity.scan(:batch_size => 200) {|e|puts "#{e.name} is a child!" if e.parent}
251
+ #
252
+ # Parameters:
253
+ # batch_size - number of records to retrieve from database with each scan iteration.
254
+ # code - the code to execute (see example above for syntax)
255
+ #
256
+ def scan(options={}, &code)
257
+ options = options.dup
258
+ limit = options.delete(:batch_size) || 100
259
+
260
+ items_processed = 0
261
+
262
+ # add an extra record for defining the next offset without duplicating records
263
+ limit += 1
264
+ last_row_id = nil
265
+
266
+ while true
267
+ items = find(:all, options.merge({:limit => limit}))
268
+
269
+ # set the new offset as the extra record
270
+ unless items.empty?
271
+ items.delete_at(0) if items[0].id == last_row_id
272
+
273
+ break if items.empty?
274
+
275
+ last_row_id = items.last.id
276
+ options[:offset] = last_row_id
277
+ items_processed += items.size
278
+
279
+ items.each do |item|
280
+ code.call(item)
281
+ end
282
+ else
283
+ break
284
+ end
285
+ end
286
+ end
287
+
288
+ def find(*args)
289
+ options = extract_options_from_args!(args)
290
+ validate_find_options(options)
291
+
292
+ # set a default view
293
+ if options[:view]
294
+ options[:view] = options[:view].to_sym
295
+ else
296
+ options[:view] = :default
297
+ end
298
+
299
+ case args.first
300
+ when :first then find_every(options.merge({:limit => 1})).first
301
+ when :all then find_every(options)
302
+ else find_from_ids(args, options)
303
+ end
304
+ end
305
+
306
+ # Returns true if the given +id+ represents the primary key of a record in the database, false otherwise.
307
+ def exists?(id)
308
+ !find(id).nil?
309
+ rescue BigRecord::BigRecordError
310
+ false
311
+ end
312
+
313
+ # Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
314
+ # fails under validations, the unsaved object is still returned.
315
+ def create(attrs = nil)
316
+ if attrs.is_a?(Array)
317
+ attrs.collect { |attr| create(attr) }
318
+ else
319
+ object = new(attrs)
320
+ object.save
321
+ object
322
+ end
323
+ end
324
+
325
+ # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
326
+ # and returns it. If the save fails under validations, the unsaved object is still returned.
327
+ #
328
+ # The arguments may also be given as arrays in which case the update method is called for each pair of +id+ and
329
+ # +attributes+ and an array of objects is returned.
330
+ #
331
+ # Example of updating one record:
332
+ # Person.update(15, {:user_name => 'Samuel', :group => 'expert'})
333
+ #
334
+ # Example of updating multiple records:
335
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} }
336
+ # Person.update(people.keys, people.values)
337
+ def update(id, attributes)
338
+ if id.is_a?(Array)
339
+ idx = -1
340
+ id.collect { |a| idx += 1; update(a, attributes[idx]) }
341
+ else
342
+ object = find(id)
343
+ object.update_attributes(attributes)
344
+ object
345
+ end
346
+ end
347
+
348
+ # Deletes the record with the given +id+ without instantiating an object first. If an array of ids is provided, all of them
349
+ # are deleted.
350
+ def delete(id)
351
+ if id.is_a?(Array)
352
+ id.each { |a| connection.delete(table_name, a) }
353
+ else
354
+ connection.delete(table_name, id)
355
+ end
356
+ end
357
+
358
+ # Destroys the record with the given +id+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
359
+ # If an array of ids is provided, all of them are destroyed.
360
+ def destroy(id)
361
+ id.is_a?(Array) ? id.each { |a| destroy(a) } : find(id).destroy
362
+ end
363
+
364
+ # Updates all records with the SET-part of an SQL update statement in +updates+ and returns an integer with the number of rows updated.
365
+ # A subset of the records can be selected by specifying +conditions+. Example:
366
+ # Billing.update_all "category = 'authorized', approved = 1", "author = 'David'"
367
+ def update_all(updates, conditions = nil)
368
+ raise NotImplemented, "update_all"
369
+ end
370
+
371
+ # Destroys the objects for all the records that match the +condition+ by instantiating each object and calling
372
+ # the destroy method. Example:
373
+ # Person.destroy_all "last_login < '2004-04-04'"
374
+ def destroy_all(conditions = nil)
375
+ find(:all, :conditions => conditions).each { |object| object.destroy }
376
+ end
377
+
378
+ # Deletes all the records that match the +condition+ without instantiating the objects first (and hence not
379
+ # calling the destroy method). Example:
380
+ # Post.delete_all "person_id = 5 AND (category = 'Something' OR category = 'Else')"
381
+ #
382
+ # TODO: take into consideration the conditions
383
+ def delete_all(conditions = nil)
384
+ connection.get_consecutive_rows(table_name, nil, nil, ["#{default_family}:"]).each do |row|
385
+ connection.delete(table_name, row["id"])
386
+ end
387
+ end
388
+
389
+ # Truncate the table for this model
390
+ def truncate
391
+ connection.truncate_table(table_name)
392
+ end
393
+
394
+ def table_name
395
+ (superclass == BigRecord::Base) ? @table_name : superclass.table_name
396
+ end
397
+
398
+ def set_table_name(name)
399
+ @table_name = name.to_s
400
+ end
401
+
402
+ def default_family
403
+ (superclass == BigRecord::Base) ? (@default_family ||= "attribute") : superclass.default_family
404
+ end
405
+
406
+ def set_default_family(name)
407
+ @default_family = name.to_s
408
+ end
409
+
410
+ def base_class
411
+ (superclass == BigRecord::Base) ? self : superclass.base_class
412
+ end
413
+
414
+ def view(name, columns)
415
+ name = name.to_sym
416
+ @views_hash ||= default_views
417
+
418
+ # The other variables that are cached and depend on @views_hash need to be reloaded
419
+ invalidate_views
420
+
421
+ @views_hash[name] = ConnectionAdapters::View.new(name, columns, self)
422
+ end
423
+
424
+ def views
425
+ @views ||= views_hash.values
426
+ end
427
+
428
+ def view_names
429
+ @view_names ||= views_hash.keys
430
+ end
431
+
432
+ def views_hash
433
+ unless @all_views_hash
434
+ # add default hbase columns
435
+ @all_views_hash =
436
+ if self == BigRecord::Base # stop at Base
437
+ @views_hash = default_views
438
+ else
439
+ if @views_hash
440
+ superclass.views_hash.merge(default_views).merge(@views_hash)
441
+ else
442
+ superclass.views_hash.merge(default_views)
443
+ end
444
+ end
445
+ end
446
+ @all_views_hash
447
+ end
448
+
449
+ def default_columns
450
+ {primary_key => ConnectionAdapters::Column.new(primary_key, 'string')}
451
+ end
452
+
453
+ def column(name, type, options={})
454
+ name = name.to_s
455
+ name = "#{self.default_family}:#{name}" unless (name =~ /:/)
456
+
457
+ super(name, type, options)
458
+ end
459
+
460
+ def default_views
461
+ {:all=>ConnectionAdapters::View.new('all', nil, self), :default=>ConnectionAdapters::View.new('default', nil, self)}
462
+ end
463
+
464
+ def find_all_by_id(ids, options={})
465
+ ids.inject([]) do |result, id|
466
+ begin
467
+ result << find_one(id, options)
468
+ rescue BigRecord::RecordNotFound => e
469
+ end
470
+ result
471
+ end
472
+ end
473
+
474
+ protected
475
+ def invalidate_views
476
+ @views = nil
477
+ @view_names = nil
478
+ end
479
+
480
+ def extract_options_from_args!(args) #:nodoc:
481
+ args.last.is_a?(Hash) ? args.pop : {}
482
+ end
483
+
484
+ VALID_FIND_OPTIONS = [:limit, :offset, :include, :view, :versions, :timestamp,
485
+ :include_deleted, :force_reload, :columns, :stop_row]
486
+
487
+ def validate_find_options(options) #:nodoc:
488
+ options.assert_valid_keys(VALID_FIND_OPTIONS)
489
+ end
490
+
491
+ def find_every(options)
492
+ requested_columns = columns_to_find(options)
493
+
494
+ raw_records = connection.get_consecutive_rows(table_name, options[:offset],
495
+ options[:limit], requested_columns, options[:stop_row])
496
+
497
+ raw_records.collect do |raw_record|
498
+ add_missing_cells(raw_record, requested_columns)
499
+ rec = instantiate(raw_record)
500
+ rec.all_attributes_loaded = true if options[:view] == :all
501
+ rec
502
+ end
503
+ end
504
+
505
+ def find_from_ids(ids, options)
506
+ expects_array = ids.first.kind_of?(Array)
507
+ return ids.first if expects_array && ids.first.empty?
508
+
509
+ ids = ids.flatten.compact.uniq
510
+
511
+ case ids.size
512
+ when 0
513
+ raise RecordNotFound, "Couldn't find #{name} without an ID"
514
+ when 1
515
+ result = find_one(ids.first, options)
516
+ expects_array ? [ result ] : result
517
+ else
518
+ ids.collect do |id|
519
+ find_one(id, options)
520
+ end
521
+ end
522
+ end
523
+
524
+ def find_one(id, options)
525
+ # allow to pass a record (e.g. Entity.find(@entity)) and not only a string (e.g. Entity.find("$-monkey-123"))
526
+ unless id.is_a?(String)
527
+ id = id.id if id and not id.is_a?(String)
528
+ end
529
+
530
+ # Allow the client to give us other objects than integers, e.g. Time and String
531
+ if options[:timestamp] && options[:timestamp].kind_of?(Time)
532
+ options[:timestamp] = options[:timestamp].to_bigrecord_timestamp
533
+ end
534
+
535
+ requested_columns = columns_to_find(options)
536
+
537
+ # TODO: this is a hack... it should be done in a single call but currently hbase doesn't allow that
538
+ raw_record =
539
+ if options[:versions] and options[:versions] > 1
540
+ timestamps = connection.get(table_name, id, "#{default_family}:updated_at", options)
541
+ timestamps.collect{|timestamp| connection.get_columns(table_name, id, requested_columns, :timestamp => timestamp.to_bigrecord_timestamp)}
542
+ else
543
+ connection.get_columns(table_name, id, requested_columns, options)
544
+ end
545
+
546
+ # Instantiate the raw record (or records, if multiple versions were asked)
547
+ if raw_record
548
+ if raw_record.is_a?(Array)
549
+ unless raw_record.empty?
550
+ raw_record.collect do |r|
551
+ add_missing_cells(r, requested_columns)
552
+ rec = instantiate(r)
553
+ rec.all_attributes_loaded = true if options[:view] == :all
554
+ rec
555
+ end
556
+ else
557
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{id}"
558
+ end
559
+ else
560
+ add_missing_cells(raw_record, requested_columns)
561
+ rec = instantiate(raw_record)
562
+ rec.all_attributes_loaded = true if options[:view] == :all
563
+ rec
564
+ end
565
+ else
566
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{id}"
567
+ end
568
+ end
569
+
570
+ # return the list of columns to get from hbase
571
+ def columns_to_find(options={})
572
+ c =
573
+ if options[:columns]
574
+ options[:columns]
575
+ elsif options[:view]
576
+ raise ArgumentError, "Unknown view: #{options[:view]}" unless views_hash[options[:view]]
577
+ if options[:view] == :all
578
+ ["#{default_family}:"]
579
+ else
580
+ views_hash[options[:view]].column_names
581
+ end
582
+ elsif views_hash[:default]
583
+ views_hash[:default].column_names
584
+ else
585
+ ["#{default_family}:"]
586
+ end
587
+ c += [options[:include]] if options[:include]
588
+ c.flatten.reject{|x| x == "id"}
589
+ end
590
+
591
+ # Add the missing cells to the raw record and set them to nil. We know that it's
592
+ # nil because else we would have received those cells. That way, when the value of
593
+ # one of these cells will be requested by the client we won't try to lazy load it.
594
+ def add_missing_cells(raw_record, requested_columns)
595
+ requested_columns.each do |k, v|
596
+ # don't do it for column families (e.g. attribute:)
597
+ unless k =~ /:$/
598
+ raw_record[k] = nil unless raw_record.has_key?(k)
599
+ end
600
+ end
601
+ end
602
+
603
+ # Define aliases to the fully qualified attributes
604
+ def alias_attribute(alias_name, fully_qualified_name)
605
+ self.class_eval <<-EOF
606
+ def #{alias_name}(options={})
607
+ read_attribute("#{fully_qualified_name}", options)
608
+ end
609
+ def #{alias_name}=(value)
610
+ write_attribute("#{fully_qualified_name}", value)
611
+ end
612
+ EOF
613
+ end
614
+
615
+ end
616
+
617
+ end
618
+ end