herringbone 0.6.1 → 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 +34 -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 +49 -10
- 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/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
|
data/lib/herringbone.rb
CHANGED
|
@@ -40,6 +40,34 @@ module Herringbone
|
|
|
40
40
|
# such as a codec whose library is unavailable
|
|
41
41
|
class UnsupportedError < Error; end
|
|
42
42
|
|
|
43
|
+
# Schemas that do not fit together: two schemas that cannot be united or intersected
|
|
44
|
+
# (Schema#union, Schema#intersect), or inputs of Herringbone.combine that do not fit the output
|
|
45
|
+
# schema or each other. The message lists every field that does not fit, one per line, e.g.
|
|
46
|
+
#
|
|
47
|
+
# Cannot unite the schemas, 2 fields do not fit:
|
|
48
|
+
# price: int64 vs double (int64 does not fit a double exactly)
|
|
49
|
+
# address.zip: int32 vs string (no common type)
|
|
50
|
+
class IncompatibleSchema < Error
|
|
51
|
+
# A field that does not fit: its dotted path, how each side declares it, why the two do not
|
|
52
|
+
# fit, and for Herringbone.combine the position of the input it was found in
|
|
53
|
+
Conflict = Struct.new(:path, :left, :right, :reason, :input) do
|
|
54
|
+
# @return [String] e.g. "address.zip: int32 vs string (no common type)"
|
|
55
|
+
def to_s = "#{path.empty? ? "(top level)" : path}: #{left} vs #{right} (#{reason})"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Array<Conflict>] every field that does not fit, in schema (and input) order
|
|
59
|
+
attr_reader :conflicts
|
|
60
|
+
|
|
61
|
+
# @param conflicts [Array<Conflict>] the fields that do not fit
|
|
62
|
+
# @param operation [String, nil] what was attempted, "unite" or "intersect", for the message
|
|
63
|
+
# @param message [String, nil] the message, instead of one built from +operation+
|
|
64
|
+
def initialize(conflicts, operation: nil, message: nil)
|
|
65
|
+
@conflicts = conflicts
|
|
66
|
+
count = (conflicts.size == 1) ? "1 field does" : "#{conflicts.size} fields do"
|
|
67
|
+
super(message || "Cannot #{operation} the schemas, #{count} not fit:\n#{conflicts.map { |c| " #{c}" }.join("\n")}")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
43
71
|
# An encrypted file or column cannot be read: its key was not given, the key or AAD prefix is
|
|
44
72
|
# wrong, or the encrypted bytes were changed
|
|
45
73
|
class DecryptionError < Error; end
|
|
@@ -56,6 +84,7 @@ module Herringbone
|
|
|
56
84
|
|
|
57
85
|
autoload :BloomFilter, "#{LIB}/bloom_filter"
|
|
58
86
|
autoload :ByteValues, "#{LIB}/byte_values"
|
|
87
|
+
autoload :Combiner, "#{LIB}/combiner"
|
|
59
88
|
autoload :Compression, "#{LIB}/compression"
|
|
60
89
|
autoload :DecryptionConfiguration, "#{LIB}/encryption_configuration"
|
|
61
90
|
autoload :Encryption, "#{LIB}/encryption"
|
|
@@ -68,6 +97,7 @@ module Herringbone
|
|
|
68
97
|
autoload :MissingCodecError, "#{LIB}/compression"
|
|
69
98
|
autoload :Reader, "#{LIB}/reader"
|
|
70
99
|
autoload :Redaction, "#{LIB}/redaction"
|
|
100
|
+
autoload :RestrictedReadableIO, "#{LIB}/restricted_readable_io"
|
|
71
101
|
autoload :Schema, "#{LIB}/schema"
|
|
72
102
|
autoload :SimpleWriter, "#{LIB}/simple_writer"
|
|
73
103
|
autoload :Thrift, "#{LIB}/thrift"
|
|
@@ -176,16 +206,17 @@ module Herringbone
|
|
|
176
206
|
writer.rows_written
|
|
177
207
|
end
|
|
178
208
|
|
|
179
|
-
# Rewrites +
|
|
180
|
-
# the block (or taking +redaction+). A shortcut for Redaction#apply.
|
|
209
|
+
# Rewrites +io_or_reader+ into +output_io+ with rows removed or values replaced, building the
|
|
210
|
+
# Redaction from the block (or taking +redaction+). A shortcut for Redaction#apply.
|
|
181
211
|
#
|
|
182
|
-
# Herringbone.redact(
|
|
212
|
+
# Herringbone.redact(io, output_io) do |r|
|
|
183
213
|
# r.where(user_id: 42).delete
|
|
184
214
|
# r.replace(:email) { |email| email && OpenSSL::HMAC.hexdigest("SHA256", KEY, email) }
|
|
185
215
|
# end
|
|
186
216
|
#
|
|
187
|
-
# @param
|
|
188
|
-
#
|
|
217
|
+
# @param io_or_reader [IO, StringIO, Reader] the Parquet file, read with #seek and #read, or a
|
|
218
|
+
# Reader of it, whose IO and decryption are used; not closed
|
|
219
|
+
# @param output_io [IO, #write] destination, written sequentially; not closed
|
|
189
220
|
# @param redaction [Redaction, nil] the redaction to apply, instead of a block
|
|
190
221
|
# @param writer_options [Hash{Symbol => Object}] Writer options for re-encoded column chunks, and
|
|
191
222
|
# +metadata:+ to replace the footer key/value metadata, see Redaction#apply
|
|
@@ -204,15 +235,71 @@ module Herringbone
|
|
|
204
235
|
# @yieldreturn [void]
|
|
205
236
|
# @return [Redaction::Report] what was done
|
|
206
237
|
# @raise [ArgumentError] when given both or neither of +redaction+ and a block, a block that takes
|
|
207
|
-
# no parameter, or when the redaction does not fit the file's schema
|
|
238
|
+
# no parameter, a Reader with +decryption:+, or when the redaction does not fit the file's schema
|
|
208
239
|
# @raise [EncodeError] when a replacement value cannot be written to its column
|
|
209
|
-
def redact(
|
|
240
|
+
def redact(io_or_reader, output_io, redaction = nil, **writer_options, &block)
|
|
210
241
|
raise ArgumentError, "Herringbone.redact takes a Redaction or a block, not both" if redaction && block
|
|
211
242
|
raise ArgumentError, "Herringbone.redact needs a Redaction or a block" unless redaction || block
|
|
212
243
|
if block&.arity&.zero?
|
|
213
|
-
raise ArgumentError, "The block receives the redaction: Herringbone.redact(
|
|
244
|
+
raise ArgumentError, "The block receives the redaction: Herringbone.redact(io, output_io) { |r| r.where(user_id: 42).delete }"
|
|
214
245
|
end
|
|
215
|
-
(redaction || Redaction.new(&block)).apply(
|
|
246
|
+
(redaction || Redaction.new(&block)).apply(io_or_reader, output_io, **writer_options)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Concatenates Parquet files into +output_io+: every row group of every input, in order. A
|
|
250
|
+
# shortcut for Combiner. Column chunks are copied byte for byte (only offsets are rebased)
|
|
251
|
+
# wherever the output stores a column's values as the input does, so files with the same schema
|
|
252
|
+
# are combined without decoding anything.
|
|
253
|
+
#
|
|
254
|
+
# Herringbone.combine([jan, feb, mar], output_io) # the same schema
|
|
255
|
+
# Herringbone.combine([old, new], output_io, schema: :union) # all fields, nulls in the gaps
|
|
256
|
+
# Herringbone.combine([a, b], output_io, schema: :intersect) # the fields all inputs have
|
|
257
|
+
#
|
|
258
|
+
# Without +schema:+ every input must have the schema of the first (see Schema#==). With
|
|
259
|
+
# +schema: :union+ the output has the fields of all inputs, widened as Schema#union widens them,
|
|
260
|
+
# and fields an input lacks are written as nulls. With +schema: :intersect+ it has the fields all
|
|
261
|
+
# inputs have, and the others are dropped. A Schema given as +schema:+ must hold every input: each
|
|
262
|
+
# must have the same fields or fewer, in the same or narrower types. Every input is checked
|
|
263
|
+
# before anything is written, and IncompatibleSchema lists all that do not fit.
|
|
264
|
+
#
|
|
265
|
+
# The output is encrypted like the encrypted inputs, if any (plaintext inputs included), unless
|
|
266
|
+
# +encryption:+ says otherwise; +encryption: false+ writes it in plaintext. Inputs encrypted
|
|
267
|
+
# differently need +encryption:+. Herringbone never opens files by path: pass open IOs, which
|
|
268
|
+
# are read twice (footers first, then chunks), so keep them open until +combine+ returns and
|
|
269
|
+
# mind the limit on open files when combining very many of them.
|
|
270
|
+
#
|
|
271
|
+
# @param ios_or_readers [Enumerable<IO, StringIO, Reader>] the Parquet files: IOs read with #seek
|
|
272
|
+
# and #read, or Readers, which bring their own decryption; enumerated once, none is closed
|
|
273
|
+
# @param output_io [IO, #write] destination, written sequentially; not closed
|
|
274
|
+
# @param schema [Schema, Symbol, nil] +:union+, +:intersect+, a Schema every input fits, or nil
|
|
275
|
+
# to require the inputs to have the same schema
|
|
276
|
+
# @param decryption [DecryptionConfiguration, Hash{Symbol => Object}, Array, #call, nil] keys of
|
|
277
|
+
# encrypted inputs given as IOs, see Reader.new
|
|
278
|
+
# @param writer_options [Hash{Symbol => Object}] Writer options for chunks encoded again (and
|
|
279
|
+
# written nulls), +metadata:+ and +encryption:+
|
|
280
|
+
# @option writer_options [Symbol] :compression (codec of each source chunk) codec for chunks
|
|
281
|
+
# encoded again
|
|
282
|
+
# @option writer_options [Integer, nil] :compression_level (nil) level for that codec, see Writer
|
|
283
|
+
# @option writer_options [Hash{String => String}] :metadata (the first input's) footer key/value metadata
|
|
284
|
+
# @option writer_options [EncryptionConfiguration, Hash{Symbol => Object}, Key, String, false] :encryption
|
|
285
|
+
# (encrypted like the encrypted inputs) see Writer; false for a plaintext output
|
|
286
|
+
# @option writer_options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
|
|
287
|
+
# columns whose encoded chunks get a bloom filter, besides those whose source chunk had one
|
|
288
|
+
# @option writer_options [Integer] :page_bytes (1MB) approximate uncompressed data page size
|
|
289
|
+
# @option writer_options [Integer] :page_rows (20_000) maximum rows per data page
|
|
290
|
+
# @option writer_options [Integer] :data_page_version (1) 1 or 2
|
|
291
|
+
# @option writer_options [Boolean, Array<String>] :dictionary (true) see Writer
|
|
292
|
+
# @option writer_options [Hash{String => Symbol}] :encodings ({}) see Writer
|
|
293
|
+
# @return [Combiner::Report] rows written, how many row groups were copied or rewritten, and per
|
|
294
|
+
# input which fields were filled with nulls, widened or dropped
|
|
295
|
+
# @raise [IncompatibleSchema] listing every input (and field) that does not fit the others or
|
|
296
|
+
# +schema:+
|
|
297
|
+
# @raise [ArgumentError] when there are no inputs, an input is neither an IO nor a Reader, the
|
|
298
|
+
# encrypted inputs are encrypted differently and +encryption:+ is not given, or for
|
|
299
|
+
# +row_group_bytes:+ / +row_group_rows:+
|
|
300
|
+
# @raise [DecryptionError] when an input is encrypted and a key it needs was not given
|
|
301
|
+
def combine(ios_or_readers, output_io, schema: nil, decryption: nil, **writer_options)
|
|
302
|
+
Combiner.new(ios_or_readers, schema: schema, decryption: decryption).apply(output_io, **writer_options)
|
|
216
303
|
end
|
|
217
304
|
|
|
218
305
|
# Compression codecs this process can read and write, e.g. [:none, :snappy, :gzip, :lz4, :lz4_hadoop, :zstd].
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: herringbone
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julik Tarkhanov
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-09 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bigdecimal
|
|
@@ -45,6 +45,7 @@ files:
|
|
|
45
45
|
- lib/herringbone/codecs/lz4.rb
|
|
46
46
|
- lib/herringbone/codecs/lzo.rb
|
|
47
47
|
- lib/herringbone/codecs/snappy.rb
|
|
48
|
+
- lib/herringbone/combiner.rb
|
|
48
49
|
- lib/herringbone/compression.rb
|
|
49
50
|
- lib/herringbone/encodings/delta.rb
|
|
50
51
|
- lib/herringbone/encodings/plain.rb
|
|
@@ -58,6 +59,7 @@ files:
|
|
|
58
59
|
- lib/herringbone/key.rb
|
|
59
60
|
- lib/herringbone/reader.rb
|
|
60
61
|
- lib/herringbone/reader/bloom_filters.rb
|
|
62
|
+
- lib/herringbone/reader/chunk_copier.rb
|
|
61
63
|
- lib/herringbone/reader/column_chunk_reader.rb
|
|
62
64
|
- lib/herringbone/reader/column_cursor.rb
|
|
63
65
|
- lib/herringbone/reader/numo.rb
|
|
@@ -65,7 +67,9 @@ files:
|
|
|
65
67
|
- lib/herringbone/reader/scan.rb
|
|
66
68
|
- lib/herringbone/redaction.rb
|
|
67
69
|
- lib/herringbone/redaction/rewriter.rb
|
|
70
|
+
- lib/herringbone/restricted_readable_io.rb
|
|
68
71
|
- lib/herringbone/schema.rb
|
|
72
|
+
- lib/herringbone/schema/merge.rb
|
|
69
73
|
- lib/herringbone/simple_writer.rb
|
|
70
74
|
- lib/herringbone/thrift.rb
|
|
71
75
|
- lib/herringbone/types.rb
|