herringbone 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +198 -74
- data/bin/herringbone +38 -26
- data/lib/herringbone/active_record.rb +48 -19
- data/lib/herringbone/bloom_filter.rb +370 -0
- data/lib/herringbone/byte_values.rb +29 -0
- data/lib/herringbone/codecs/lz4.rb +66 -3
- data/lib/herringbone/codecs/snappy.rb +132 -3
- data/lib/herringbone/compression.rb +121 -16
- data/lib/herringbone/encodings/delta.rb +76 -19
- data/lib/herringbone/encodings/plain.rb +29 -7
- data/lib/herringbone/encodings/rle.rb +55 -3
- data/lib/herringbone/format.rb +203 -0
- data/lib/herringbone/inspector.rb +1784 -0
- data/lib/herringbone/io_buffer_support.rb +3 -1
- data/lib/herringbone/reader/column_chunk_reader.rb +388 -0
- data/lib/herringbone/reader/column_cursor.rb +225 -0
- data/lib/herringbone/reader/numo.rb +365 -0
- data/lib/herringbone/reader/page_stream.rb +615 -0
- data/lib/herringbone/reader/scan.rb +350 -0
- data/lib/herringbone/reader.rb +585 -272
- data/lib/herringbone/schema.rb +326 -90
- data/lib/herringbone/thrift.rb +155 -4
- data/lib/herringbone/types.rb +177 -42
- data/lib/herringbone/version.rb +2 -1
- data/lib/herringbone/visualizer.rb +1136 -0
- data/lib/herringbone/writer.rb +550 -105
- data/lib/herringbone/xxhash.rb +425 -0
- data/lib/herringbone.rb +64 -9
- metadata +14 -33
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herringbone
|
|
4
|
+
class Reader
|
|
5
|
+
# read(as: :numo) and each_batch(as: :numo): columns as Numo arrays. Numo is optional:
|
|
6
|
+
# "numo/narray" (from numo-narray-alt or numo-narray) is required on first use.
|
|
7
|
+
#
|
|
8
|
+
# Flat INT32 / INT64 / FLOAT / DOUBLE / BOOLEAN columns are decoded without a Ruby object
|
|
9
|
+
# per value: PLAIN and BYTE_STREAM_SPLIT pages go through Numo::X.from_binary, dictionary
|
|
10
|
+
# pages through dictionary[indices], and definition levels become a validity mask. Other
|
|
11
|
+
# columns are read like as: :columns and converted per batch.
|
|
12
|
+
#
|
|
13
|
+
# Type mapping (following Polars' Series#to_numo and Rover):
|
|
14
|
+
# INT32 / INT64 (also TIME) Numo::Int32 / Int64
|
|
15
|
+
# INT(8/16, signed) Int8 / Int16
|
|
16
|
+
# INT(8/16/32/64, unsigned) UInt8 / UInt16 / UInt32 / UInt64
|
|
17
|
+
# FLOAT / FLOAT16 / DOUBLE SFloat / SFloat / DFloat, NaN for nulls
|
|
18
|
+
# integers with nulls DFloat with NaN (exact up to 2^53)
|
|
19
|
+
# BOOLEAN Bit; RObject of true/false/nil with nulls
|
|
20
|
+
# list<number>, all rows of the 2-D [rows, length] of the element type
|
|
21
|
+
# same length and no nulls
|
|
22
|
+
# anything else RObject of the values as: :rows returns
|
|
23
|
+
module NumoColumns
|
|
24
|
+
# Whether the host is little-endian, so PLAIN bytes (always little-endian) can be handed to
|
|
25
|
+
# Numo's from_binary, which reads native byte order.
|
|
26
|
+
LITTLE_ENDIAN = [1].pack("S") == [1].pack("v")
|
|
27
|
+
# Shorthand for Format::Type.
|
|
28
|
+
T = Format::Type
|
|
29
|
+
|
|
30
|
+
@loaded = false
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
|
|
33
|
+
# How a top-level field becomes a Numo array.
|
|
34
|
+
# kind: :fixed (a numeric or boolean leaf), :list (list of numbers) or :object
|
|
35
|
+
# klass: the Numo class of the result without nulls
|
|
36
|
+
# bin_klass: for :fixed leaves whose pages can be decoded straight into Numo, the class
|
|
37
|
+
# of the decoded values (same width as the physical type), else nil
|
|
38
|
+
# arrays: whether values may be Arrays (list fields)
|
|
39
|
+
Spec = Struct.new(:kind, :klass, :bin_klass, :arrays) do
|
|
40
|
+
# @return [Boolean] whether pages can be decoded straight into Numo (via NumoCursor)
|
|
41
|
+
def fast? = !bin_klass.nil?
|
|
42
|
+
|
|
43
|
+
# @return [Boolean] whether the result class is a float type, which holds nulls as NaN in place
|
|
44
|
+
def float? = klass == Numo::SFloat || klass == Numo::DFloat
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
# Requires Numo once per process (thread-safe); a no-op after the first success.
|
|
50
|
+
# @return [void]
|
|
51
|
+
# @raise [UnsupportedError] if neither numo-narray-alt nor numo-narray can be loaded
|
|
52
|
+
def load!
|
|
53
|
+
return if @loaded
|
|
54
|
+
@mutex.synchronize do
|
|
55
|
+
next if @loaded
|
|
56
|
+
begin
|
|
57
|
+
require_library
|
|
58
|
+
rescue LoadError => e
|
|
59
|
+
raise UnsupportedError, "as: :numo needs the \"numo-narray-alt\" gem (or \"numo-narray\"), " \
|
|
60
|
+
"which could not be loaded (#{e.message}). Add `gem \"numo-narray-alt\"` to your Gemfile " \
|
|
61
|
+
"to read columns into Numo arrays."
|
|
62
|
+
end
|
|
63
|
+
@loaded = true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Separate from load! so tests can stub it to simulate a missing gem.
|
|
68
|
+
# @return [Boolean] true if the library was loaded by this call
|
|
69
|
+
# @raise [LoadError] if "numo/narray" is not installed
|
|
70
|
+
def require_library
|
|
71
|
+
require "numo/narray"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Picks how +field+ is converted: a numeric or boolean leaf, a list of numbers with a single
|
|
75
|
+
# repetition level, or anything else as RObject.
|
|
76
|
+
# @param field [Schema::Field] top-level field of the read schema
|
|
77
|
+
# @return [Spec] how to build the field's Numo array
|
|
78
|
+
def spec_for(field)
|
|
79
|
+
if field.leaf?
|
|
80
|
+
klass, bin = leaf_classes(field.column)
|
|
81
|
+
return Spec.new(:fixed, klass, bin) if klass
|
|
82
|
+
elsif field.kind == :list && field.element.leaf? && field.element.column.max_repetition_level == 1
|
|
83
|
+
klass, = leaf_classes(field.element.column)
|
|
84
|
+
return Spec.new(:list, klass, nil) if klass && klass != Numo::Bit
|
|
85
|
+
end
|
|
86
|
+
Spec.new(:object, Numo::RObject, nil, field.kind == :list)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# [Numo class, class to decode pages into (nil: convert Ruby values)], or nil for columns
|
|
90
|
+
# that become RObject
|
|
91
|
+
# @param column [Schema::Column] leaf column
|
|
92
|
+
# @return [Array(Class, Class), Array(Class, nil), nil] result class and page decode class, or nil
|
|
93
|
+
def leaf_classes(column)
|
|
94
|
+
kind, bits, signed = Types.logical_of(column.node)
|
|
95
|
+
case column.type
|
|
96
|
+
when T::BOOLEAN
|
|
97
|
+
[Numo::Bit, Numo::Bit] if kind.nil?
|
|
98
|
+
when T::INT32
|
|
99
|
+
case kind
|
|
100
|
+
when nil, :time then [Numo::Int32, Numo::Int32]
|
|
101
|
+
when :integer
|
|
102
|
+
if signed
|
|
103
|
+
[{8 => Numo::Int8, 16 => Numo::Int16}.fetch(bits, Numo::Int32), Numo::Int32]
|
|
104
|
+
else
|
|
105
|
+
[{8 => Numo::UInt8, 16 => Numo::UInt16}.fetch(bits, Numo::UInt32), Numo::UInt32]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
when T::INT64
|
|
109
|
+
case kind
|
|
110
|
+
when nil, :time then [Numo::Int64, Numo::Int64]
|
|
111
|
+
when :integer then signed ? [Numo::Int64, Numo::Int64] : [Numo::UInt64, Numo::UInt64]
|
|
112
|
+
end
|
|
113
|
+
when T::FLOAT then [Numo::SFloat, Numo::SFloat] if kind.nil?
|
|
114
|
+
when T::DOUBLE then [Numo::DFloat, Numo::DFloat] if kind.nil?
|
|
115
|
+
when T::FIXED_LEN_BYTE_ARRAY then [Numo::SFloat, nil] if kind == :float16
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The result for a field read through Numo cursors: +parts+ are [values, validity] pairs
|
|
120
|
+
# (validity is a Numo::Bit, or nil when every value is present)
|
|
121
|
+
# Booleans with nulls become an RObject of true/false/nil, other types with nulls a float
|
|
122
|
+
# array with NaN in the null slots.
|
|
123
|
+
# @param spec [Spec] field spec from spec_for
|
|
124
|
+
# @param parts [Array<Array(Numo::NArray, Numo::Bit)>] one [values, validity] pair per batch
|
|
125
|
+
# @return [Numo::NArray] the whole column
|
|
126
|
+
def finish_fixed(spec, parts)
|
|
127
|
+
return spec.klass.new(0) if parts.empty?
|
|
128
|
+
full = (parts.size == 1) ? parts[0][0] : Numo::NArray.concatenate(parts.map(&:first))
|
|
129
|
+
valid = nil
|
|
130
|
+
if parts.any? { |_, v| v }
|
|
131
|
+
valid = (parts.size == 1) ? parts[0][1] : Numo::NArray.concatenate(parts.map { |f, v| v || Numo::Bit.ones(f.size) })
|
|
132
|
+
valid = nil if valid.count_false.zero?
|
|
133
|
+
end
|
|
134
|
+
unless valid
|
|
135
|
+
return full if full.instance_of?(spec.klass)
|
|
136
|
+
return spec.klass.cast(full)
|
|
137
|
+
end
|
|
138
|
+
if spec.klass == Numo::Bit
|
|
139
|
+
bits = full.to_a
|
|
140
|
+
present = valid.to_a
|
|
141
|
+
return robject(Array.new(bits.size) { |i| (present[i] == 1) ? bits[i] == 1 : nil })
|
|
142
|
+
end
|
|
143
|
+
out_class = spec.float? ? spec.klass : Numo::DFloat
|
|
144
|
+
out = full.instance_of?(out_class) ? full.dup : out_class.cast(full) # never write into a view
|
|
145
|
+
out[(~valid).where] = Float::NAN
|
|
146
|
+
out
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The result for a field read as Ruby values (+parts+ are Arrays of values)
|
|
150
|
+
# @param spec [Spec] field spec from spec_for
|
|
151
|
+
# @param parts [Array<Array>] the field's values, one Array per batch
|
|
152
|
+
# @return [Numo::NArray] the whole column
|
|
153
|
+
def finish_values(spec, parts)
|
|
154
|
+
values = (parts.size == 1) ? parts[0] : parts.flatten(1)
|
|
155
|
+
case spec.kind
|
|
156
|
+
when :object then robject(values, arrays: spec.arrays)
|
|
157
|
+
when :list then list_array(spec.klass, values)
|
|
158
|
+
else from_values(spec.klass, values)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# A numeric / boolean leaf's Ruby values as a Numo array
|
|
163
|
+
# @param klass [Class] Numo class for the column without nulls
|
|
164
|
+
# @param values [Array<Numeric, Boolean, nil>] the column's values, nil for nulls
|
|
165
|
+
# @return [Numo::NArray] +klass+ without nulls; SFloat/DFloat with NaN (or RObject for booleans) with nulls
|
|
166
|
+
def from_values(klass, values)
|
|
167
|
+
if values.include?(nil)
|
|
168
|
+
return robject(values) if klass == Numo::Bit
|
|
169
|
+
filled = values.map { |v| v.nil? ? Float::NAN : v }
|
|
170
|
+
return ((klass == Numo::SFloat) ? Numo::SFloat : Numo::DFloat).cast(filled)
|
|
171
|
+
end
|
|
172
|
+
return klass.new(0) if values.empty?
|
|
173
|
+
return Numo::Bit.cast(values.map { |v| v ? 1 : 0 }) if klass == Numo::Bit
|
|
174
|
+
klass.cast(values)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Lists of numbers: 2-D [rows, length] when every row is a list of the same, non-zero
|
|
178
|
+
# length without nulls; otherwise an RObject of the Arrays
|
|
179
|
+
# @param klass [Class] Numo class of the list elements
|
|
180
|
+
# @param values [Array<Array<Numeric, nil>, nil>] one list (or nil) per row
|
|
181
|
+
# @return [Numo::NArray] a 2-D +klass+ array or a 1-D Numo::RObject
|
|
182
|
+
def list_array(klass, values)
|
|
183
|
+
first = values.first
|
|
184
|
+
width = first.is_a?(Array) ? first.size : 0
|
|
185
|
+
if width.positive? && values.all? { |v| v.is_a?(Array) && v.size == width && !v.include?(nil) }
|
|
186
|
+
return klass.cast(values)
|
|
187
|
+
end
|
|
188
|
+
robject(values, arrays: true)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# A 1-D Numo::RObject holding +values+ as they are. Only list columns hold Arrays, which
|
|
192
|
+
# #store and .cast would turn into more dimensions.
|
|
193
|
+
# @param values [Array] one Ruby value per row
|
|
194
|
+
# @param arrays [Boolean] whether +values+ may contain Arrays that must stay single elements
|
|
195
|
+
# @return [Numo::RObject] 1-D array of +values+
|
|
196
|
+
def robject(values, arrays: false)
|
|
197
|
+
return Numo::RObject.new(values.size).seq.map { |i| values[i] } if arrays
|
|
198
|
+
out = Numo::RObject.new(values.size)
|
|
199
|
+
out.store(values)
|
|
200
|
+
out
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Width => Numo vector of 1 << j for each bit j, built lazily; dotting a bit matrix with it
|
|
204
|
+
# turns rows of bits into integers. Int64 above 30 bits so the sums cannot overflow.
|
|
205
|
+
POWERS = Hash.new { |h, w| h[w] = ((w > 30) ? Numo::Int64 : Numo::Int32).cast(Array.new(w) { |j| 1 << j }) }
|
|
206
|
+
|
|
207
|
+
# +count+ bit-packed values of +width+ bits (LSB first) from +data+ at +pos+ as a Numo
|
|
208
|
+
# array, without a Ruby object per value
|
|
209
|
+
# @param data [String] binary page data
|
|
210
|
+
# @param pos [Integer] byte offset of the first packed value
|
|
211
|
+
# @param count [Integer] number of values, a multiple of 8
|
|
212
|
+
# @param width [Integer] bits per value
|
|
213
|
+
# @return [Numo::NArray] unsigned integer array of +count+ values (element type depends on +width+)
|
|
214
|
+
def unpack_bits(data, pos, count, width)
|
|
215
|
+
return Numo::Int32.zeros(count) if width.zero?
|
|
216
|
+
nbytes = count * width / 8 # count is a multiple of 8
|
|
217
|
+
bytes = data.byteslice(pos, nbytes) || "".b
|
|
218
|
+
bytes += "\0" * (nbytes - bytes.bytesize) if bytes.bytesize < nbytes # truncated last run
|
|
219
|
+
case width
|
|
220
|
+
when 1 then return Numo::UInt8.cast(Numo::Bit.from_binary(bytes, [count]))
|
|
221
|
+
when 8 then return Numo::UInt8.from_binary(bytes)
|
|
222
|
+
when 16 then return Numo::UInt16.from_binary(bytes) if LITTLE_ENDIAN
|
|
223
|
+
when 32 then return Numo::UInt32.from_binary(bytes) if LITTLE_ENDIAN
|
|
224
|
+
end
|
|
225
|
+
# Row i of the [count, width] bit matrix holds value i's bits, least significant first
|
|
226
|
+
bits = Numo::Bit.from_binary(bytes, [count * width]).reshape(count, width)
|
|
227
|
+
pow = POWERS[width]
|
|
228
|
+
pow.class.cast(bits).dot(pow)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Hands out the next +k+ rows of a flat (non-repeated) numeric or boolean column as a
|
|
233
|
+
# [Numo values, validity] pair: values has one slot per row (zero where the row is null),
|
|
234
|
+
# validity is a Numo::Bit or nil when all rows are present.
|
|
235
|
+
class NumoCursor
|
|
236
|
+
# @param chunk_reader [ColumnChunkReader] reader for the column's chunk in one row group
|
|
237
|
+
# @param spec [NumoColumns::Spec] a :fixed spec with a bin_klass
|
|
238
|
+
def initialize(chunk_reader, spec)
|
|
239
|
+
@src = chunk_reader
|
|
240
|
+
col = chunk_reader.column
|
|
241
|
+
@path = col.dotted_path
|
|
242
|
+
@max_def = col.max_definition_level
|
|
243
|
+
@klass = spec.bin_klass
|
|
244
|
+
@bytes = NumoColumns::LITTLE_ENDIAN && @klass != Numo::Bit
|
|
245
|
+
@dictionaries = {}.compare_by_identity
|
|
246
|
+
@page = nil
|
|
247
|
+
@row = 0
|
|
248
|
+
@page_idx = -1
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# @return [Integer] index within the chunk of the next row #take returns
|
|
252
|
+
attr_reader :row
|
|
253
|
+
|
|
254
|
+
# Moves forward to row +target+ of the chunk, jumping over pages with the OffsetIndex
|
|
255
|
+
# @param target [Integer] row index within the chunk, not before #row
|
|
256
|
+
# @return [void]
|
|
257
|
+
# @raise [ArgumentError] if +target+ is before the current row
|
|
258
|
+
# @raise [FormatError] if the chunk runs out of pages
|
|
259
|
+
def seek(target)
|
|
260
|
+
raise ArgumentError, "Cannot seek backwards (at row #{@row}, asked for #{target})" if target < @row
|
|
261
|
+
return if target == @row
|
|
262
|
+
locs = @src.locations
|
|
263
|
+
if locs
|
|
264
|
+
j = locs.bsearch_index { |loc| loc.first_row_index > target }
|
|
265
|
+
j = (j || locs.size) - 1
|
|
266
|
+
if j > @page_idx
|
|
267
|
+
@src.jump_to_page(j)
|
|
268
|
+
@page = nil
|
|
269
|
+
@page_idx = j - 1
|
|
270
|
+
@row = locs[j].first_row_index
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
k = target - @row
|
|
274
|
+
while k > 0
|
|
275
|
+
load_page! while @page.nil? || @page.remaining.zero?
|
|
276
|
+
t = @page.remaining
|
|
277
|
+
t = k if k < t
|
|
278
|
+
defs, = @page.read_levels(t)
|
|
279
|
+
@page.skip_values(defs ? defs.count(@max_def) : t)
|
|
280
|
+
k -= t
|
|
281
|
+
end
|
|
282
|
+
@row = target
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Reads the next +k+ rows, crossing page boundaries as needed.
|
|
286
|
+
# @param k [Integer] number of rows
|
|
287
|
+
# @return [Array(Numo::NArray, Numo::Bit), Array(Numo::NArray, nil)] values (zero in null slots)
|
|
288
|
+
# and validity, nil when all +k+ rows are present
|
|
289
|
+
# @raise [FormatError] if the chunk runs out of pages
|
|
290
|
+
def take(k)
|
|
291
|
+
parts = []
|
|
292
|
+
nulls = false
|
|
293
|
+
@row += k
|
|
294
|
+
while k > 0
|
|
295
|
+
load_page! while @page.nil? || @page.remaining.zero?
|
|
296
|
+
t = @page.remaining
|
|
297
|
+
t = k if k < t
|
|
298
|
+
nv = t
|
|
299
|
+
if (valid = @page.read_validity_numo(t, @max_def))
|
|
300
|
+
nv = valid.count_true
|
|
301
|
+
end
|
|
302
|
+
dense = values(nv)
|
|
303
|
+
if nv < t
|
|
304
|
+
full = @klass.zeros(t)
|
|
305
|
+
full[valid.where] = dense if nv.positive?
|
|
306
|
+
nulls = true
|
|
307
|
+
else
|
|
308
|
+
full = dense
|
|
309
|
+
valid = nil
|
|
310
|
+
end
|
|
311
|
+
parts << [full, valid]
|
|
312
|
+
k -= t
|
|
313
|
+
end
|
|
314
|
+
return parts.first if parts.size == 1
|
|
315
|
+
full = Numo::NArray.concatenate(parts.map(&:first))
|
|
316
|
+
return [full, nil] unless nulls
|
|
317
|
+
[full, Numo::NArray.concatenate(parts.map { |f, v| v || Numo::Bit.ones(f.size) })]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
# Decodes the current page's next +n+ non-null values into a +@klass+ array, by the cheapest
|
|
323
|
+
# route the value decoder supports: raw bytes, dictionary indices, its own Numo decoding, or
|
|
324
|
+
# Ruby values as a last resort.
|
|
325
|
+
# @param n [Integer] number of non-null values
|
|
326
|
+
# @return [Numo::NArray] +n+ values, never a view into a cached dictionary
|
|
327
|
+
def values(n)
|
|
328
|
+
return @klass.new(0) if n.zero?
|
|
329
|
+
dec = @page.value_decoder
|
|
330
|
+
if @bytes && dec.respond_to?(:read_bytes)
|
|
331
|
+
@klass.from_binary(dec.read_bytes(n))
|
|
332
|
+
elsif dec.is_a?(PageStream::DictionaryDecoder)
|
|
333
|
+
dict = @dictionaries[dec.dictionary] ||= cast(dec.dictionary)
|
|
334
|
+
dict[dec.read_indices_numo(n)].dup # a copy, not a view the caller could write through
|
|
335
|
+
elsif dec.respond_to?(:read_numo)
|
|
336
|
+
dec.read_numo(n)
|
|
337
|
+
else
|
|
338
|
+
vals = @page.read_values(n)
|
|
339
|
+
conv = @page.converter
|
|
340
|
+
cast(conv ? vals.map!(&conv) : vals)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# @param values [Array<Numeric, Boolean>] decoded values, without nulls
|
|
345
|
+
# @return [Numo::NArray] +values+ as a +@klass+ array (booleans as 0/1 bits)
|
|
346
|
+
def cast(values)
|
|
347
|
+
if @klass == Numo::Bit
|
|
348
|
+
Numo::Bit.cast(values.map { |v| v ? 1 : 0 })
|
|
349
|
+
else
|
|
350
|
+
@klass.cast(values)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Advances to the chunk's next data page.
|
|
355
|
+
# @return [void]
|
|
356
|
+
# @raise [FormatError] if there are no more pages
|
|
357
|
+
def load_page!
|
|
358
|
+
@page = @src.next_stream
|
|
359
|
+
@page_idx += 1
|
|
360
|
+
return if @page
|
|
361
|
+
raise FormatError, "Column #{@path}: ran out of pages after #{@src.seen} of #{@src.total} values"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|