herringbone 0.2.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.
@@ -5,6 +5,8 @@ module Herringbone
5
5
  # It still carries an "experimental" warning, which is silenced once here: the gem only uses
6
6
  # new/for/copy/get_string/free, and falls back to String operations where it is missing.
7
7
  module IOBufferSupport
8
+ # true when IO::Buffer has the methods the decompressors use and HERRINGBONE_NO_IO_BUFFER is
9
+ # not set in the environment (which forces the String fallback)
8
10
  AVAILABLE = begin
9
11
  if defined?(IO::Buffer) && IO::Buffer.method_defined?(:copy) && IO::Buffer.method_defined?(:get_string)
10
12
  previous = Warning[:experimental]
@@ -18,7 +20,7 @@ module Herringbone
18
20
  else
19
21
  false
20
22
  end
21
- rescue StandardError
23
+ rescue
22
24
  false
23
25
  end
24
26
  end
@@ -14,22 +14,35 @@ module Herringbone
14
14
  # The declared total_compressed_size of the chunk is not relied on (some old writers
15
15
  # under-report it): pages are read until the chunk's num_values have been seen.
16
16
  class ColumnChunkReader
17
+ # Shorthand for the physical type constants
17
18
  T = Format::Type
19
+ # Shorthand for the encoding constants
18
20
  E = Format::Encoding
19
21
 
20
22
  # Bytes read past a page body, so that the next page header usually needs no extra read
21
23
  READ_AHEAD = 64 * 1024
24
+ # Bytes read for a page header whose size is not known; grown 4x while decoding fails
22
25
  HEADER_GUESS = 1024
26
+ # Largest header read attempted before a page header is declared corrupt
23
27
  MAX_HEADER = 64 * 1024 * 1024
24
28
 
29
+ # @return [Schema::Column] the leaf column this chunk belongs to
25
30
  attr_reader :column
26
31
 
27
32
  # With +lazy+, values of data pages that are not dictionary-encoded are returned as
28
33
  # physical values, and #page_converter is what still has to be applied to them. This keeps
29
34
  # a decoded page small (Integers instead of Time or BigDecimal objects) when only a slice
30
35
  # of it is needed at a time.
36
+ #
37
+ # @return [Proc, nil] converter of the last page returned by #next_page, nil when none
31
38
  attr_reader :page_converter
32
39
 
40
+ # @param io [IO, StringIO] the file, read with #seek and #read
41
+ # @param chunk [Format::ColumnChunk] the chunk's footer entry
42
+ # @param column [Schema::Column] the leaf column the chunk stores
43
+ # @param converter [Proc, nil] physical value => Ruby value, applied to values and dictionaries
44
+ # @param lazy [Boolean] leave non-dictionary values physical in #next_page (see #page_converter)
45
+ # @raise [UnsupportedError] for chunks without metadata (encrypted) or stored in another file
33
46
  def initialize(io, chunk, column, converter: column.converter, lazy: false)
34
47
  @io = io
35
48
  @chunk = chunk
@@ -43,7 +56,7 @@ module Herringbone
43
56
  start = @meta.data_page_offset
44
57
  dict = @meta.dictionary_page_offset
45
58
  # Some writers store 0 when there is no dictionary page
46
- start = dict if dict && dict.positive? && dict < start
59
+ start = dict if dict&.positive? && dict < start
47
60
  @pos = start
48
61
  @start = start
49
62
  @locations = nil # OffsetIndex page locations, when jumping between pages
@@ -55,6 +68,10 @@ module Herringbone
55
68
  end
56
69
 
57
70
  # All pages concatenated: [definition_levels, repetition_levels, values]
71
+ #
72
+ # @return [Array(Array<Integer>, Array<Integer>, Array)] levels are nil when the column's
73
+ # max level is 0
74
+ # @raise [FormatError] when a page is corrupt or overruns the file
58
75
  def read
59
76
  defs = @max_def.positive? ? [] : nil
60
77
  reps = @max_rep.positive? ? [] : nil
@@ -69,6 +86,10 @@ module Herringbone
69
86
  end
70
87
 
