aerospike 2.29.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +13 -9
  4. data/lib/aerospike/batch_attr.rb +292 -0
  5. data/lib/aerospike/batch_delete.rb +48 -0
  6. data/lib/aerospike/batch_read.rb +97 -0
  7. data/lib/aerospike/batch_record.rb +83 -0
  8. data/lib/aerospike/batch_results.rb +38 -0
  9. data/lib/aerospike/batch_udf.rb +76 -0
  10. data/lib/aerospike/batch_write.rb +79 -0
  11. data/lib/aerospike/cdt/bit_operation.rb +4 -5
  12. data/lib/aerospike/cdt/map_operation.rb +24 -10
  13. data/lib/aerospike/cdt/map_policy.rb +6 -3
  14. data/lib/aerospike/cdt/map_return_type.rb +8 -0
  15. data/lib/aerospike/client.rb +39 -56
  16. data/lib/aerospike/cluster.rb +50 -46
  17. data/lib/aerospike/command/batch_index_command.rb +7 -11
  18. data/lib/aerospike/command/batch_index_node.rb +3 -4
  19. data/lib/aerospike/command/batch_operate_command.rb +151 -0
  20. data/lib/aerospike/command/batch_operate_node.rb +51 -0
  21. data/lib/aerospike/command/command.rb +231 -128
  22. data/lib/aerospike/exp/exp.rb +54 -27
  23. data/lib/aerospike/exp/exp_bit.rb +24 -24
  24. data/lib/aerospike/exp/exp_hll.rb +12 -12
  25. data/lib/aerospike/exp/exp_list.rb +101 -86
  26. data/lib/aerospike/exp/exp_map.rb +118 -110
  27. data/lib/aerospike/exp/operation.rb +2 -2
  28. data/lib/aerospike/info.rb +2 -4
  29. data/lib/aerospike/node.rb +20 -3
  30. data/lib/aerospike/operation.rb +38 -0
  31. data/lib/aerospike/policy/batch_delete_policy.rb +71 -0
  32. data/lib/aerospike/policy/batch_policy.rb +53 -4
  33. data/lib/aerospike/{command/batch_direct_node.rb → policy/batch_read_policy.rb} +17 -19
  34. data/lib/aerospike/policy/batch_udf_policy.rb +75 -0
  35. data/lib/aerospike/policy/batch_write_policy.rb +105 -0
  36. data/lib/aerospike/policy/policy.rb +3 -40
  37. data/lib/aerospike/query/query_command.rb +3 -205
  38. data/lib/aerospike/query/query_executor.rb +2 -2
  39. data/lib/aerospike/query/query_partition_command.rb +4 -230
  40. data/lib/aerospike/query/scan_executor.rb +2 -2
  41. data/lib/aerospike/query/scan_partition_command.rb +3 -3
  42. data/lib/aerospike/query/server_command.rb +2 -2
  43. data/lib/aerospike/query/statement.rb +5 -21
  44. data/lib/aerospike/task/execute_task.rb +2 -2
  45. data/lib/aerospike/utils/buffer.rb +15 -15
  46. data/lib/aerospike/version.rb +1 -1
  47. data/lib/aerospike.rb +13 -12
  48. metadata +16 -14
  49. data/lib/aerospike/command/batch_direct_command.rb +0 -105
  50. data/lib/aerospike/command/batch_direct_exists_command.rb +0 -51
  51. data/lib/aerospike/query/pred_exp/and_or.rb +0 -32
  52. data/lib/aerospike/query/pred_exp/geo_json_value.rb +0 -41
  53. data/lib/aerospike/query/pred_exp/integer_value.rb +0 -32
  54. data/lib/aerospike/query/pred_exp/op.rb +0 -27
  55. data/lib/aerospike/query/pred_exp/regex.rb +0 -32
  56. data/lib/aerospike/query/pred_exp/regex_flags.rb +0 -23
  57. data/lib/aerospike/query/pred_exp/string_value.rb +0 -29
  58. data/lib/aerospike/query/pred_exp.rb +0 -192
