hyper_record 0.2.8

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.
@@ -0,0 +1,680 @@
1
+ # For each supported data store, ActiveRecord has an adapter that implements
2
+ # functionality specific to that store as well as providing metadata for
3
+ # data held within the store. Features implemented by adapters typically
4
+ # include connection handling, listings metadata (tables and schema),
5
+ # statement execution (selects, writes, etc.), latency measurement, fixture
6
+ # handling.
7
+ #
8
+ # This file implements the adapter for Hypertable used by ActiveRecord
9
+ # (HyperRecord). The adapter communicates with Hypertable using the
10
+ # Thrift client API documented here:
11
+ # http://hypertable.org/thrift-api-ref/index.html
12
+ #
13
+ # Refer to the main hypertable site (http://hypertable.org/) for additional
14
+ # information and documentation (http://hypertable.org/documentation.html)
15
+ # on Hypertable and the Thrift client API.
16
+
17
+ unless defined?(ActiveRecord::ConnectionAdapters::AbstractAdapter)
18
+ # running into some situations where rails has already loaded this, without
19
+ # require realizing it, and loading again is unsafe (alias_method_chain is a
20
+ # great way to create infinite recursion loops)
21
+ require 'active_record/connection_adapters/abstract_adapter'
22
+ end
23
+ require 'active_record/connection_adapters/qualified_column'
24
+ require 'active_record/connection_adapters/hyper_table_definition'
25
+
26
+ module ActiveRecord
27
+ class Base
28
+ # Include the thrift driver if one hasn't already been loaded
29
+ def self.require_hypertable_thrift_client
30
+ unless defined? Hypertable::ThriftClient
31
+ gem 'hypertable-thrift-client'
32
+ require_dependency 'thrift_client'
33
+ end
34
+ end
35
+
36
+ # Establishes a connection to the Thrift Broker (which brokers requests
37
+ # to Hypertable itself. The connection details must appear in
38
+ # config/database.yml. e.g.,
39
+ # hypertable_dev:
40
+ # host: localhost
41
+ # port: 38088
42
+ # timeout: 20000
43
+ #
44
+ # Options:
45
+ # * <tt>:host</tt> - Defaults to localhost
46
+ # * <tt>:port</tt> - Defaults to 38088
47
+ # * <tt>:timeout</tt> - Timeout for queries in milliseconds. Defaults to 20000
48
+ def self.hypertable_connection(config)
49
+ config = config.symbolize_keys
50
+ require_hypertable_thrift_client
51
+
52
+ raise "Hypertable config missing :host in database.yml" if !config[:host]
53
+
54
+ config[:host] ||= 'localhost'
55
+ config[:port] ||= 38088
56
+ config[:timeout] ||= 20000
57
+
58
+ connection = Hypertable::ThriftClient.new(config[:host], config[:port],
59
+ config[:timeout])
60
+
61
+ ConnectionAdapters::HypertableAdapter.new(connection, logger, config)
62
+ end
63
+ end
64
+
65
+ module ConnectionAdapters
66
+ class HypertableAdapter < AbstractAdapter
67
+ # Following cattr_accessors are used to record and access query
68
+ # performance statistics.
69
+ @@read_latency = 0.0
70
+ @@write_latency = 0.0
71
+ @@cells_read = 0
72
+ cattr_accessor :read_latency, :write_latency, :cells_read
73
+
74
+ # Used by retry_on_connection_error() to determine whether to retry
75
+ @retry_on_failure = true
76
+ attr_accessor :retry_on_failure
77
+
78
+ def initialize(connection, logger, config)
79
+ super(connection, logger)
80
+ @config = config
81
+ @hypertable_column_names = {}
82
+ end
83
+
84
+ def raw_thrift_client(&block)
85
+ Hypertable.with_thrift_client(@config[:host], @config[:port],
86
+ @config[:timeout], &block)
87
+ end
88
+
89
+ # Return the current set of performance statistics. The application
90
+ # can retrieve (and reset) these statistics after every query or
91
+ # request for its own logging purposes.
92
+ def self.get_timing
93
+ [@@read_latency, @@write_latency, @@cells_read]
94
+ end
95
+
96
+ # Reset performance metrics.
97
+ def self.reset_timing
98
+ @@read_latency = 0.0
99
+ @@write_latency = 0.0
100
+ @@cells_read = 0
101
+ end
102
+
103
+ def adapter_name
104
+ 'Hypertable'
105
+ end
106
+
107
+ def supports_migrations?
108
+ true
109
+ end
110
+
111
+ # Hypertable only supports string types at the moment, so treat
112
+ # all values as strings and leave it to the application to handle
113
+ # types.
114
+ def native_database_types
115
+ {
116
+ :string => { :name => "varchar", :limit => 255 }
117
+ }
118
+ end
119
+
120
+ def sanitize_conditions(options)
121
+ case options[:conditions]
122
+ when Hash
123
+ # requires Hypertable API to support query by arbitrary cell value
124
+ raise "HyperRecord does not support specifying conditions by Hash"
125
+ when NilClass
126
+ # do nothing
127
+ else
128
+ raise "Only hash conditions are supported"
129
+ end
130
+ end
131
+
132
+ # Execute an HQL query against Hypertable and return the native
133
+ # HqlResult object that comes back from the Thrift client API.
134
+ def execute(hql, name=nil)
135
+ log(hql, name) {
136
+ retry_on_connection_error { @connection.hql_query(hql) }
137
+ }
138
+ end
139
+
140
+ # Execute a query against Hypertable and return the matching cells.
141
+ # The query parameters are denoted in an options hash, which is
142
+ # converted to a "scan spec" by convert_options_to_scan_spec.
143
+ # A "scan spec" is the mechanism used to specify query parameters
144
+ # (e.g., the columns to retrieve, the number of rows to retrieve, etc.)
145
+ # to Hypertable.
146
+ def execute_with_options(options)
147
+ scan_spec = convert_options_to_scan_spec(options)
148
+ t1 = Time.now
149
+
150
+ # Use native array method (get_cells_as_arrays) for cell retrieval -
151
+ # much faster than get_cells that returns Hypertable::ThriftGen::Cell
152
+ # objects.
153
+ # [
154
+ # ["page_1", "name", "", "LOLcats and more", "1237331693147619001"],
155
+ # ["page_1", "url", "", "http://...", "1237331693147619002"]
156
+ # ]
157
+ cells = retry_on_connection_error {
158
+ @connection.get_cells_as_arrays(options[:table_name], scan_spec)
159
+ }
160
+
161
+ # Capture performance metrics
162
+ @@read_latency += Time.now - t1
163
+ @@cells_read += cells.length
164
+
165
+ cells
166
+ end
167
+
168
+ # Convert an options hash to a scan spec. A scan spec is native
169
+ # representation of the query parameters that must be sent to
170
+ # Hypertable.
171
+ # http://hypertable.org/thrift-api-ref/Client.html#Struct_ScanSpec
172
+ def convert_options_to_scan_spec(options={})
173
+ sanitize_conditions(options)
174
+
175
+ # Rows can be specified using a number of different options:
176
+ # :row_keys => [row_key_1, row_key_2, ...]
177
+ # :start_row and :end_row
178
+ # :row_intervals => [[start_1, end_1], [start_2, end_2]]
179
+ row_intervals = []
180
+
181
+ options[:start_inclusive] = options.has_key?(:start_inclusive) ? options[:start_inclusive] : true
182
+ options[:end_inclusive] = options.has_key?(:end_inclusive) ? options[:end_inclusive] : true
183
+
184
+ if options[:row_keys]
185
+ options[:row_keys].flatten.each do |rk|
186
+ row_intervals << [rk, rk]
187
+ end
188
+ elsif options[:row_intervals]
189
+ options[:row_intervals].each do |ri|
190
+ row_intervals << [ri.first, ri.last]
191
+ end
192
+ elsif options[:start_row]
193
+ raise "missing :end_row" if !options[:end_row]
194
+ row_intervals << [options[:start_row], options[:end_row]]
195
+ end
196
+
197
+ # Add each row interval to the scan spec
198
+ options[:row_intervals] = row_intervals.map do |row_interval|
199
+ ri = Hypertable::ThriftGen::RowInterval.new
200
+ ri.start_row = row_interval.first
201
+ ri.start_inclusive = options[:start_inclusive]
202
+ ri.end_row = row_interval.last
203
+ ri.end_inclusive = options[:end_inclusive]
204
+ ri
205
+ end
206
+
207
+ scan_spec = Hypertable::ThriftGen::ScanSpec.new
208
+
209
+ # Hypertable can store multiple revisions for each cell but this
210
+ # feature does not map into an ORM very well. By default, just
211
+ # retrieve the latest revision of each cell. Since this is most
212
+ # common config when using HyperRecord, tables should be defined
213
+ # with MAX_VERSIONS=1 at creation time to save space and reduce
214
+ # query time.
215
+ options[:revs] ||= 1
216
+
217
+ # Most of the time, we're not interested in cells that have been
218
+ # marked deleted but have not actually been deleted yet.
219
+ options[:return_deletes] ||= false
220
+
221
+ for key in options.keys
222
+ case key.to_sym
223
+ when :row_intervals
224
+ scan_spec.row_intervals = options[key]
225
+ when :cell_intervals
226
+ scan_spec.cell_intervals = options[key]
227
+ when :start_time
228
+ scan_spec.start_time = options[key]
229
+ when :end_time
230
+ scan_spec.end_time = options[key]
231
+ when :limit
232
+ scan_spec.row_limit = options[key]
233
+ when :revs
234
+ scan_spec.revs = options[key]
235
+ when :return_deletes
236
+ scan_spec.return_deletes = options[key]
237
+ when :select
238
+ # Columns listed here can only be column families (not
239
+ # column qualifiers) at this time.
240
+ requested_columns = if options[key].is_a?(String)
241
+ requested_columns_from_string(options[key])
242
+ elsif options[key].is_a?(Symbol)
243
+ requested_columns_from_string(options[key].to_s)
244
+ elsif options[key].is_a?(Array)
245
+ options[key].map{|k| k.to_s}
246
+ else
247
+ options[key]
248
+ end
249
+
250
+ scan_spec.columns = requested_columns.map do |column|
251
+ status, family, qualifier = is_qualified_column_name?(column)
252
+ family
253
+ end.uniq
254
+ when :table_name, :start_row, :end_row, :start_inclusive, :end_inclusive, :select, :columns, :row_keys, :conditions, :include, :readonly, :scan_spec, :instantiate_only_requested_columns
255
+ # ignore
256
+ else
257
+ raise "Unrecognized scan spec option: #{key}"
258
+ end
259
+ end
260
+
261
+ scan_spec
262
+ end
263
+
264
+ def requested_columns_from_string(s)
265
+ if s == '*'
266
+ []
267
+ else
268
+ s.split(',').map{|s| s.strip}
269
+ end
270
+ end
271
+
272
+ # Exceptions generated by Thrift IDL do not set a message.
273
+ # This causes a lot of problems for Rails which expects a String
274
+ # value and throws exception when it encounters NilClass.
275
+ # Unfortunately, you cannot assign a message to exceptions so define
276
+ # a singleton to accomplish same goal.
277
+ def handle_thrift_exceptions_with_missing_message
278
+ begin
279
+ yield
280
+ rescue Exception => err
281
+ if !err.message
282
+ if err.respond_to?("message=")
283
+ err.message = err.what || ''
284
+ else
285
+ def err.message
286
+ self.what || ''
287
+ end
288
+ end
289
+ end
290
+
291
+ raise err
292
+ end
293
+ end
294
+
295
+ # Attempt to reconnect to the Thrift Broker once before aborting.
296
+ # This ensures graceful recovery in the case that the Thrift Broker
297
+ # goes down and then comes back up.
298
+ def retry_on_connection_error
299
+ @retry_on_failure = true
300
+ begin
301
+ handle_thrift_exceptions_with_missing_message { yield }
302
+ rescue Thrift::TransportException, IOError, Thrift::ApplicationException, Thrift::ProtocolException => err
303
+ if @retry_on_failure
304
+ @retry_on_failure = false
305
+ @connection.close
306
+ @connection.open
307
+ retry
308
+ else
309
+ raise err
310
+ end
311
+ end
312
+ end
313
+
314
+ # Column Operations
315
+
316
+ # Returns array of column objects for the table associated with this
317
+ # class. Hypertable allows columns to include dashes in the name.
318
+ # This doesn't play well with Ruby (can't have dashes in method names),
319
+ # so we maintain a mapping of original column names to Ruby-safe
320
+ # names.
321
+ def columns(table_name, name = nil)
322
+ # Each table always has a row key called 'ROW'
323
+ columns = [ Column.new('ROW', '') ]
324
+
325
+ schema = describe_table(table_name)
326
+ doc = REXML::Document.new(schema)
327
+ column_families = doc.each_element('Schema/AccessGroup/ColumnFamily') { |cf| cf }
328
+
329
+ @hypertable_column_names[table_name] ||= {}
330
+ for cf in column_families
331
+ # Columns are lazily-deleted in Hypertable so still may show up
332
+ # in describe table output. Ignore.
333
+ deleted = cf.elements['deleted'].text
334
+ next if deleted == 'true'
335
+
336
+ column_name = cf.elements['Name'].text
337
+ rubified_name = rubify_column_name(column_name)
338
+ @hypertable_column_names[table_name][rubified_name] = column_name
339
+ columns << new_column(rubified_name, '')
340
+ end
341
+
342
+ columns
343
+ end
344
+
345
+ def remove_column_from_name_map(table_name, name)
346
+ @hypertable_column_names[table_name].delete(rubify_column_name(name))
347
+ end
348
+
349
+ def add_column_to_name_map(table_name, name)
350
+ @hypertable_column_names[table_name][rubify_column_name(name)] = name
351
+ end
352
+
353
+ def add_qualified_column(table_name, column_family, qualifiers=[], default='', sql_type=nil, null=true)
354
+ qc = QualifiedColumn.new(column_family, default, sql_type, null)
355
+ qc.qualifiers = qualifiers
356
+ qualifiers.each{|q| add_column_to_name_map(table_name, qualified_column_name(column_family, q))}
357
+ qc
358
+ end
359
+
360
+ def new_column(column_name, default_value='')
361
+ Column.new(rubify_column_name(column_name), default_value)
362
+ end
363
+
364
+ def qualified_column_name(column_family, qualifier=nil)
365
+ [column_family, qualifier].compact.join(':')
366
+ end
367
+
368
+ def rubify_column_name(column_name)
369
+ column_name.to_s.gsub(/-+/, '_')
370
+ end
371
+
372
+ def is_qualified_column_name?(column_name)
373
+ column_family, qualifier = column_name.split(':', 2)
374
+ if qualifier
375
+ [true, column_family, qualifier]
376
+ else
377
+ [false, column_name, nil]
378
+ end
379
+ end
380
+
381
+ # Schema alterations
382
+
383
+ def rename_column(table_name, column_name, new_column_name)
384
+ raise "rename_column operation not supported by Hypertable."
385
+ end
386
+
387
+ def change_column(table_name, column_name, new_column_name)
388
+ raise "change_column operation not supported by Hypertable."
389
+ end
390
+
391
+ # Translate "sexy" ActiveRecord::Migration syntax to an HQL
392
+ # CREATE TABLE statement.
393
+ def create_table_hql(table_name, options={}, &block)
394
+ table_definition = HyperTableDefinition.new(self)
395
+
396
+ yield table_definition
397
+
398
+ if options[:force] && table_exists?(table_name)
399
+ drop_table(table_name, options)
400
+ end
401
+
402
+ create_sql = [ "CREATE TABLE #{quote_table_name(table_name)} (" ]
403
+ column_sql = []
404
+ for col in table_definition.columns
405
+ column_sql << [
406
+ quote_table_name(col.name),
407
+ col.max_versions ? "MAX_VERSIONS=#{col.max_versions}" : ''
408
+ ].join(' ')
409
+ end
410
+ create_sql << column_sql.join(', ')
411
+
412
+ create_sql << ") #{options[:options]}"
413
+ create_sql.join(' ').strip
414
+ end
415
+
416
+ def create_table(table_name, options={}, &block)
417
+ execute(create_table_hql(table_name, options, &block))
418
+ end
419
+
420
+ def drop_table(table_name, options = {})
421
+ retry_on_connection_error {
422
+ @connection.drop_table(table_name, options[:if_exists] || false)
423
+ }
424
+ end
425
+
426
+ def rename_table(table_name, options = {})
427
+ raise "rename_table operation not supported by Hypertable."
428
+ end
429
+
430
+ def change_column_default(table_name, column_name, default)
431
+ raise "change_column_default operation not supported by Hypertable."
432
+ end
433
+
434
+ def change_column_null(table_name, column_name, null, default = nil)
435
+ raise "change_column_null operation not supported by Hypertable."
436
+ end
437
+
438
+ def add_column(table_name, column_name, type=:string, options = {})
439
+ hql = [ "ALTER TABLE #{quote_table_name(table_name)} ADD (" ]
440
+ hql << quote_column_name(column_name)
441
+ hql << "MAX_VERSIONS=#{options[:max_versions]}" if !options[:max_versions].blank?
442
+ hql << ")"
443
+ execute(hql.join(' '))
444
+ end
445
+
446
+ def add_column_options!(hql, options)
447
+ hql << " MAX_VERSIONS =1 #{quote(options[:default], options[:column])}" if options_include_default?(options)
448
+ # must explicitly check for :null to allow change_column to work on migrations
449
+ if options[:null] == false
450
+ hql << " NOT NULL"
451
+ end
452
+ end
453
+
454
+ def remove_column(table_name, *column_names)
455
+ column_names.flatten.each do |column_name|
456
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP(#{quote_column_name(column_name)})"
457
+ end
458
+ end
459
+ alias :remove_columns :remove_column
460
+
461
+ def quote(value, column = nil)
462
+ case value
463
+ when NilClass then ''
464
+ when String then value
465
+ else super(value, column)
466
+ end
467
+ end
468
+
469
+ def quote_column_name(name)
470
+ "'#{name}'"
471
+ end
472
+
473
+ def quote_column_name_for_table(name, table_name)
474
+ quote_column_name(hypertable_column_name(name, table_name))
475
+ end
476
+
477
+ def hypertable_column_name(name, table_name, declared_columns_only=false)
478
+ begin
479
+ columns(table_name) if @hypertable_column_names[table_name].blank?
480
+ n = @hypertable_column_names[table_name][name]
481
+ n ||= name if !declared_columns_only
482
+ n
483
+ rescue Exception => err
484
+ raise [
485
+ "hypertable_column_name exception",
486
+ err.message,
487
+ "table: #{table_name}",
488
+ "column: #{name}",
489
+ "@htcn: #{pp @hypertable_column_names}"
490
+ ].join("\n")
491
+ end
492
+ end
493
+
494
+ # Return an XML document describing the table named in the first
495
+ # argument. Output is equivalent to that returned by the DESCRIBE
496
+ # TABLE command available in the Hypertable CLI.
497
+ # <Schema generation="2">
498
+ # <AccessGroup name="default">
499
+ # <ColumnFamily id="1">
500
+ # <Generation>1</Generation>
501
+ # <Name>date</Name>
502
+ # <deleted>false</deleted>
503
+ # </ColumnFamily>
504
+ # </AccessGroup>
505
+ # </Schema>
506
+ def describe_table(table_name)
507
+ retry_on_connection_error {
508
+ @connection.get_schema(table_name)
509
+ }
510
+ end
511
+
512
+ # Returns an array of tables available in the current Hypertable
513
+ # instance.
514
+ def tables(name=nil)
515
+ retry_on_connection_error {
516
+ @connection.get_tables
517
+ }
518
+ end
519
+
520
+ # Write an array of cells to the named table. By default, write_cells
521
+ # will open and close a mutator for this operation. Closing the
522
+ # mutator flushes the data, which guarantees is it is stored in
523
+ # Hypertable before the call returns. This also slows down the
524
+ # operation, so if you're doing lots of writes and want to manage
525
+ # mutator flushes at the application layer then you can pass in a
526
+ # mutator as argument. Mutators can be created with the open_mutator
527
+ # method. In the near future (Summer 2009), Hypertable will provide
528
+ # a periodic mutator that automatically flushes at specific intervals.
529
+ def write_cells(table_name, cells, mutator=nil, flags=nil, flush_interval=nil)
530
+ return if cells.blank?
531
+
532
+ retry_on_connection_error {
533
+ local_mutator_created = !mutator
534
+
535
+ begin
536
+ t1 = Time.now
537
+ mutator ||= open_mutator(table_name, flags, flush_interval)
538
+ @connection.set_cells_as_arrays(mutator, cells)
539
+ ensure
540
+ if local_mutator_created && mutator
541
+ close_mutator(mutator)
542
+ mutator = nil
543
+ end
544
+ @@write_latency += Time.now - t1
545
+ end
546
+ }
547
+ end
548
+
549
+ # Return a Hypertable::ThriftGen::Cell object from a cell passed in
550
+ # as an array of format: [row_key, column_name, value]
551
+ # Hypertable::ThriftGen::Cell objects are required when setting a flag
552
+ # on write - used by special operations (e.g,. delete )
553
+ def thrift_cell_from_native_array(array)
554
+ cell = Hypertable::ThriftGen::Cell.new
555
+ cell.row_key = array[0]
556
+ cell.column_family = array[1]
557
+ cell.column_qualifier = array[2] if !array[2].blank?
558
+ cell.value = array[3] if array[3]
559
+ cell.timestamp = array[4] if array[4]
560
+ cell
561
+ end
562
+
563
+ # Create native array format for cell. Most HyperRecord operations
564
+ # deal with cells in native array format since operations on an
565
+ # array are much faster than operations on Hypertable::ThriftGen::Cell
566
+ # objects.
567
+ # ["row_key", "column_family", "column_qualifier", "value"],
568
+ def cell_native_array(row_key, column_family, column_qualifier, value=nil, timestamp=nil)
569
+ [
570
+ row_key.to_s,
571
+ column_family.to_s,
572
+ column_qualifier.to_s,
573
+ value.to_s
574
+ ].map do |s|
575
+ s.respond_to?(:force_encoding) ? s.force_encoding('ascii-8bit') : s
576
+ end
577
+ end
578
+
579
+ # Delete cells from a table.
580
+ def delete_cells(table_name, cells)
581
+ t1 = Time.now
582
+
583
+ retry_on_connection_error {
584
+ @connection.with_mutator(table_name) do |mutator|
585
+ thrift_cells = cells.map{|c|
586
+ cell = thrift_cell_from_native_array(c)
587
+ cell.flag = Hypertable::ThriftGen::CellFlag::DELETE_CELL
588
+ cell
589
+ }
590
+ @connection.set_cells(mutator, thrift_cells)
591
+ end
592
+ }
593
+
594
+ @@write_latency += Time.now - t1
595
+ end
596
+
597
+ # Delete rows from a table.
598
+ def delete_rows(table_name, row_keys)
599
+ t1 = Time.now
600
+ cells = row_keys.map do |row_key|
601
+ cell = Hypertable::ThriftGen::Cell.new
602
+ cell.row_key = row_key
603
+ cell.flag = Hypertable::ThriftGen::CellFlag::DELETE_ROW
604
+ cell
605
+ end
606
+
607
+ retry_on_connection_error {
608
+ @connection.with_mutator(table_name) do |mutator|
609
+ @connection.set_cells(mutator, cells)
610
+ end
611
+ }
612
+
613
+ @@write_latency += Time.now - t1
614
+ end
615
+
616
+ # Insert a test fixture into a table.
617
+ def insert_fixture(fixture, table_name)
618
+ fixture_hash = fixture.to_hash
619
+ timestamp = fixture_hash.delete('timestamp')
620
+ row_key = fixture_hash.delete('ROW')
621
+ cells = []
622
+ fixture_hash.keys.each do |k|
623
+ column_name, column_family = k.split(':', 2)
624
+ cells << cell_native_array(row_key, column_name, column_family, fixture_hash[k], timestamp)
625
+ end
626
+ write_cells(table_name, cells)
627
+ end
628
+
629
+ # Mutator methods
630
+
631
+ def open_mutator(table_name, flags=0, flush_interval=0)
632
+ @connection.open_mutator(table_name, flags, flush_interval)
633
+ end
634
+
635
+ # Flush is always called in a mutator's destructor due to recent
636
+ # no_log_sync changes. Adding an explicit flush here just adds
637
+ # one round trip for an extra flush call, so change the default to
638
+ # flush=0. Consider removing this argument and always sending 0.
639
+ def close_mutator(mutator, flush=0)
640
+ @connection.close_mutator(mutator, flush)
641
+ end
642
+
643
+ def flush_mutator(mutator)
644
+ @connection.flush_mutator(mutator)
645
+ end
646
+
647
+ # Scanner methods
648
+
649
+ def open_scanner(table_name, scan_spec)
650
+ @connection.open_scanner(table_name, scan_spec, true)
651
+ end
652
+
653
+ def close_scanner(scanner)
654
+ @connection.close_scanner(scanner)
655
+ end
656
+
657
+ def with_scanner(table_name, scan_spec, &block)
658
+ @connection.with_scanner(table_name, scan_spec, &block)
659
+ end
660
+
661
+ # Iterator methods
662
+
663
+ def each_cell(scanner, &block)
664
+ @connection.each_cell(scanner, &block)
665
+ end
666
+
667
+ def each_cell_as_arrays(scanner, &block)
668
+ @connection.each_cell_as_arrays(scanner, &block)
669
+ end
670
+
671
+ def each_row(scanner, &block)
672
+ @connection.each_row(scanner, &block)
673
+ end
674
+
675
+ def each_row_as_arrays(scanner, &block)
676
+ @connection.each_row_as_arrays(scanner, &block)
677
+ end
678
+ end
679
+ end
680
+ end