71
88
  # The next data page as [defs, reps, values], or nil after the last one
89
+ #
90
+ # @return [Array(Array<Integer>, Array<Integer>, Array), nil] levels are nil when the
91
+ # column's max level is 0; values hold the non-null entries
92
+ # @raise [FormatError] when a page is corrupt or overruns the file
72
93
  def next_page
73
94
  page = next_stream or return nil
74
95
  n = page.remaining
@@ -89,6 +110,11 @@ module Herringbone
89
110
 
90
111
  # The next data page as a PageStream::Page that decodes its levels and values on demand,
91
112
  # or nil after the last one. Values come out physical; apply Page#converter to them.
113
+ # Dictionary pages are read on the way.
114
+ #
115
+ # @return [PageStream::Page, nil] the next data page
116
+ # @raise [FormatError] when a page header is corrupt or a page overruns the file
117
+ # @raise [UnsupportedError] for an unsupported encoding or compression codec
92
118
  def next_stream
93
119
  while more_pages?
94
120
  header, body = read_page
@@ -112,11 +138,17 @@ module Herringbone
112
138
  raise FormatError, "Corrupt page header in #{@column.dotted_path}: #{e.message}"
113
139
  end
114
140
 
115
- # The chunk's data page locations from its OffsetIndex (enables #jump_to_page)
141
+ # @return [Array<Format::PageLocation>, nil] the chunk's data page locations from its
142
+ # OffsetIndex (enables #jump_to_page)
116
143
  attr_accessor :locations
117
144
 
118
145
  # Continues reading at data page +index+ of the OffsetIndex. The dictionary page (which
119
146
  # the OffsetIndex does not list) is read first if it has not been yet.
147
+ #
148
+ # @param index [Integer] position of the page in #locations
149
+ # @return [void]
150
+ # @raise [ArgumentError] when no #locations are set
151
+ # @raise [IndexError] when +index+ is outside #locations
120
152
  def jump_to_page(index)
121
153
  raise ArgumentError, "No OffsetIndex for #{@column.dotted_path}" unless @locations
122
154
  load_dictionary
@@ -125,18 +157,29 @@ module Herringbone
125
157
  end
126
158
 
127
159
  # Whether all of the chunk's values have been returned
160
+ #
161
+ # @return [Boolean] true once num_values entries have been seen
128
162
  def done? = @seen >= @total
129
163
 
130
- def seen = @seen
131
- def total = @total
164
+ # @return [Integer] entries (levels, nulls included) of the data pages returned so far
165
+ attr_reader :seen
166
+
167
+ # @return [Integer] the chunk's num_values from its ColumnMetaData
168
+ attr_reader :total
132
169
 
133
170
  private
134
171
 
172
+ # Whether another data page is due: up to the last OffsetIndex location after a jump,
173
+ # otherwise until num_values entries have been seen
174
+ #
175
+ # @return [Boolean] true when #next_stream should read on
135
176
  def more_pages?
136
177
  @page_number ? @page_number < @locations.size : @seen < @total
137
178
  end
138
179
 
139
180
  # Reads the dictionary page at the start of the chunk, if there is one
181
+ #
182
+ # @return [void]
140
183
  def load_dictionary
141
184
  return if @dictionary || @dictionary_checked
142
185
  @dictionary_checked = true
@@ -150,6 +193,9 @@ module Herringbone
150
193
  end
151
194
 
152
195
  # Reads the page header at @pos and the page body after it
196
+ #
197
+ # @return [Array(Format::PageHeader, String)] the header and the (still compressed) body
198
+ # @raise [FormatError] for a negative page size or a page that overruns the file
153
199
  def read_page
154
200
  # With an OffsetIndex the page's size (header included) is known, so read exactly that
155
201
  loc = @page_number && @locations[@page_number]
@@ -171,13 +217,18 @@ module Herringbone
171
217
  if buf.bytesize - off < size
172
218
  raise FormatError, "Column #{@column.dotted_path}: page overruns the file (read #{@seen} of #{@total} values)"
173
219
  end