@@ -120,15 +120,15 @@ module Aerospike
120
120
  # Returns a node on the cluster for read operations
121
121
  def batch_read_node(partition, replica_policy)
122
122
  case replica_policy
123
- when Aerospike::Replica::MASTER, Aerospike::Replica::SEQUENCE
124
- return master_node(partition)
125
- when Aerospike::Replica::MASTER_PROLES
126
- return master_proles_node(partition)
127
- when Aerospike::Replica::PREFER_RACK
128
- return rack_node(partition, seq)
129
- when Aerospike::Replica::RANDOM
130
- return random_node
131
- else
123
+ when Aerospike::Replica::MASTER, Aerospike::Replica::SEQUENCE
124
+ master_node(partition)
125
+ when Aerospike::Replica::MASTER_PROLES
126
+ master_proles_node(partition)
127
+ when Aerospike::Replica::PREFER_RACK
128
+ rack_node(partition, seq)
129
+ when Aerospike::Replica::RANDOM
130
+ random_node
131
+ else
132
132
  raise Aerospike::Exceptions::InvalidNode("invalid policy.replica value")
133
133
  end
134
134
  end
@@ -136,17 +136,17 @@ module Aerospike
136
136
  # Returns a node on the cluster for read operations
137
137
  def read_node(partition, replica_policy, seq)
138
138
  case replica_policy
139
- when Aerospike::Replica::MASTER
140
- return master_node(partition)
141
- when Aerospike::Replica::MASTER_PROLES
142
- return master_proles_node(partition)
143
- when Aerospike::Replica::PREFER_RACK
144
- return rack_node(partition, seq)
145
- when Aerospike::Replica::SEQUENCE
146
- return sequence_node(partition, seq)
147
- when Aerospike::Replica::RANDOM
148
- return random_node
149
- else
139
+ when Aerospike::Replica::MASTER
140
+ master_node(partition)
141
+ when Aerospike::Replica::MASTER_PROLES
142
+ master_proles_node(partition)
143
+ when Aerospike::Replica::PREFER_RACK
144
+ rack_node(partition, seq)
145
+ when Aerospike::Replica::SEQUENCE
146
+ sequence_node(partition, seq)
147
+ when Aerospike::Replica::RANDOM
148
+ random_node
149
+ else
150
150
  raise Aerospike::Exceptions::InvalidNode("invalid policy.replica value")
151
151
  end
152
152
  end
@@ -155,12 +155,12 @@ module Aerospike
155
155
  def master_node(partition)
156
156
  partition_map = partitions
157
157
  replica_array = partition_map[partition.namespace]
158
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
158
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
159
159
 
160
- node_array = (replica_array.get)[0]
161
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !node_array
160
+ node_array = replica_array.get[0]
161
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless node_array
162
162
 
163
- node = (node_array.get)[partition.partition_id]
163
+ node = node_array.get[partition.partition_id]
164
164
  raise Aerospike::Exceptions::InvalidNode if !node || !node.active?
165
165
 
166
166
  node
@@ -170,7 +170,7 @@ module Aerospike
170
170
  def rack_node(partition, seq)
171
171
  partition_map = partitions
172
172
  replica_array = partition_map[partition.namespace]
173
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
173
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
174
174
 
175
175
  replica_array = replica_array.get
176
176
 
@@ -179,10 +179,10 @@ module Aerospike
179
179
  node = nil
180
180
  fallback = nil
181
181
  for i in 1..replica_array.length
182
- idx = (seq.update{|v| v.succ} % replica_array.size).abs
183
- node = (replica_array[idx].get)[partition.partition_id]
182
+ idx = (seq.update { |v| v.succ } % replica_array.size).abs
183
+ node = replica_array[idx].get[partition.partition_id]
184
184
 
185
- next if !node
185
+ next unless node
186
186
 
187
187
  fallback = node
