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.
@@ -6,20 +6,38 @@ module Herringbone
6
6
  module Delta
7
7
  module_function
8
8
 
9
+ # Values per block written by the encoder (the size parquet-mr and Arrow use).
9
10
  BLOCK_SIZE = 128
11
+ # Miniblocks per block written by the encoder, so 32 values per miniblock.
10
12
  MINIBLOCKS = 4
11
13
 
14
+ # @param n [Integer] zigzag-encoded unsigned integer
15
+ # @return [Integer] the signed integer it represents
12
16
  def zigzag_decode(n) = (n >> 1) ^ -(n & 1)
17
+
18
+ # @param n [Integer] signed integer
19
+ # @return [Integer] zigzag-encoded form (0, -1, 1, -2 map to 0, 1, 2, 3)
13
20
  def zigzag_encode(n) = n.negative? ? ((-n) << 1) - 1 : n << 1
14
21
 
15
22
  # Wraps an integer into the signed range of +bits+ bits
23
+ # @param v [Integer] integer to wrap
24
+ # @param bits [Integer] target width, 32 or 64
25
+ # @return [Integer] +v+ modulo 2^bits, as a two's complement signed integer
16
26
  def wrap(v, bits)
17
27
  half = 1 << (bits - 1)
18
28
  ((v + half) & ((1 << bits) - 1)) - half
19
29
  end
20
30
 
21
31
  # Decodes DELTA_BINARY_PACKED integers. +bits+ is 32 or 64 (for wraparound).
22
- # Returns [values, new_pos]. If +count+ is nil, the total from the header is used.
32
+ # Returns [values, new_pos]. If +count+ is nil, the total from the header is used. A smaller
33
+ # +count+ decodes only that many values, but the returned offset is still the end of the
34
+ # whole encoded block, so data following it can be read from there.
35
+ # @param data [String] binary page data
36
+ # @param pos [Integer] byte offset of the block header
37
+ # @param bits [Integer] integer width that deltas wrap around in, 32 or 64
38
+ # @param count [Integer, nil] maximum number of values to decode
39
+ # @return [Array(Array<Integer>, Integer)] the decoded values and the offset just past the encoded block
40
+ # @raise [FormatError] on an invalid header or miniblock bit width, or truncated data
23
41
  def decode_binary_packed(data, pos, bits = 64, count = nil)
24
42
  block_size, pos = RLE.read_uleb(data, pos)
25
43
  miniblocks, pos = RLE.read_uleb(data, pos)
@@ -28,34 +46,44 @@ module Herringbone
28
46
  raise FormatError, "Invalid DELTA_BINARY_PACKED header" if miniblocks.zero? || block_size % miniblocks != 0
29
47
  per_mini = block_size / miniblocks
30
48
  raise FormatError, "Invalid miniblock size #{per_mini}" if per_mini % 8 != 0
31
- total = count if count && count < total
49
+ want = (count && count < total) ? count : total
32
50
  values = []
33
51
  return [values, pos] if total.zero?
34
52
  last = zigzag_decode(first)
35
- values << last
53
+ values << last if want.positive?
36
54
  half = 1 << (bits - 1)
37
55
  mask = (1 << bits) - 1
38
- while values.size < total
56
+ left = total - 1 # deltas still encoded, decoded or skipped
57
+ while left.positive?
39
58
  min_delta, pos = RLE.read_uleb(data, pos)
40
59
  min_delta = zigzag_decode(min_delta)
41
60
  widths = data.byteslice(pos, miniblocks).unpack("C*")
42
61
  pos += miniblocks
43
62
  widths.each do |w|
44
- break if values.size >= total
63
+ # Miniblocks past the last value have a width byte but no body
64
+ break unless left.positive?
45
65
  raise FormatError, "Invalid delta bit width #{w}" if w > bits