174
- body = off.zero? && buf.bytesize == size ? buf : buf.byteslice(off, size)
220
+ body = (off.zero? && buf.bytesize == size) ? buf : buf.byteslice(off, size)
175
221
  @pos += hlen + size
176
222
  [header, body]
177
223
  end
178
224
 
179
225
  # Returns [buffer, offset] where buffer[offset..] holds at least +need+ bytes from file
180
226
  # position +pos+ (fewer only at EOF), reading +len+ bytes when the buffer does not cover it.
227
+ #
228
+ # @param pos [Integer] file offset wanted
229
+ # @param len [Integer] bytes to read when the buffer has to be refilled
230
+ # @param need [Integer] bytes that must be available from +pos+ to reuse the buffer
231
+ # @return [Array(String, Integer)] binary buffer and the offset of +pos+ in it
181
232
  def window(pos, len, need = len)
182
233
  off = pos - @buf_pos
183
234
  return [@buf, off] if off >= 0 && off + need <= @buf.bytesize
@@ -188,12 +239,23 @@ module Herringbone
188
239
  [@buf, 0]
189
240
  end
190
241
 
242
+ # Decompresses a page body with the chunk's codec
243
+ #
244
+ # @param body [String] compressed bytes
245
+ # @param size [Integer] uncompressed size from the page header
246
+ # @return [String] uncompressed bytes
247
+ # @raise [UnsupportedError] for an unsupported codec, naming the column
191
248
  def decompress(body, size)
192
249
  Compression.decompress(@meta.codec, body, size)
193
250
  rescue UnsupportedError => e
194
251
  raise e, "#{e.message} (column #{@column.dotted_path})"
195
252
  end
196
253
 
254
+ # Decodes a dictionary page (always PLAIN) and keeps its converted values
255
+ #
256
+ # @param header [Format::PageHeader] the dictionary page header
257
+ # @param body [String] the compressed page body
258
+ # @return [Array] the dictionary values
197
259
  def read_dictionary(header, body)
198
260
  dh = header.dictionary_page_header
199
261
  data = decompress(body, header.uncompressed_page_size)
@@ -202,6 +264,11 @@ module Herringbone
202
264
  @dictionary = vals
203
265
  end
204
266
 
267
+ # A DATA_PAGE: the whole body is compressed, levels come first with their own length prefix
268
+ #
269
+ # @param header [Format::PageHeader] the data page header
270
+ # @param body [String] the compressed page body
271
+ # @return [PageStream::Page] the page, decoded on demand
205
272
  def data_page_v1(header, body)
206
273
  dh = header.data_page_header
207
274
  n = dh.num_values
@@ -214,6 +281,11 @@ module Herringbone
214
281
  PageStream::Page.new(n, defs, reps, values, conv)
215
282
  end
216
283
 
284
+ # A DATA_PAGE_V2: levels are stored uncompressed before the (optionally compressed) values
285
+ #
286
+ # @param header [Format::PageHeader] the data page header
287
+ # @param body [String] the page body
288
+ # @return [PageStream::Page] the page, decoded on demand
217
289
  def data_page_v2(header, body)
218
290
  dh = header.data_page_header_v2
219
291
  n = dh.num_values
@@ -230,9 +302,20 @@ module Herringbone
230
302
  PageStream::Page.new(n, defs, reps, values, conv)
231
303
  end
232
304
 
305
+ # Bit width of levels up to a max level (memoized Integer#bit_length)
233
306
  RLE_WIDTH = Hash.new { |h, k| h[k] = k.bit_length }
234
307
 
235
308
  # [decoder, position after the levels]
309
+ #
310
+ # @param data [String] the uncompressed page
311
+ # @param pos [Integer] offset of the levels in +data+
312
+ # @param encoding [Integer] Format::Encoding of the levels (RLE or BIT_PACKED)
313
+ # @param max [Integer] max level of the column, which sets the bit width
314
+ # @param n [Integer] number of entries in the page
315
+ # @return [Array(PageStream::HybridDecoder, Integer), Array(PageStream::ArrayDecoder, Integer)]
316
+ # the decoder and the offset just past the levels
317
+ # @raise [FormatError] when the RLE length prefix is cut off
318
+ # @raise [UnsupportedError] for any other level encoding
236
319
  def level_decoder(data, pos, encoding, max, n)
