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,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