46
- deltas = RLE.unpack_bits(data, pos, per_mini, w)
47
- pos += per_mini * w / 8
48
- take = total - values.size
49
- deltas = deltas.first(take) if take < per_mini
50
- deltas.each do |d|
51
- last = ((last + min_delta + d + half) & mask) - half
52
- values << last
66
+ if values.size < want
67
+ deltas = RLE.unpack_bits(data, pos, per_mini, w)
68
+ take = want - values.size
69
+ deltas = deltas.first(take) if take < per_mini
70
+ deltas.each do |d|
71
+ last = ((last + min_delta + d + half) & mask) - half
72
+ values << last
73
+ end
53
74
  end
75
+ pos += per_mini * w / 8
76
+ left -= per_mini
54
77
  end
55
78
  end
56
79
  [values, pos]
57
80
  end
58
81
 
82
+ # Encodes integers with DELTA_BINARY_PACKED, using BLOCK_SIZE values per block in MINIBLOCKS
83
+ # miniblocks. Deltas wrap at +bits+ so INT32 columns never need more than 32-bit widths.
84
+ # @param values [Array<Integer>] signed integers that fit in +bits+ bits
85
+ # @param bits [Integer] physical integer width, 32 or 64
86
+ # @return [String] encoded bytes in ASCII-8BIT
59
87
  def encode_binary_packed(values, bits = 64)
60
88
  out = String.new(encoding: Encoding::BINARY)
61
89
  per_mini = BLOCK_SIZE / MINIBLOCKS
@@ -82,6 +110,12 @@ module Herringbone
82
110
  out
83
111
  end
84
112
 
113
+ # Decodes DELTA_LENGTH_BYTE_ARRAY: DELTA_BINARY_PACKED lengths followed by the concatenated bytes.
114
+ # @param data [String] binary page data
115
+ # @param pos [Integer] byte offset of the lengths block
116
+ # @param count [Integer] number of values to decode
117
+ # @return [Array(Array<String>, Integer)] binary slices of +data+ and the offset just past them
118
+ # @raise [FormatError] if there are too few lengths or a value runs past the end of +data+
85
119
  def decode_length_byte_array(data, pos, count)
86
120
  lengths, pos = decode_binary_packed(data, pos, 32, count)
87
121
  raise FormatError, "DELTA_LENGTH_BYTE_ARRAY has #{lengths.size} lengths, need #{count}" if lengths.size < count
@@ -95,22 +129,35 @@ module Herringbone
95
129
  [out, pos]
96
130
  end
97
131
 
132
+ # Encodes byte strings with DELTA_LENGTH_BYTE_ARRAY.
133
+ # @param values [Array<String>] byte strings, in any encoding
134
+ # @return [String] encoded bytes in ASCII-8BIT
98
135
  def encode_length_byte_array(values)
99
136
  encode_binary_packed(values.map(&:bytesize), 32) << values.pack("a*" * values.size)
100
137
  end
101
138
 
139
+ # Decodes DELTA_BYTE_ARRAY (incremental encoding): DELTA_BINARY_PACKED prefix lengths, then
140
+ # the suffixes as DELTA_LENGTH_BYTE_ARRAY. Each value is the previous value's prefix plus its suffix.
141
+ # @param data [String] binary page data
142
+ # @param pos [Integer] byte offset of the prefix lengths block
143
+ # @param count [Integer] number of values to decode
144
+ # @return [Array(Array<String>, Integer)] binary values and the offset just past them
145
+ # @raise [FormatError] if a prefix is negative or longer than the previous value, or the data is malformed
102
146
  def decode_byte_array(data, pos, count)
103
147
  prefixes, pos = decode_binary_packed(data, pos, 32, count)
104
148
  suffixes, pos = decode_length_byte_array(data, pos, count)
105
149
  prev = "".b
106
150
  out = Array.new(count) do |i|
107
151
  prefix = prefixes[i]
108
- raise FormatError, "DELTA_BYTE_ARRAY prefix longer than previous value" if prefix > prev.bytesize
152
+ raise FormatError, "DELTA_BYTE_ARRAY prefix length #{prefix} out of range" if prefix > prev.bytesize || prefix.negative?
109
153
  prev = prefix.zero? ? suffixes[i] : prev.byteslice(0, prefix) + suffixes[i]