188
188
 
@@ -202,14 +202,14 @@ module Aerospike
202
202
  def master_proles_node(partition)
203
203
  partition_map = partitions
204
204
  replica_array = partition_map[partition.namespace]
205
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
205
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
206
206
 
207
207
  replica_array = replica_array.get
208
208
 
209
209
  node = nil
210
210
  for replica in replica_array
211
- idx = (@replica_index.update{|v| v.succ} % replica_array.size).abs
212
- node = (replica_array[idx].get)[partition.partition_id]
211
+ idx = (@replica_index.update { |v| v.succ } % replica_array.size).abs
212
+ node = replica_array[idx].get[partition.partition_id]
213
213
 
214
214
  return node if node && node.active?
215
215
  end
@@ -221,14 +221,14 @@ module Aerospike
221
221
  def sequence_node(partition, seq)
222
222
  partition_map = partitions
223
223
  replica_array = partition_map[partition.namespace]
224
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
224
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
225
225
 
226
226
  replica_array = replica_array.get
227
227
 
228
228
  node = nil
229
229
  for replica in replica_array
230
- idx = (seq.update{|v| v.succ} % replica_array.size).abs
231
- node = (replica_array[idx].get)[partition.partition_id]
230
+ idx = (seq.update { |v| v.succ } % replica_array.size).abs
231
+ node = replica_array[idx].get[partition.partition_id]
232
232
 
233
233
  return node if node && node.active?
234
234
  end
@@ -236,9 +236,13 @@ module Aerospike
236
236
  raise Aerospike::Exceptions::InvalidNode
237
237
  end
238
238
 
239
- def get_node_for_key(replica_policy, key)
239
+ def get_node_for_key(replica_policy, key, is_write: false)
240
240
  partition = Partition.new_by_key(key)
241
- batch_read_node(partition, replica_policy)
241
+ if is_write
242
+ master_node(partition)
243
+ else
244
+ batch_read_node(partition, replica_policy)
245
+ end
242
246
  end
243
247
 
244
248
  # Returns partitions pertaining to a node
@@ -247,10 +251,10 @@ module Aerospike
247
251
 
248
252
  partition_map = partitions
249
253
  replica_array = partition_map[namespace]
250
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
254
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
251
255
 
252
- node_array = (replica_array.get)[0]
253
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !node_array
256
+ node_array = replica_array.get[0]
257
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless node_array
254
258
 
255
259
 
256
260
  pid = 0
@@ -270,7 +274,7 @@ module Aerospike
270
274
  i = 0
271
275
  while i < length
272
276
  # Must handle concurrency with other non-tending threads, so node_index is consistent.
273
- idx = (@node_index.update{ |v| v.succ } % node_array.length).abs
277
+ idx = (@node_index.update { |v| v.succ } % node_array.length).abs
274
278
  node = node_array[idx]
275
279
 
276
280
  return node if node.active?
@@ -366,13 +370,13 @@ module Aerospike
366
370
  @tend_thread = Thread.new do
367
371
  Thread.current.abort_on_exception = false
368
372
  loop do
369
- begin
373
+
370
374
  tend
371
375
  sleep(@tend_interval / 1000.0)
372
- rescue => e
376
+ rescue => e
373
377
  Aerospike.logger.error("Exception occured during tend: #{e}")
374
378
  Aerospike.logger.debug { e.backtrace.join("\n") }
375
- end
379
+
376
380
  end
377
381
  end
378
382
  end
@@ -453,7 +457,7 @@ module Aerospike
453
457
 
454
458
  def log_tend_stats(nodes)
455
459
  diff = nodes.size - @old_node_count
456
- action = "#{diff.abs} #{diff.abs == 1 ? "node has" : "nodes have"} #{diff > 0 ? "joined" : "left"} the cluster."
460
+ action = "#{diff.abs} #{diff.abs == 1 ? 'node has' : 'nodes have'} #{diff > 0 ? 'joined' : 'left'} the cluster."
457
461
  Aerospike.logger.info("Tend finished. #{action} Old node count: #{@old_node_count}, New node count: #{nodes.size}")