237
320
  width = RLE_WIDTH[max]
238
321
  case encoding
@@ -248,11 +331,20 @@ module Herringbone
248
331
  end
249
332
  end
250
333
 
334
+ # Physical type => [String#unpack directive, byte width] for PLAIN fixed-width values
251
335
  FIXED_FORMATS = {
252
336
  T::INT32 => ["l<", 4], T::INT64 => ["q<", 8], T::FLOAT => ["e", 4], T::DOUBLE => ["E", 8]
253
337
  }.freeze
254
338
 
255
339
  # [value decoder, converter still to apply to its values (nil for dictionary pages)]
340
+ #
341
+ # @param data [String] the uncompressed values section
342
+ # @param pos [Integer] offset of the values in +data+
343
+ # @param encoding [Integer] Format::Encoding of the values
344
+ # @return [Array(Object, Proc)] a PageStream decoder (responds to #read) and the converter,
345
+ # which may be nil
346
+ # @raise [FormatError] for a dictionary-encoded page in a chunk without a dictionary page
347
+ # @raise [UnsupportedError] for an encoding not valid for the column's type, or unknown
256
348
  def value_decoder(data, pos, encoding)
257
349
  type = @column.type
258
350
  decoder = case encoding
@@ -272,7 +364,7 @@ module Herringbone
272
364
  raise UnsupportedError, "RLE value encoding is only supported for BOOLEAN" unless type == T::BOOLEAN
273
365
  PageStream::RleBooleanDecoder.new(data, pos)
274
366
  when E::DELTA_BINARY_PACKED
275
- bits = type == T::INT32 ? 32 : 64
367
+ bits = (type == T::INT32) ? 32 : 64
276
368
  PageStream::ArrayDecoder.new(Encodings::Delta.decode_binary_packed(data, pos, bits).first)
277
369
  when E::DELTA_LENGTH_BYTE_ARRAY
278
370
  PageStream::DeltaLengthDecoder.new(data, pos)
@@ -11,6 +11,7 @@ module Herringbone
11
11
  # Levels decoded ahead at a time when looking for row starts in repeated columns
12
12
  LOOKAHEAD = 4096
13
13
 
14
+ # @param chunk_reader [ColumnChunkReader] reader of the chunk, positioned at its start
14
15
  def initialize(chunk_reader)
15
16
  @src = chunk_reader
16
17
  col = chunk_reader.column
@@ -26,12 +27,17 @@ module Herringbone
26
27
  @page_idx = -1 # index of the current data page within the chunk
27
28
  end
28
29
 
29
- # Rows handed out or skipped so far
30
+ # @return [Integer] rows handed out or skipped so far (the chunk row the cursor is at)
30
31
  attr_reader :row
31
32
 
32
33
  # Moves forward to row +target+ of the chunk (0-based). With an OffsetIndex, pages before
33
34
  # the one holding +target+ are not read at all; otherwise rows are skipped page by page,
34
35
  # decoding levels but not building values.
36
+ #
37
+ # @param target [Integer] row of the chunk to stop at
38
+ # @return [void]
39
+ # @raise [ArgumentError] when +target+ is before the current row
40
+ # @raise [FormatError] when the chunk runs out of pages before +target+
35
41
  def seek(target)
36
42
  raise ArgumentError, "Cannot seek backwards (at row #{@row}, asked for #{target})" if target < @row
37
43
  return if target == @row
@@ -53,6 +59,10 @@ module Herringbone
53
59
  end
54
60
 
55
61
  # Moves past the next +k+ rows without building their values
62
+ #
63
+ # @param k [Integer] number of rows to skip; zero or less does nothing
64
+ # @return [void]
65
+ # @raise [FormatError] when the chunk runs out of pages
56
66
  def skip(k)
57
67
  return if k <= 0
58
68
  @repeated ? take_repeated(k, false) : take_flat(k, false)
@@ -62,6 +72,11 @@ module Herringbone
62
72
 
63
73
  # [definition_levels, repetition_levels, values] of the next +k+ rows. Levels are nil