110
154
  end
111
155
  [out, pos]
112
156
  end
113
157
 
158
+ # Encodes byte strings with DELTA_BYTE_ARRAY, sharing the common byte prefix with the previous value.
159
+ # @param values [Array<String>] byte strings, in any encoding
160
+ # @return [String] encoded bytes in ASCII-8BIT
114
161
  def encode_byte_array(values)
115
162
  prev = "".b
116
163
  prefixes = []
@@ -132,6 +179,12 @@ module Herringbone
132
179
  module_function
133
180
 
134
181
  # Returns the value bytes re-interleaved into PLAIN layout
182
+ # @param data [String] binary page data
183
+ # @param pos [Integer] byte offset of the first stream
184
+ # @param count [Integer] number of values
185
+ # @param width [Integer] byte width of one value
186
+ # @return [Array(String, Integer)] PLAIN-layout bytes and the offset just past the streams
187
+ # @raise [FormatError] if fewer than count * width bytes remain
135
188
  def decode(data, pos, count, width)
136
189
  nbytes = count * width
137
190
  raise FormatError, "Truncated BYTE_STREAM_SPLIT data" if pos + nbytes > data.bytesize
@@ -139,6 +192,10 @@ module Herringbone
139
192
  [streams[0].zip(*streams[1..]).flatten.pack("C*"), pos + nbytes]
140
193
  end
141
194
 
195
+ # Splits PLAIN-layout fixed-width values into +width+ byte streams. Inverse of decode.
196
+ # @param plain [String] PLAIN-encoded values, a multiple of +width+ bytes
197
+ # @param width [Integer] byte width of one value
198
+ # @return [String] the concatenated streams in ASCII-8BIT
142
199
  def encode(plain, width)
143
200
  count = plain.bytesize / width
144
201
  bytes = plain.unpack("C*")
@@ -7,6 +7,7 @@ module Herringbone
7
7
  module Plain
8
8
  module_function
9
9
 