458
462
  @old_node_count = nodes.size
459
463
  end
@@ -689,11 +693,11 @@ module Aerospike
689
693
  end
690
694
 
691
695
  def node_exists(search, node_list)
692
- node_list.any? {|node| node == search }
696
+ node_list.any? { |node| node == search }
693
697
  end
694
698
 
695
699
  def find_node_by_name(node_name)
696
- nodes.detect{|node| node.name == node_name }
700
+ nodes.detect { |node| node.name == node_name }
697
701
  end
698
702
  end
699
703
  end
@@ -21,11 +21,7 @@ module Aerospike
21
21
 
22
22
  class BatchIndexCommand < MultiCommand #:nodoc:
23
23
 
24
- attr_accessor :batch
25
- attr_accessor :policy
26
- attr_accessor :bin_names
27
- attr_accessor :results
28
- attr_accessor :read_attr
24
+ attr_accessor :batch, :policy, :bin_names, :results, :read_attr
29
25
 
30
26
  def initialize(node, batch, policy, bin_names, results, read_attr)
31
27
  super(node)
@@ -42,8 +38,8 @@ module Aerospike
42
38
  field_count_row = 1
43
39
  field_count = 1
44
40
 
45
- predexp_size = estimate_predexp(@policy.predexp)
46
- field_count += 1 if predexp_size > 0
41
+ exp_size = estimate_expression_size(@policy.filter_exp)
42
+ field_count += 1 if exp_size > 0
47
43
 
48
44
  if bin_names
49
45
  bin_names.each do |bin_name|
@@ -58,7 +54,7 @@ module Aerospike
58
54
  batch.keys.each do |key|
59
55
  @data_offset += key.digest.length + 4 # 4 byte batch offset
60
56
 
61
- if prev != nil && prev.namespace == key.namespace
57
+ if !prev.nil? && prev.namespace == key.namespace
62
58
  @data_offset += 1
63
59
  else
64
60
  @data_offset += key.namespace.bytesize + FIELD_HEADER_SIZE + 1 + 1 + 2 + 2 # repeat/no-repeat flag + read_attr flags + field_count + operation_count
@@ -66,9 +62,9 @@ module Aerospike
66
62
  end
67
63
  end
68
64
  size_buffer
69
- write_header(policy,read_attr | INFO1_BATCH, 0, field_count, 0)
65
+ write_header_read(policy, read_attr | INFO1_BATCH, 0, field_count, 0)
70
66
 
71
- write_predexp(@policy.predexp, predexp_size)
67
+ write_filter_exp(@policy.filter_exp, exp_size)
72
68
 
73
69
  write_field_header(0, Aerospike::FieldType::BATCH_INDEX)
74
70
  @data_offset += @data_buffer.write_int32(batch.keys.length, @data_offset)
@@ -80,7 +76,7 @@ module Aerospike
80
76
  @data_offset += @data_buffer.write_int32(index, @data_offset)
81
77
  @data_offset += @data_buffer.write_binary(key.digest, @data_offset)
82
78
 
83
- if (prev != nil && prev.namespace == key.namespace)
79
+ if !prev.nil? && prev.namespace == key.namespace
84
80
  @data_offset += @data_buffer.write_byte(1, @data_offset)
85
81
  else
86
82
  @data_offset += @data_buffer.write_byte(0, @data_offset)
@@ -19,13 +19,12 @@ module Aerospike
19
19
 
20
20
  class BatchIndexNode #:nodoc:
21
21
 
22
- attr_accessor :node
23
- attr_accessor :keys_by_idx
22
+ attr_accessor :node, :keys_by_idx
24
23
 
25
24
  def self.generate_list(cluster, replica_policy, keys)
26
25
  keys.each_with_index
