herringbone 0.6.0 → 0.7.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/CHANGELOG.md +40 -0
- data/MANUAL.md +161 -12
- data/README.md +19 -4
- data/bin/herringbone +56 -1
- data/lib/herringbone/active_record.rb +2 -0
- data/lib/herringbone/bloom_filter.rb +2 -0
- data/lib/herringbone/byte_values.rb +2 -0
- data/lib/herringbone/codecs/lz4.rb +9 -4
- data/lib/herringbone/codecs/lzo.rb +2 -0
- data/lib/herringbone/codecs/snappy.rb +48 -6
- data/lib/herringbone/combiner.rb +426 -0
- data/lib/herringbone/compression.rb +53 -9
- data/lib/herringbone/encodings/delta.rb +4 -0
- data/lib/herringbone/encodings/plain.rb +2 -0
- data/lib/herringbone/encodings/rle.rb +2 -0
- data/lib/herringbone/encryption.rb +2 -0
- data/lib/herringbone/inspector.rb +29 -5
- data/lib/herringbone/io_buffer_support.rb +2 -0
- data/lib/herringbone/reader/bloom_filters.rb +1 -0
- data/lib/herringbone/reader/chunk_copier.rb +177 -0
- data/lib/herringbone/reader/column_chunk_reader.rb +4 -2
- data/lib/herringbone/reader/column_cursor.rb +2 -0
- data/lib/herringbone/reader/numo.rb +4 -0
- data/lib/herringbone/reader/page_stream.rb +2 -0
- data/lib/herringbone/reader/scan.rb +2 -0
- data/lib/herringbone/reader.rb +19 -6
- data/lib/herringbone/redaction/rewriter.rb +51 -157
- data/lib/herringbone/redaction.rb +25 -15
- data/lib/herringbone/restricted_readable_io.rb +85 -0
- data/lib/herringbone/schema/merge.rb +355 -0
- data/lib/herringbone/schema.rb +80 -0
- data/lib/herringbone/thrift.rb +166 -68
- data/lib/herringbone/types.rb +2 -0
- data/lib/herringbone/version.rb +1 -1
- data/lib/herringbone/visualizer.rb +2 -0
- data/lib/herringbone/writer.rb +58 -22
- data/lib/herringbone/xxhash.rb +2 -0
- data/lib/herringbone.rb +96 -9
- metadata +6 -2
data/lib/herringbone/thrift.rb
CHANGED
|
@@ -4,6 +4,8 @@ module Herringbone
|
|
|
4
4
|
# Minimal Thrift Compact Protocol implementation, just enough for Parquet metadata.
|
|
5
5
|
# Structs are described declaratively (see Herringbone::Thrift::Struct) so that
|
|
6
6
|
# both the reader and the writer are driven by the same field tables.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
7
9
|
module Thrift
|
|
8
10
|
# Raised on malformed or truncated Thrift data, and on values the writer cannot encode
|
|
9
11
|
class Error < StandardError; end
|
|
@@ -45,6 +47,14 @@ module Herringbone
|
|
|
45
47
|
double: T_DOUBLE, binary: T_BINARY, string: T_BINARY
|
|
46
48
|
}.freeze
|
|
47
49
|
|
|
50
|
+
# Values each declared integer type can hold, whatever width it came in on the wire
|
|
51
|
+
INT_RANGES = {
|
|
52
|
+
byte: -2**7...2**7, i16: -2**15...2**15, i32: -2**31...2**31, i64: -2**63...2**63
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
# Deepest nesting of structs, lists, sets and maps read or skipped, as in Apache Thrift
|
|
56
|
+
MAX_DEPTH = 64
|
|
57
|
+
|
|
48
58
|
# Wire type written for a declared field type
|
|
49
59
|
#
|
|
50
60
|
# @param type [Symbol, Array, Class] a scalar type from WIRE_TYPES, a +[:list, elem]+ Array,
|
|
@@ -86,9 +96,12 @@ module Herringbone
|
|
|
86
96
|
|
|
87
97
|
# @param buf [String] the encoded data (binary)
|
|
88
98
|
# @param pos [Integer] byte offset to start reading at
|
|
99
|
+
# @raise [Error] when +pos+ is negative
|
|
89
100
|
def initialize(buf, pos = 0)
|
|
101
|
+
raise Error, "Negative offset #{pos}" if pos.negative?
|
|
90
102
|
@buf = buf
|
|
91
103
|
@pos = pos
|
|
104
|
+
@depth = 0
|
|
92
105
|
end
|
|
93
106
|
|
|
94
107
|
# Reads one raw byte
|
|
@@ -104,17 +117,21 @@ module Herringbone
|
|
|
104
117
|
|
|
105
118
|
# Reads an unsigned LEB128 varint
|
|
106
119
|
#
|
|
107
|
-
# @return [Integer] the decoded non-negative value
|
|
108
|
-
# @raise [Error] when the varint runs past the buffer
|
|
120
|
+
# @return [Integer] the decoded non-negative value, below 2**64
|
|
121
|
+
# @raise [Error] when the varint runs past the buffer, is longer than 10 bytes or does not
|
|
122
|
+
# fit in 64 bits
|
|
109
123
|
def read_varint
|
|
110
124
|
result = 0
|
|
111
125
|
shift = 0
|
|
112
126
|
while true
|
|
113
127
|
b = read_byte
|
|
114
128
|
result |= (b & 0x7F) << shift
|
|
115
|
-
|
|
129
|
+
if b < 0x80
|
|
130
|
+
raise Error, "Varint exceeds 64 bits" if result >= 2**64
|
|
131
|
+
return result
|
|
132
|
+
end
|
|
116
133
|
shift += 7
|
|
117
|
-
raise Error, "Varint too long" if shift >
|
|
134
|
+
raise Error, "Varint too long" if shift > 63
|
|
118
135
|
end
|
|
119
136
|
end
|
|
120
137
|
|
|
@@ -141,8 +158,10 @@ module Herringbone
|
|
|
141
158
|
|
|
142
159
|
# Reads an 8-byte little-endian double
|
|
143
160
|
#
|
|
144
|
-
# @return [Float
|
|
161
|
+
# @return [Float] the value
|
|
162
|
+
# @raise [Error] when fewer than 8 bytes are left
|
|
145
163
|
def read_double
|
|
164
|
+
raise Error, "Unexpected end of Thrift data at #{@pos}" if @pos + 8 > @buf.bytesize
|
|
146
165
|
v = @buf.byteslice(@pos, 8).unpack1("E")
|
|
147
166
|
@pos += 8
|
|
148
167
|
v
|
|
@@ -155,36 +174,39 @@ module Herringbone
|
|
|
155
174
|
#
|
|
156
175
|
# @param klass [Class] a Thrift::Struct subclass
|
|
157
176
|
# @return [Struct] an instance of +klass+ with the fields that were present set
|
|
158
|
-
# @raise [Error] on truncated or malformed data
|
|
177
|
+
# @raise [Error] on truncated or malformed data, or nesting deeper than MAX_DEPTH
|
|
159
178
|
def read_struct(klass)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
nested do
|
|
180
|
+
obj = klass.new
|
|
181
|
+
fields = klass.fields_by_id
|
|
182
|
+
last_id = 0
|
|
183
|
+
while true
|
|
184
|
+
header = read_byte
|
|
185
|
+
wire = header & 0x0F
|
|
186
|
+
break if wire == T_STOP
|
|
187
|
+
delta = header >> 4
|
|
188
|
+
fid = delta.zero? ? read_zigzag : last_id + delta
|
|
189
|
+
last_id = fid
|
|
190
|
+
field = fields[fid]
|
|
191
|
+
if field && Thrift.compatible?(wire, field.type)
|
|
192
|
+
obj.instance_variable_set(field.ivar, read_value(wire, field.type))
|
|
193
|
+
else
|
|
194
|
+
skip(wire)
|
|
195
|
+
end
|
|
175
196
|
end
|
|
197
|
+
obj
|
|
176
198
|
end
|
|
177
|
-
obj
|
|
178
199
|
end
|
|
179
200
|
|
|
180
201
|
# Reads one value of wire type +wire+ as declared +type+
|
|
181
202
|
#
|
|
182
203
|
# @param wire [Integer] wire type from the field or list header
|
|
183
|
-
# @param type [Symbol, Array, Class] declared type; +:string+ makes binary values UTF-8
|
|
184
|
-
# an Array or Struct subclass gives the element type or struct to read
|
|
204
|
+
# @param type [Symbol, Array, Class] declared type; +:string+ makes binary values UTF-8 and
|
|
205
|
+
# +:binary+ makes them BINARY, an Array or Struct subclass gives the element type or struct to read
|
|
185
206
|
# @return [Object] true/false, an Integer (bytes are signed), a Float, a String, an Array
|
|
186
207
|
# (or nil, see #read_list) or a Struct
|
|
187
|
-
# @raise [Error] on an unsupported wire type or
|
|
208
|
+
# @raise [Error] on an unsupported wire type, truncated data or an integer out of the range
|
|
209
|
+
# of +type+
|
|
188
210
|
def read_value(wire, type)
|
|
189
211
|
case wire
|
|
190
212
|
when T_TRUE then true
|
|
@@ -192,11 +214,14 @@ module Herringbone
|
|
|
192
214
|
when T_BYTE
|
|
193
215
|
b = read_byte
|
|
194
216
|
(b >= 0x80) ? b - 0x100 : b
|
|
195
|
-
when T_I16, T_I32, T_I64
|
|
217
|
+
when T_I16, T_I32, T_I64
|
|
218
|
+
n = read_zigzag
|
|
219
|
+
raise Error, "Integer #{n} out of range for #{type}" unless INT_RANGES.fetch(type).cover?(n)
|
|
220
|
+
n
|
|
196
221
|
when T_DOUBLE then read_double
|
|
197
222
|
when T_BINARY
|
|
198
223
|
s = read_binary
|
|
199
|
-
(type == :string) ?
|
|
224
|
+
s.force_encoding((type == :string) ? Encoding::UTF_8 : Encoding::BINARY)
|
|
200
225
|
when T_LIST, T_SET then read_list(type)
|
|
201
226
|
when T_STRUCT then read_struct(type)
|
|
202
227
|
else
|
|
@@ -210,24 +235,26 @@ module Herringbone
|
|
|
210
235
|
# @param type [Array] declared list type, +[:list, elem_type]+
|
|
211
236
|
# @return [Array, nil] the elements, or nil (with the list skipped) when the element wire
|
|
212
237
|
# type does not match +elem_type+
|
|
213
|
-
# @raise [Error] on truncated or malformed data
|
|
238
|
+
# @raise [Error] on truncated or malformed data, or nesting deeper than MAX_DEPTH
|
|
214
239
|
def read_list(type)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
size.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
240
|
+
nested do
|
|
241
|
+
header = read_byte
|
|
242
|
+
size = header >> 4
|
|
243
|
+
size = read_size if size == 15
|
|
244
|
+
elem_wire = header & 0x0F
|
|
245
|
+
elem_type = type[1]
|
|
246
|
+
bool_elems = elem_wire == T_TRUE || elem_wire == T_FALSE
|
|
247
|
+
if size.positive? && !(bool_elems && elem_type == :bool) && !Thrift.compatible?(elem_wire, elem_type)
|
|
248
|
+
size.times { bool_elems ? read_byte : skip(elem_wire) }
|
|
249
|
+
return nil
|
|
250
|
+
end
|
|
251
|
+
Array.new(size) do
|
|
252
|
+
if elem_wire == T_TRUE || elem_wire == T_FALSE
|
|
253
|
+
# Booleans inside lists are encoded as full bytes
|
|
254
|
+
read_byte == T_TRUE
|
|
255
|
+
else
|
|
256
|
+
read_value(elem_wire, elem_type)
|
|
257
|
+
end
|
|
231
258
|
end
|
|
232
259
|
end
|
|
233
260
|
end
|
|
@@ -236,40 +263,76 @@ module Herringbone
|
|
|
236
263
|
#
|
|
237
264
|
# @param wire [Integer] wire type of the value to skip
|
|
238
265
|
# @return [void]
|
|
239
|
-
# @raise [Error] on an unknown wire type (including T_STOP) or
|
|
266
|
+
# @raise [Error] on an unknown wire type (including T_STOP), truncated data or nesting deeper
|
|
267
|
+
# than MAX_DEPTH
|
|
240
268
|
def skip(wire)
|
|
241
269
|
case wire
|
|
242
270
|
when T_TRUE, T_FALSE then nil
|
|
243
271
|
when T_BYTE then read_byte
|
|
244
272
|
when T_I16, T_I32, T_I64 then read_varint
|
|
245
|
-
when T_DOUBLE then
|
|
273
|
+
when T_DOUBLE then read_double
|
|
246
274
|
when T_BINARY then read_binary
|
|
247
275
|
when T_LIST, T_SET
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
276
|
+
nested do
|
|
277
|
+
header = read_byte
|
|
278
|
+
size = header >> 4
|
|
279
|
+
size = read_size if size == 15
|
|
280
|
+
elem = header & 0x0F
|
|
281
|
+
size.times { (elem == T_TRUE || elem == T_FALSE) ? read_byte : skip(elem) }
|
|
282
|
+
end
|
|
253
283
|
when T_MAP
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
284
|
+
nested do
|
|
285
|
+
size = read_size
|
|
286
|
+
unless size.zero?
|
|
287
|
+
kv = read_byte
|
|
288
|
+
size.times do
|
|
289
|
+
[kv >> 4, kv & 0x0F].each { |w| (w == T_TRUE || w == T_FALSE) ? read_byte : skip(w) }
|
|
290
|
+
end
|
|
259
291
|
end
|
|
260
292
|
end
|
|
261
293
|
when T_STRUCT
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
294
|
+
nested do
|
|
295
|
+
while true
|
|
296
|
+
header = read_byte
|
|
297
|
+
w = header & 0x0F
|
|
298
|
+
break if w == T_STOP
|
|
299
|
+
read_zigzag if (header >> 4).zero?
|
|
300
|
+
skip(w)
|
|
301
|
+
end
|
|
268
302
|
end
|
|
269
303
|
else
|
|
270
304
|
raise Error, "Cannot skip wire type #{wire}"
|
|
271
305
|
end
|
|
272
306
|
end
|
|
307
|
+
|
|
308
|
+
private
|
|
309
|
+
|
|
310
|
+
# Reads the element count of a list, set or map
|
|
311
|
+
#
|
|
312
|
+
# Every element takes at least one byte, so a count above the bytes left is corrupt. Checking
|
|
313
|
+
# that up front keeps a forged count from preallocating a huge Array or skipping for ever.
|
|
314
|
+
#
|
|
315
|
+
# @return [Integer] the count, at most the number of bytes left in the buffer
|
|
316
|
+
# @raise [Error] when the count exceeds the bytes left
|
|
317
|
+
def read_size
|
|
318
|
+
size = read_varint
|
|
319
|
+
left = @buf.bytesize - @pos
|
|
320
|
+
raise Error, "Collection of #{size} elements exceeds the #{left} bytes left" if size > left
|
|
321
|
+
size
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Runs the block one nesting level deeper
|
|
325
|
+
#
|
|
326
|
+
# @yieldreturn [Object] the value read inside the nested container
|
|
327
|
+
# @return [Object] the block's value
|
|
328
|
+
# @raise [Error] when the nesting gets deeper than MAX_DEPTH
|
|
329
|
+
def nested
|
|
330
|
+
@depth += 1
|
|
331
|
+
raise Error, "Thrift data nested deeper than #{MAX_DEPTH} levels" if @depth > MAX_DEPTH
|
|
332
|
+
yield
|
|
333
|
+
ensure
|
|
334
|
+
@depth -= 1
|
|
335
|
+
end
|
|
273
336
|
end
|
|
274
337
|
|
|
275
338
|
# Encodes compact protocol data by appending to a binary String
|
|
@@ -348,12 +411,15 @@ module Herringbone
|
|
|
348
411
|
# @param type [Symbol, Array, Class] declared type (see WIRE_TYPES)
|
|
349
412
|
# @param value [Object] Integer, Float, String, Array or Struct matching +type+
|
|
350
413
|
# @return [void]
|
|
414
|
+
# @raise [Error] when an integer type gets a value that is not an Integer in its range, or a
|
|
415
|
+
# +:string+ gets a value that is not valid UTF-8 and cannot be converted to it
|
|
351
416
|
def write_value(type, value)
|
|
352
417
|
case type
|
|
353
|
-
when :byte then @buf << (value & 0xFF)
|
|
354
|
-
when :i16, :i32, :i64 then write_zigzag(value)
|
|
418
|
+
when :byte then @buf << (checked_int(type, value) & 0xFF)
|
|
419
|
+
when :i16, :i32, :i64 then write_zigzag(checked_int(type, value))
|
|
355
420
|
when :double then @buf << [value].pack("E")
|
|
356
|
-
when :binary
|
|
421
|
+
when :binary then write_binary(value)
|
|
422
|
+
when :string then write_binary(utf8(value))
|
|
357
423
|
when Array then write_list(type[1], value)
|
|
358
424
|
else write_struct(value)
|
|
359
425
|
end
|
|
@@ -381,6 +447,32 @@ module Herringbone
|
|
|
381
447
|
end
|
|
382
448
|
end
|
|
383
449
|
end
|
|
450
|
+
|
|
451
|
+
private
|
|
452
|
+
|
|
453
|
+
# @param type [Symbol] declared integer type, a key of INT_RANGES
|
|
454
|
+
# @param value [Object] value to be written as +type+
|
|
455
|
+
# @return [Integer] +value+
|
|
456
|
+
# @raise [Error] when +value+ is not an Integer that +type+ can hold
|
|
457
|
+
def checked_int(type, value)
|
|
458
|
+
raise Error, "#{value.inspect} is not a valid #{type}" unless value.is_a?(Integer) && INT_RANGES.fetch(type).cover?(value)
|
|
459
|
+
value
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Parquet strings are UTF-8. Binary Strings are taken to hold UTF-8 bytes, Strings in other
|
|
463
|
+
# encodings are converted.
|
|
464
|
+
#
|
|
465
|
+
# @param s [String] value of a +:string+ field
|
|
466
|
+
# @return [String] +s+, or a UTF-8 copy of it
|
|
467
|
+
# @raise [Error] when +s+ is not valid UTF-8 and cannot be converted to it
|
|
468
|
+
def utf8(s)
|
|
469
|
+
return s if s.ascii_only? || (s.encoding == Encoding::UTF_8 && s.valid_encoding?)
|
|
470
|
+
u = (s.encoding == Encoding::BINARY) ? s.dup.force_encoding(Encoding::UTF_8) : s.encode(Encoding::UTF_8)
|
|
471
|
+
raise Error, "String in #{s.encoding} is not valid UTF-8" unless u.valid_encoding?
|
|
472
|
+
u
|
|
473
|
+
rescue EncodingError => e
|
|
474
|
+
raise Error, "String in #{s.encoding} cannot be converted to UTF-8: #{e.message}"
|
|
475
|
+
end
|
|
384
476
|
end
|
|
385
477
|
|
|
386
478
|
# A field declared on a Thrift::Struct subclass
|
|
@@ -399,14 +491,14 @@ module Herringbone
|
|
|
399
491
|
# field 1, :name, :i32
|
|
400
492
|
class Struct
|
|
401
493
|
class << self
|
|
402
|
-
# @return [Array<Field>] declared fields, sorted by id
|
|
494
|
+
# @return [Array<Field>] declared fields, sorted by id, including the superclass's
|
|
403
495
|
def fields
|
|
404
|
-
@fields || []
|
|
496
|
+
@fields || ((superclass < Struct) ? superclass.fields : [])
|
|
405
497
|
end
|
|
406
498
|
|
|
407
499
|
# @return [Hash{Integer => Field}] declared fields by id, for decoding
|
|
408
500
|
def fields_by_id
|
|
409
|
-
@fields_by_id || {}
|
|
501
|
+
@fields_by_id || ((superclass < Struct) ? superclass.fields_by_id : {})
|
|
410
502
|
end
|
|
411
503
|
|
|
412
504
|
# Declares a field and defines its accessor
|
|
@@ -475,6 +567,12 @@ module Herringbone
|
|
|
475
567
|
def ==(other)
|
|
476
568
|
other.class == self.class && other.to_h == to_h
|
|
477
569
|
end
|
|
570
|
+
alias_method :eql?, :==
|
|
571
|
+
|
|
572
|
+
# @return [Integer] hash agreeing with #==, so equal structs work as Hash keys and in +uniq+
|
|
573
|
+
def hash
|
|
574
|
+
[self.class, to_h].hash
|
|
575
|
+
end
|
|
478
576
|
|
|
479
577
|
# @return [String] short class name and the set fields
|
|
480
578
|
def inspect
|
data/lib/herringbone/types.rb
CHANGED
data/lib/herringbone/version.rb
CHANGED
|
@@ -15,6 +15,8 @@ module Herringbone
|
|
|
15
15
|
# Used through Inspector#to_html:
|
|
16
16
|
#
|
|
17
17
|
# File.open("data.parquet", "rb") { |io| Herringbone::Inspector.new(io).to_html }
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
18
20
|
class Visualizer
|
|
19
21
|
# highlight.js build loaded by the page to colour the footer JSON; optional.
|
|
20
22
|
HIGHLIGHT_JS = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"
|
data/lib/herringbone/writer.rb
CHANGED
|
@@ -76,6 +76,8 @@ module Herringbone
|
|
|
76
76
|
MAX_DICTIONARY_BYTES = 1024 * 1024
|
|
77
77
|
# The row group byte size is first estimated after this many rows, then after every row group
|
|
78
78
|
ESTIMATE_AFTER_ROWS = 1000
|
|
79
|
+
# Options .open_for_copies refuses, since the row groups it writes keep their boundaries
|
|
80
|
+
ROW_GROUP_OPTIONS = %i[row_group_bytes row_group_rows].freeze
|
|
79
81
|
|
|
80
82
|
# @return [Schema] schema the rows are written with
|
|
81
83
|
attr_reader :schema
|
|
@@ -121,6 +123,29 @@ module Herringbone
|
|
|
121
123
|
result
|
|
122
124
|
end
|
|
123
125
|
|
|
126
|
+
# Internal (used by Redaction and Combiner): .open for a file whose row groups are all written
|
|
127
|
+
# with #write_row_group, one for each row group of another file, so they keep their boundaries
|
|
128
|
+
#
|
|
129
|
+
# @param io [IO, #write] destination
|
|
130
|
+
# @param schema [Schema] schema of the rows
|
|
131
|
+
# @param refusal [String] why +row_group_bytes:+ and +row_group_rows:+ cannot be given, e.g.
|
|
132
|
+
# "a redaction keeps the row groups of the input"
|
|
133
|
+
# @param options [Hash{Symbol => Object}] see .open, without the row group options
|
|
134
|
+
# @option options [Symbol] :compression (:snappy) codec of the chunks encoded again
|
|
135
|
+
# @option options [Hash{String => String}] :metadata ({}) key/value metadata for the footer
|
|
136
|
+
# @option options [EncryptionConfiguration, Hash{Symbol => Object}, nil] :encryption (nil) see #initialize
|
|
137
|
+
# @yield [writer] the open writer
|
|
138
|
+
# @yieldparam writer [Writer] writer to write row groups to
|
|
139
|
+
# @yieldreturn [Object] returned by open_for_copies
|
|
140
|
+
# @return [Object] the block's value
|
|
141
|
+
# @raise [ArgumentError] for +row_group_bytes:+ / +row_group_rows:+, or an invalid option
|
|
142
|
+
# @api private
|
|
143
|
+
def self.open_for_copies(io, schema, refusal, **options, &block)
|
|
144
|
+
bad = options.keys & ROW_GROUP_OPTIONS
|
|
145
|
+
raise ArgumentError, "#{bad.join(", ")}: #{refusal}" unless bad.empty?
|
|
146
|
+
self.open(io, schema, row_group_bytes: 1 << 62, **options, &block)
|
|
147
|
+
end
|
|
148
|
+
|
|
124
149
|
# Validates the options and writes the leading magic bytes to +io+.
|
|
125
150
|
#
|
|
126
151
|
# @param io [IO, #write] destination; switched to binmode when it supports that
|
|
@@ -269,11 +294,12 @@ module Herringbone
|
|
|
269
294
|
# @return [Format::OffsetIndex, nil] the chunk's OffsetIndex, with source file offsets
|
|
270
295
|
# @!attribute bloom_filter
|
|
271
296
|
# @return [String, nil] the chunk's encoded bloom filter, header included
|
|
297
|
+
# @api private
|
|
272
298
|
CopiedChunk = Struct.new(:chunk, :start, :bytes, :column_index, :offset_index, :bloom_filter)
|
|
273
299
|
|
|
274
|
-
# Internal (used by Redaction): writes a row group of +num_rows+ rows from the
|
|
275
|
-
# except for the columns in +copies+, whose chunks are copied byte for byte
|
|
276
|
-
# with their offsets rebased. Starts a new row group afterwards.
|
|
300
|
+
# Internal (used by Redaction and Combiner): writes a row group of +num_rows+ rows from the
|
|
301
|
+
# buffered values, except for the columns in +copies+, whose chunks are copied byte for byte
|
|
302
|
+
# from another file with their offsets rebased. Starts a new row group afterwards.
|
|
277
303
|
#
|
|
278
304
|
# @param num_rows [Integer] rows in the row group
|
|
279
305
|
# @param copies [Hash{Integer => CopiedChunk}] column index => chunk to copy instead of encoding
|
|
@@ -285,6 +311,7 @@ module Herringbone
|
|
|
285
311
|
# @param sorting_columns [Array<Format::SortingColumn>, nil] stored on the RowGroup as given
|
|
286
312
|
# @return [void]
|
|
287
313
|
# @raise [Error] when the writer is closed
|
|
314
|
+
# @api private
|
|
288
315
|
def write_row_group(num_rows, copies: {}, codecs: {}, bloom_filters: {}, sorting_columns: nil)
|
|
289
316
|
raise Error, "Writer is closed" if @closed
|
|
290
317
|
if @encryption && (encrypted = copies.keys.find { |i| @encryption.encrypted?(i) })
|
|
@@ -293,7 +320,7 @@ module Herringbone
|
|
|
293
320
|
start = @pos
|
|
294
321
|
chunks = @schema.columns.map do |col|
|
|
295
322
|
if (copy = copies[col.index])
|
|
296
|
-
copy_column_chunk(copy)
|
|
323
|
+
copy_column_chunk(col, copy)
|
|
297
324
|
else
|
|
298
325
|
bloom = @bloom_filters[col.dotted_path]
|
|
299
326
|
bloom ||= bloom_filter_settings({}) if bloom_filters[col.index] && BloomFilter::TYPES.include?(col.type)
|
|
@@ -314,21 +341,23 @@ module Herringbone
|
|
|
314
341
|
reset_buffers
|
|
315
342
|
end
|
|
316
343
|
|
|
317
|
-
# Internal (used by Redaction): whether the column is encrypted in this file
|
|
344
|
+
# Internal (used by Redaction and Combiner): whether the column is encrypted in this file
|
|
318
345
|
#
|
|
319
346
|
# @param index [Integer] leaf column index
|
|
320
347
|
# @return [Boolean]
|
|
348
|
+
# @api private
|
|
321
349
|
def encrypted_column?(index) = @encryption&.encrypted?(index) || false
|
|
322
350
|
|
|
323
|
-
# Internal (used by Redaction): shreds one value per row of a top-level field into
|
|
324
|
-
# for #write_row_group. Other fields are left as they are, so the caller decides
|
|
325
|
-
# are encoded and which are copied.
|
|
351
|
+
# Internal (used by Redaction and Combiner): shreds one value per row of a top-level field into
|
|
352
|
+
# the buffers, for #write_row_group. Other fields are left as they are, so the caller decides
|
|
353
|
+
# which columns are encoded and which are copied.
|
|
326
354
|
#
|
|
327
355
|
# @param name [String] top-level field name
|
|
328
356
|
# @param values [Array] the field's value for each row
|
|
329
357
|
# @return [void]
|
|
330
358
|
# @raise [ArgumentError] when the schema has no such field
|
|
331
359
|
# @raise [EncodeError] when a value cannot be written to the field
|
|
360
|
+
# @api private
|
|
332
361
|
def buffer_field(name, values)
|
|
333
362
|
field = @schema.field(name) or raise ArgumentError, "No such field #{name.inspect}"
|
|
334
363
|
values.each_with_index do |value, i|
|
|
@@ -414,6 +443,7 @@ module Herringbone
|
|
|
414
443
|
# @return [String, Array<Integer>, nil] repetition levels, like +defs+; nil for non-repeated columns
|
|
415
444
|
# @!attribute values
|
|
416
445
|
# @return [Array, ByteValues] non-null physical values
|
|
446
|
+
# @api private
|
|
417
447
|
ColumnBuffer = Struct.new(:defs, :reps, :values)
|
|
418
448
|
|
|
419
449
|
# Starts a new row group: fresh buffers per column, and the per-field write plan
|
|
@@ -769,6 +799,7 @@ module Herringbone
|
|
|
769
799
|
# @return [Integer] non-null values in the page
|
|
770
800
|
# @!attribute range
|
|
771
801
|
# @return [Array(Object, Object), nil] [min, max] physical values, nil when there are none
|
|
802
|
+
# @api private
|
|
772
803
|
PageInfo = Struct.new(:offset, :size, :first_row, :nulls, :non_null, :range)
|
|
773
804
|
|
|
774
805
|
# @param pages [Array<PageInfo>] pages of a column chunk
|
|
@@ -781,14 +812,17 @@ module Herringbone
|
|
|
781
812
|
|
|
782
813
|
# Writes a chunk copied from another file. The pages, the ColumnIndex and the bloom filter
|
|
783
814
|
# hold no file offsets and go out as they are; the ColumnMetaData and the OffsetIndex do, so
|
|
784
|
-
# those are rebased onto where the chunk lands in this file.
|
|
815
|
+
# those are rebased onto where the chunk lands in this file. The path is this file's, which
|
|
816
|
+
# differs when the source file names list elements another way ("list.item", "array").
|
|
785
817
|
#
|
|
818
|
+
# @param col [Schema::Column] the column the chunk is copied into
|
|
786
819
|
# @param copy [CopiedChunk] the chunk to copy
|
|
787
820
|
# @return [Format::ColumnChunk] chunk with its ColumnMetaData, for the row group
|
|
788
|
-
def copy_column_chunk(copy)
|
|
821
|
+
def copy_column_chunk(col, copy)
|
|
789
822
|
start = @pos
|
|
790
823
|
shift = start - copy.start
|
|
791
824
|
meta = Format::ColumnMetaData.decode(copy.chunk.meta_data.encode).first
|
|
825
|
+
meta.path_in_schema = col.path
|
|
792
826
|
meta.data_page_offset += shift
|
|
793
827
|
dict = meta.dictionary_page_offset
|
|
794
828
|
# Some writers store 0 when there is no dictionary page
|
|
@@ -1070,7 +1104,7 @@ module Herringbone
|
|
|
1070
1104
|
# keeps in their source codec use that codec's default
|
|
1071
1105
|
#
|
|
1072
1106
|
# @param codec [Integer] codec id
|
|
1073
|
-
# @param data [String] bytes to compress
|
|
1107
|
+
# @param data [String, Array<String>] bytes to compress, or the parts of them
|
|
1074
1108
|
# @return [String] compressed bytes
|
|
1075
1109
|
def compress(codec, data)
|
|
1076
1110
|
Compression.compress(codec, data, (codec == @codec) ? @compression_level : nil)
|
|
@@ -1080,28 +1114,30 @@ module Herringbone
|
|
|
1080
1114
|
# stored, encrypted.
|
|
1081
1115
|
#
|
|
1082
1116
|
# @param header [Format::PageHeader] header; sizes and CRC32 are filled in here
|
|
1083
|
-
# @param body [String] uncompressed page body
|
|
1084
|
-
# @param compressed [String, nil] bytes to write as the page body,
|
|
1085
|
-
# pages, whose levels stay uncompressed); +body+ is
|
|
1117
|
+
# @param body [String, Array<String>] uncompressed page body, or its parts
|
|
1118
|
+
# @param compressed [String, Array<String>, nil] bytes to write as the page body, or their
|
|
1119
|
+
# parts, when already prepared (v2 data pages, whose levels stay uncompressed); +body+ is
|
|
1120
|
+
# compressed otherwise
|
|
1086
1121
|
# @param codec [Integer] codec id to compress +body+ with
|
|
1087
1122
|
# @param crypto [Encryption::ModuleCrypto, nil] encryption of the column's modules
|
|
1088
1123
|
# @param ordinal [Integer, nil] data page ordinal within the chunk; nil for a dictionary page
|
|
1089
1124
|
# @return [Integer]
|
|
1090
1125
|
def write_page(header, body, compressed = nil, codec: @codec, crypto: nil, ordinal: nil)
|
|
1091
|
-
compressed
|
|
1092
|
-
header.uncompressed_page_size ||= body.bytesize
|
|
1126
|
+
parts = Array(compressed || compress(codec, body))
|
|
1127
|
+
header.uncompressed_page_size ||= Array(body).sum(&:bytesize)
|
|
1093
1128
|
dictionary = header.type == Format::PageType::DICTIONARY_PAGE
|
|
1094
1129
|
if crypto
|
|
1095
|
-
|
|
1130
|
+
parts = [crypto.encrypt(dictionary ? Encryption::DICTIONARY_PAGE : Encryption::DATA_PAGE, parts.join, ordinal)]
|
|
1096
1131
|
end
|
|
1097
|
-
header.compressed_page_size =
|
|
1098
|
-
|
|
1132
|
+
header.compressed_page_size = parts.sum(&:bytesize)
|
|
1133
|
+
crc = parts.inject(0) { |c, part| Zlib.crc32(part, c) }
|
|
1134
|
+
header.crc = (crc >= 0x8000_0000) ? crc - 0x1_0000_0000 : crc
|
|
1099
1135
|
encoded = header.encode
|
|
1100
1136
|
if crypto
|
|
1101
1137
|
encoded = crypto.encrypt(dictionary ? Encryption::DICTIONARY_PAGE_HEADER : Encryption::DATA_PAGE_HEADER, encoded, ordinal)
|
|
1102
1138
|
end
|
|
1103
1139
|
write_raw(encoded)
|
|
1104
|
-
write_raw(
|
|
1140
|
+
parts.each { |part| write_raw(part) unless part.empty? }
|
|
1105
1141
|
encoded.bytesize + header.uncompressed_page_size
|
|
1106
1142
|
end
|
|
1107
1143
|
|
|
@@ -1117,7 +1153,7 @@ module Herringbone
|
|
|
1117
1153
|
# modules and the page's ordinal, for an encrypted column
|
|
1118
1154
|
# @return [Integer] uncompressed size including the header
|
|
1119
1155
|
def write_data_page_v1(n, rep_bytes, def_bytes, encoded, encoding, codec, page_crypto)
|
|
1120
|
-
body =
|
|
1156
|
+
body = []
|
|
1121
1157
|
body << [rep_bytes.bytesize].pack("V") << rep_bytes unless rep_bytes.empty?
|
|
1122
1158
|
body << [def_bytes.bytesize].pack("V") << def_bytes unless def_bytes.empty?
|
|
1123
1159
|
body << encoded
|
|
@@ -1156,7 +1192,7 @@ module Herringbone
|
|
|
1156
1192
|
is_compressed: codec != Format::Codec::UNCOMPRESSED
|
|
1157
1193
|
)
|
|
1158
1194
|
)
|
|
1159
|
-
write_page(header, "".b, rep_bytes
|
|
1195
|
+
write_page(header, "".b, [rep_bytes, def_bytes, compressed], codec: codec, crypto: page_crypto&.first,
|
|
1160
1196
|
ordinal: page_crypto&.last)
|
|
1161
1197
|
end
|
|
1162
1198
|
|
data/lib/herringbone/xxhash.rb
CHANGED
|
@@ -14,6 +14,8 @@ module Herringbone
|
|
|
14
14
|
# allocations trigger so many garbage collections that hashing becomes up to 10x slower. The
|
|
15
15
|
# arithmetic is written once with the small code generators below, whose output is inlined into
|
|
16
16
|
# the hashing methods: no method calls or allocations in the hot paths.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
17
19
|
module XXHash
|
|
18
20
|
# 64-bit mask
|
|
19
21
|
M = 0xFFFF_FFFF_FFFF_FFFF
|