64
74
  # when the column's max level is 0.
75
+ #
76
+ # @param k [Integer] number of rows to hand out
77
+ # @return [Array(Array<Integer>, Array<Integer>, Array)] definition levels, repetition
78
+ # levels and the (converted) values of the non-null entries
79
+ # @raise [FormatError] when the chunk runs out of pages or does not start at a row boundary
65
80
  def take(k)
66
81
  pieces = @repeated ? take_repeated(k) : take_flat(k)
67
82
  @row += k
@@ -79,6 +94,13 @@ module Herringbone
79
94
 
80
95
  private
81
96
 
97
+ # Non-repeated columns, where every entry is a row: reads page by page until +k+ entries
98
+ #
99
+ # @param k [Integer] number of rows
100
+ # @param keep [Boolean] false to skip the values instead of decoding them
101
+ # @return [Array<Array(Array<Integer>, nil, Array)>] [defs, nil, values] per page touched;
102
+ # empty when +keep+ is false
103
+ # @raise [FormatError] when the chunk runs out of pages
82
104
  def take_flat(k, keep = true)
83
105
  pieces = []
84
106
  while k > 0
@@ -99,6 +121,12 @@ module Herringbone
99
121
 
100
122
  # Collects entries until +k+ rows have started and the next row start (or the end of the
101
123
  # column) is reached. Values are read from a page before moving on to the next one.
124
+ #
125
+ # @param k [Integer] number of rows
126
+ # @param keep [Boolean] false to skip the values instead of decoding them
127
+ # @return [Array<Array(Array<Integer>, Array<Integer>, Array)>] [defs, reps, values] per
128
+ # page touched (defs nil without definition levels); empty when +keep+ is false
129
+ # @raise [FormatError] when the first page does not start at a row boundary
102
130
  def take_repeated(k, keep = true)
103
131
  pieces = []
104
132
  rows = 0
@@ -106,7 +134,7 @@ module Herringbone
106
134
  reps = []
107
135
  while true
108
136
  if @bi >= @br.to_a.size
109
- if @page && @page.remaining.positive?
137
+ if @page&.remaining&.positive?
110
138
  @bd, @br = @page.read_levels(LOOKAHEAD)
111
139
  @bi = 0
112
140
  else
@@ -147,6 +175,12 @@ module Herringbone
147
175
  end
148
176
 
149
177
  # Reads (or skips) the values belonging to the collected entries of the current page
178
+ #
179
+ # @param pieces [Array<Array>] output list a [defs, reps, values] piece is appended to
180
+ # @param defs [Array<Integer>, nil] collected definition levels (nil without them)
181
+ # @param reps [Array<Integer>] collected repetition levels; nothing happens when empty
182
+ # @param keep [Boolean] false to skip the values instead of appending a piece
183
+ # @return [void]
150
184
  def flush(pieces, defs, reps, keep)
151
185
  return if reps.empty?
152
186
  nv = defs ? defs.count(@max_def) : reps.size
@@ -157,18 +191,28 @@ module Herringbone
157
191
  end
158
192
  end
159
193
 
194
+ # The next +n+ values of the current page, converted when the page has a converter
195
+ #
196
+ # @param n [Integer] number of values (non-null entries)
197
+ # @return [Array] Ruby values
160
198
  def values(n)
161
199
  vals = @page.read_values(n)
162
200
  conv = @page.converter
163
201
  conv ? vals.map!(&conv) : vals
164
202
  end
165
203
 
204
+ # Like #load_page, but running out of pages is an error
205
+ #
206
+ # @return [void]
207
+ # @raise [FormatError] when the chunk has no more data pages
166
208
  def load_page!
167
209
  return if load_page
168
210
  raise FormatError, "Column #{@path}: ran out of pages after #{@src.seen} of #{@src.total} values"
169
211
  end
170
212
 
171
213
  # Moves to the next data page; false at the end of the chunk
214
+ #
215
+ # @return [Boolean] whether a page was loaded
172
216
  def load_page
173
217
  @page = @src.next_stream or return false
174
218
  @page_idx += 1