27
- .group_by { |key, _| cluster.get_node_for_key(replica_policy, key) }
28
- .map { |node, keys_with_idx| BatchIndexNode.new(node, keys_with_idx) }
26
+ .group_by { |key, _| cluster.get_node_for_key(replica_policy, key) }
27
+ .map { |node, keys_with_idx| BatchIndexNode.new(node, keys_with_idx) }
29
28
  end
30
29
 
31
30
  def initialize(node, keys_with_idx)
@@ -0,0 +1,151 @@
1
+ # Copyright 2018 Aerospike, Inc.
2
+ #
3
+ # Portions may be licensed to Aerospike, Inc. under one or more contributor
4
+ # license agreements.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not
7
+ # use this file except in compliance with the License. You may obtain a copy of
8
+ # the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+ # License for the specific language governing permissions and limitations under
16
+ # the License.
17
+
18
+ require 'aerospike/command/multi_command'
19
+
20
+ module Aerospike
21
+
22
+ class BatchOperateCommand < MultiCommand #:nodoc:
23
+
24
+ attr_accessor :batch, :policy, :attr, :records
25
+
26
+ def initialize(node, batch, policy, records)
27
+ super(node)
28
+ @batch = batch
29
+ @policy = policy
30
+ @records = records
31
+ end
32
+
33
+ def batch_flags
34
+ flags = 0
35
+ # flags |= 0x1 if @policy.allow_inline
36
+ flags |= 0x2 if @policy.allow_inline_ssd
37
+ flags |= 0x4 if @policy.respond_all_keys
38
+ flags
39
+ end
40
+
41
+ def write_buffer
42
+ field_count = 1
43
+
44
+ exp_size = estimate_expression_size(@policy.filter_exp)
45
+ @data_offset += exp_size
46
+ field_count += 1 if exp_size > 0
47
+
48
+ @data_buffer.reset
49
+ begin_cmd
50
+ @data_offset += FIELD_HEADER_SIZE + 4 + 1 # batch.keys.length + flags
51
+
52
+ prev = nil
53
+ @records.each do |record|
54
+ key = record.key
55
+ @data_offset += key.digest.length + 4 # 4 byte batch offset
56
+
57
+ if !@policy.send_key && !prev.nil? && prev.key.namespace == key.namespace && prev.key.set_name == key.set_name && record == prev
58
+ @data_offset += 1
59
+ else
60
+ @data_offset += 12
61
+ @data_offset += key.namespace.bytesize + FIELD_HEADER_SIZE
62
+ @data_offset += key.set_name.bytesize + FIELD_HEADER_SIZE
63
+ @data_offset += record.size
64
+ end
65
+
66
+ prev = record
67
+ end
68
+ size_buffer
69
+ write_batch_header(policy, field_count)
70
+
71
+ write_filter_exp(@policy.filter_exp, exp_size)
72
+
73
+ field_size_offset = @data_offset
74
+
75
+ write_field_header(0, Aerospike::FieldType::BATCH_INDEX)
76
+ @data_offset += @data_buffer.write_int32(batch.records.length, @data_offset)
77
+ @data_offset += @data_buffer.write_byte(batch_flags, @data_offset)
78
+
79
+ prev = nil
80
+ attr = BatchAttr.new
81
+ batch.records.each_with_index do |record, index|
82
+ @data_offset += @data_buffer.write_int32(index, @data_offset)
83
+ key = record.key
84
+ @data_offset += @data_buffer.write_binary(key.digest, @data_offset)
85
+
86
+ if !@policy.send_key && !prev.nil? && prev.key.namespace == key.namespace && prev.key.set_name == key.set_name && record == prev
87
+ @data_offset += @data_buffer.write_byte(BATCH_MSG_REPEAT, @data_offset)
88
+ else
89
+ case record
90
+ when BatchRead
91
+ attr.set_batch_read(record.policy)
92
+ if record.bin_names&.length&.> 0
93
+ write_batch_bin_names(key, record.bin_names, attr, attr.filter_exp)
94
+ elsif record.ops&.length&.> 0
95
+ attr.adjust_read(br.ops)
96
+ write_batch_operations(key, record.ops, attr, attr.filter_exp)
97
+ else
98
+ attr.adjust_read_all_bins(record.read_all_bins)
99
+ write_batch_read(key, attr, attr.filter_exp, 0)
100
+ end
101
+
102
+ when BatchWrite
103
+ attr.set_batch_write(record.policy)
104
+ attr.adjust_write(record.ops)
105
+ write_batch_operations(key, record.ops, attr, attr.filter_exp)
106
+
107
+ when BatchUDF
108
+ attr.set_batch_udf(record.policy)
109
+ write_batch_write(key, attr, attr.filter_exp, 3, 0)
110
+ write_field_string(record.package_name, Aerospike::FieldType::UDF_PACKAGE_NAME)
111
+ write_field_string(record.function_name, Aerospike::FieldType::UDF_FUNCTION)
112
+ write_field_bytes(record.arg_bytes, Aerospike::FieldType::UDF_ARGLIST)
113
+
114
+ when BatchDelete
115
+ attr.set_batch_delete(record.policy)
116
+ write_batch_write(key, attr, attr.filter_exp, 0, 0)
117
+ end
118
+
119
+ prev = record
120
+ end
121
+ end
122
+
123
+ @data_buffer.write_uint32(@data_offset-MSG_TOTAL_HEADER_SIZE-4, field_size_offset)
124
+
125
+ end_cmd
126
+ mark_compressed(@policy)
127
+ end
128
+
129
+ # Parse all results in the batch. Add records to shared list.
130
+ # If the record was not found, the bins will be nil.
131
+ def parse_row(result_code)
132
+ generation = @data_buffer.read_int32(6)
133
+ expiration = @data_buffer.read_int32(10)
134
+ batch_index = @data_buffer.read_int32(14)
135
+ field_count = @data_buffer.read_int16(18)
136
+ op_count = @data_buffer.read_int16(20)
137
+
138
+ skip_key(field_count)
139
+ req_key = records[batch_index].key
140
+
141
+ records[batch_index].result_code = result_code
142
+ case result_code
143
+ when 0, ResultCode::UDF_BAD_RESPONSE
144
+ record = parse_record(req_key, op_count, generation, expiration)
145
+ records[batch_index].record = record
146
+ end
147
+ end
148
+
149
+ end # class
150
+
151
+ end # module
@@ -0,0 +1,51 @@
1
+ # Copyright 2018 Aerospike, Inc.
2
+ #
3
+ # Portions may be licensed to Aerospike, Inc. under one or more contributor
4
+ # license agreements.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not
7
+ # use this file except in compliance with the License. You may obtain a copy of
8
+ # the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+ # License for the specific language governing permissions and limitations under
16
+ # the License.
17
+
18
+ module Aerospike
19
+
20
+ class BatchOperateNode #:nodoc:
21
+
22
+ attr_accessor :node, :records_by_idx
23
+
24
+ def self.generate_list(cluster, replica_policy, records)
25
+ records.each_with_index
26
+ .group_by { |record, _| cluster.get_node_for_key(replica_policy, record.key, is_write: record.has_write) }
27
+ .map { |node, records_with_idx| BatchOperateNode.new(node, records_with_idx) }
28
+ end
29
+
30
+ def initialize(node, records_with_idx)
31
+ @node = node
32
+ @records_by_idx = records_with_idx.map(&:reverse).to_h
33
+ end
34
+
35
+ def records
36
+ records_by_idx.values
37
+ end
38
+
39
+ def each_record_with_index
40
+ records_by_idx.each do |idx, rec|
41
+ yield rec, idx
42
+ end
43
+ end
44
+
45
+ def record_for_index(idx)
46
+ @records_by_idx[idx]
47
+ end
48
+
49
+ end
50
+
51
+ end