10
+ # Fixed-width numeric types: pack/unpack directive and byte width (all little-endian).
10
11
  FORMATS = {
11
12
  Format::Type::INT32 => ["l<", 4],
12
13
  Format::Type::INT64 => ["q<", 8],
@@ -16,6 +17,14 @@ module Herringbone
16
17
 
17
18
  # Decodes +count+ values of +type+ from +data+ starting at +pos+.
18
19
  # Returns [values, new_pos].
20
+ # INT96 values come back as [nanoseconds_of_day, julian_day] pairs.
21
+ # @param data [String] binary page data
22
+ # @param pos [Integer] byte offset of the first value
23
+ # @param count [Integer] number of values to decode
24
+ # @param type [Integer] physical type, a Format::Type constant
25
+ # @param type_length [Integer, nil] byte width, required for FIXED_LEN_BYTE_ARRAY
26
+ # @return [Array(Array, Integer)] the decoded values and the offset just past them
27
+ # @raise [FormatError] if the data is truncated or the type is unknown
19
28
  def decode(data, pos, count, type, type_length = nil)
20
29
  case type
21
30
  when Format::Type::BOOLEAN
@@ -44,6 +53,12 @@ module Herringbone
44
53
  end
45
54
  end
46
55
 
56
+ # Decodes PLAIN BYTE_ARRAY values: each is a 4-byte little-endian length followed by the bytes.
57
+ # @param data [String] binary page data
58
+ # @param pos [Integer] byte offset of the first length prefix
59
+ # @param count [Integer] number of values to decode
60
+ # @return [Array(Array<String>, Integer)] binary slices of +data+ and the offset just past them
61
+ # @raise [FormatError] if a length prefix or value runs past the end of +data+
47
62
  def decode_byte_arrays(data, pos, count)
48
63
  out = Array.new(count)
49
64
  size = data.bytesize
@@ -60,6 +75,13 @@ module Herringbone
60
75
  [out, pos]
61
76
  end
62
77
 
78
+ # Encodes values of a physical type with PLAIN. Inverse of decode; INT96 values are
79
+ # [nanoseconds_of_day, julian_day] pairs and booleans are packed LSB-first, 8 per byte.
80
+ # @param values [Array] values in their physical Ruby form, without nulls
81
+ # @param type [Integer] physical type, a Format::Type constant
82
+ # @param type_length [Integer, nil] byte width, required for FIXED_LEN_BYTE_ARRAY
83
+ # @return [String] encoded bytes in ASCII-8BIT
84
+ # @raise [EncodeError] if a FIXED_LEN_BYTE_ARRAY value has the wrong size or the type is unknown
63
85
  def encode(values, type, type_length = nil)
64
86
  case type
65
87
  when Format::Type::BOOLEAN
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Herringbone
4
+ # Decoders and encoders for the Parquet value and level encodings: PLAIN, the RLE / bit-packed
5
+ # hybrid, the DELTA_* family and BYTE_STREAM_SPLIT. They work on binary Strings and byte offsets,
6
+ # and know nothing about pages or columns.
4
7
  module Encodings
5
8
  # Bit packing (LSB-first, as used by Parquet) and the RLE / bit-packed hybrid encoding
6
9
  # used for repetition/definition levels, dictionary indices and RLE booleans.
@@ -9,6 +12,11 @@ module Herringbone
9
12
 
10
13
  # Unpacks +count+ values of +width+ bits each, starting at byte +offset+ of +data+.
11
14
  # Missing trailing bytes are treated as zeroes.
15
+ # @param data [String] binary input
16
+ # @param offset [Integer] byte offset of the first packed value
17
+ # @param count [Integer] number of values to unpack
18
+ # @param width [Integer] bits per value, 0..64
19
+ # @return [Array<Integer>] +count+ unsigned values
12
20
  def unpack_bits(data, offset, count, width)
13
21
  return Array.new(count, 0) if width.zero?
14
22
  nbytes = (count * width + 7) / 8
@@ -22,7 +30,7 @@ module Herringbone
22
30
 
23
31
  # Pad to a whole number of 32-bit words plus one spare word, so reads never go out of range
24
32
  pad = (-chunk.bytesize % 4) + 4
25
- words = (pad.zero? ? chunk : chunk + ("\0" * pad)).unpack("V*")
33
+ words = (chunk + ("\0" * pad)).unpack("V*")
26
34
  mask = (1 << width) - 1
27
35
  out = Array.new(count)
28
36
  bitpos = 0
@@ -41,6 +49,10 @@ module Herringbone
41
49
  end
42
50
 
43
51
  # Slow path for widths above 32 bits (used by DELTA_BINARY_PACKED with 64-bit values)
52
+ # @param chunk [String] packed bytes, starting at the first value
53
+ # @param count [Integer] number of values to unpack
54
+ # @param width [Integer] bits per value, 33..64
55
+ # @return [Array<Integer>] +count+ unsigned values
44
56
  def unpack_wide(chunk, count, width)
45
57
  mask = (1 << width) - 1
46
58
  out = Array.new(count)
@@ -61,6 +73,9 @@ module Herringbone
61
73
  end
62
74
 
63
75
  # Packs +values+ with +width+ bits each; the value count is padded up to a multiple of 8.
76
+ # @param values [Array<Integer>] non-negative integers that fit in +width+ bits
77
+ # @param width [Integer] bits per value
78
+ # @return [String] packed bytes in ASCII-8BIT, +width+ bytes per group of 8 values
64
79
  def pack_bits(values, width)
65
80
  return "".b if width.zero? || values.empty?
66
81
  n = values.size
@@ -93,6 +108,11 @@ module Herringbone
93
108
  out
94
109
  end
95
110
 
111
+ # Reads an unsigned LEB128 varint (hybrid run headers, DELTA_BINARY_PACKED headers).
112
+ # @param data [String] binary input
113
+ # @param pos [Integer] byte offset of the varint
114
+ # @return [Array(Integer, Integer)] the decoded value and the offset just past it
115
+ # @raise [FormatError] if the input ends inside the varint
96
116
  def read_uleb(data, pos)
97
117
  result = 0
98
118
  shift = 0
@@ -106,6 +126,10 @@ module Herringbone
106
126
  end
107
127
  end
108
128
 
129
+ # Appends +n+ as an unsigned LEB128 varint.
130
+ # @param out [String] binary output buffer, appended to
131
+ # @param n [Integer] non-negative integer to encode
132
+ # @return [String] +out+
109
133
  def write_uleb(out, n)
110
134
  while n >= 0x80
111
135
  out << ((n & 0x7F) | 0x80)
@@ -116,6 +140,13 @@ module Herringbone
116
140
 
117
141
  # Decodes the RLE/bit-packed hybrid from +data+ between +pos+ and +limit+,
118
142
  # returning exactly +count+ values (missing values are an error).
143
+ # @param data [String] binary input
144
+ # @param pos [Integer] byte offset of the first run header
145
+ # @param limit [Integer] byte offset just past the encoded data
146
+ # @param width [Integer] bits per value
147
+ # @param count [Integer] number of values to decode
148
+ # @return [Array<Integer>] exactly +count+ values
149
+ # @raise [FormatError] if the runs end before +count+ values were produced
119
150
  def decode_hybrid(data, pos, limit, width, count)
120
151
  out = []
121
152
  value_bytes = (width + 7) / 8
@@ -145,6 +176,10 @@ module Herringbone
145
176
 
146
177
  # Encodes +values+ with the RLE/bit-packed hybrid. Repeated runs of 8+ equal values
147
178
  # become RLE runs, everything else goes into bit-packed groups of 8.
179
+ # Output has no length prefix; callers that need one (levels in data page v1) add it.
180
+ # @param values [Array<Integer>] non-negative integers that fit in +width+ bits
181
+ # @param width [Integer] bits per value
182
+ # @return [String] encoded runs in ASCII-8BIT
148
183
  def encode_hybrid(values, width)
149
184
  out = String.new(encoding: Encoding::BINARY)
150
185
  n = values.size
@@ -181,6 +216,13 @@ module Herringbone
181
216
  out
182
217
  end
183
218
 
219
+ # Appends values[from...to] as one bit-packed run, zero-padded to whole groups of 8.
220
+ # @param out [String] binary output buffer, appended to
221
+ # @param values [Array<Integer>] all values being encoded
222
+ # @param from [Integer] index of the first literal value
223
+ # @param to [Integer] index just past the last literal value
224
+ # @param width [Integer] bits per value
225
+ # @return [String] +out+
184
226
  def flush_literals(out, values, from, to, width)
185
227
  count = to - from
186
228
  groups = (count + 7) / 8
@@ -188,13 +230,23 @@ module Herringbone
188
230
  out << pack_bits(values[from, count], width)
189
231
  end
190
232
 
233
+ # Number of bits needed to store values up to +max_value+ (0 for 0).
234
+ # @param max_value [Integer, nil] largest value to encode; nil counts as 0
235
+ # @return [Integer] bit width
191
236
  def bit_width(max_value)
192
237
  max_value.to_i.bit_length
193
238
  end
194
239
 
195
240
  # Legacy BIT_PACKED level encoding (deprecated): MSB-first bit order, no header.
241
+ # @param data [String] binary input
242
+ # @param pos [Integer] byte offset of the packed levels
243
+ # @param width [Integer] bits per value
244
+ # @param count [Integer] number of values to decode
245
+ # @return [Array<Integer>] +count+ levels
246
+ # @raise [FormatError] if +data+ ends before +count+ levels
196
247
  def decode_legacy_bit_packed(data, pos, width, count)
197
248
  nbytes = (count * width + 7) / 8
249
+ raise FormatError, "Truncated BIT_PACKED levels" if pos + nbytes > data.bytesize
198
250
  bits = data.byteslice(pos, nbytes).unpack1("B*")
199
251
  Array.new(count) { |i| bits[i * width, width].to_i(2) }
200
252
  end