herringbone 0.5.0 → 0.6.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 +28 -0
- data/MANUAL.md +773 -0
- data/README.md +68 -437
- data/bin/herringbone +86 -7
- data/lib/herringbone/bloom_filter.rb +0 -55
- data/lib/herringbone/compression.rb +27 -4
- data/lib/herringbone/encryption.rb +716 -0
- data/lib/herringbone/encryption_configuration.rb +304 -0
- data/lib/herringbone/format.rb +55 -3
- data/lib/herringbone/inferring_writer.rb +17 -2
- data/lib/herringbone/inspector.rb +158 -28
- data/lib/herringbone/key.rb +107 -0
- data/lib/herringbone/reader/bloom_filters.rb +91 -0
- data/lib/herringbone/reader/column_chunk_reader.rb +55 -1
- data/lib/herringbone/reader.rb +100 -15
- data/lib/herringbone/redaction/rewriter.rb +59 -7
- data/lib/herringbone/redaction.rb +20 -5
- data/lib/herringbone/schema.rb +2 -0
- data/lib/herringbone/simple_writer.rb +28 -0
- data/lib/herringbone/version.rb +1 -1
- data/lib/herringbone/writer.rb +102 -24
- data/lib/herringbone.rb +69 -25
- metadata +7 -2
data/lib/herringbone/writer.rb
CHANGED
|
@@ -24,6 +24,8 @@ module Herringbone
|
|
|
24
24
|
# Options:
|
|
25
25
|
# compression: :snappy (default), :zstd, :gzip, :lz4 (LZ4_RAW), :lz4_hadoop, :brotli, :none
|
|
26
26
|
# (:zstd and :brotli need the zstd-ruby / brotli gems)
|
|
27
|
+
# compression_level: nil (the codec's default), or a level for :zstd (up to 22), :gzip (0-9)
|
|
28
|
+
# or :brotli (0-11)
|
|
27
29
|
# row_group_bytes: flush a row group once the buffered values take roughly this much memory
|
|
28
30
|
# (default 16MB). This bounds memory use while writing.
|
|
29
31
|
# row_group_rows: also flush after this many rows (default: no row limit)
|
|
@@ -38,6 +40,9 @@ module Herringbone
|
|
|
38
40
|
# an Array of column paths, or { "path" => true | { ndv:, fpp:, max_bytes: } }.
|
|
39
41
|
# Without ndv: the distinct values of each row group are counted. fpp defaults
|
|
40
42
|
# to 0.01 and max_bytes to 1MB. Filters are written after each row group.
|
|
43
|
+
# encryption: Parquet modular encryption: { footer_key: "...", columns: { "ssn" => "..." } }.
|
|
44
|
+
# Without columns: every column is encrypted with the footer key. See
|
|
45
|
+
# #initialize for the settings.
|
|
41
46
|
#
|
|
42
47
|
# Statistics and page indexes (ColumnIndex/OffsetIndex) are always written.
|
|
43
48
|
class Writer
|
|
@@ -83,6 +88,7 @@ module Herringbone
|
|
|
83
88
|
# @param schema [Schema] schema of the rows
|
|
84
89
|
# @param options [Hash{Symbol => Object}] see the class description and #initialize
|
|
85
90
|
# @option options [Symbol] :compression (:snappy) codec, see Herringbone.codecs
|
|
91
|
+
# @option options [Integer, nil] :compression_level (nil) level for :zstd, :gzip or :brotli
|
|
86
92
|
# @option options [Integer] :row_group_bytes (16MB) approximate buffered size that triggers a row group
|
|
87
93
|
# @option options [Integer, nil] :row_group_rows (nil) also flush a row group after this many rows
|
|
88
94
|
# @option options [Integer] :page_bytes (1MB) approximate uncompressed data page size
|
|
@@ -95,6 +101,8 @@ module Herringbone
|
|
|
95
101
|
# @option options [Hash{String => String}] :metadata ({}) key/value metadata for the footer
|
|
96
102
|
# @option options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
|
|
97
103
|
# columns to write split block bloom filters for
|
|
104
|
+
# @option options [EncryptionConfiguration, Hash{Symbol => Object}, nil] :encryption (nil) modular
|
|
105
|
+
# encryption settings, see #initialize
|
|
98
106
|
# @yield [writer] the open writer
|
|
99
107
|
# @yieldparam writer [Writer] writer to append rows to
|
|
100
108
|
# @yieldreturn [Object] returned by open
|
|
@@ -118,6 +126,8 @@ module Herringbone
|
|
|
118
126
|
# @param io [IO, #write] destination; switched to binmode when it supports that
|
|
119
127
|
# @param schema [Schema] schema of the rows
|
|
120
128
|
# @param compression [Symbol, Integer] codec name (see Herringbone.codecs) or Format::Codec id
|
|
129
|
+
# @param compression_level [Integer, nil] level for :zstd, :gzip or :brotli (see
|
|
130
|
+
# Compression::LEVELS), nil for the codec's default
|
|
121
131
|
# @param row_group_bytes [Integer] approximate buffered size that triggers a row group
|
|
122
132
|
# @param row_group_rows [Integer, nil] also flush a row group after this many rows
|
|
123
133
|
# @param page_bytes [Integer] approximate uncompressed data page size
|
|
@@ -131,17 +141,35 @@ module Herringbone
|
|
|
131
141
|
# @param metadata [Hash{#to_s => #to_s}] key/value metadata for the footer
|
|
132
142
|
# @param bloom_filters [Boolean, Array<String>, Hash{String => Boolean, Hash}, nil] see the class
|
|
133
143
|
# description
|
|
134
|
-
# @
|
|
144
|
+
# @param encryption [EncryptionConfiguration, Hash{Symbol => Object}, false, nil] encrypts the file
|
|
145
|
+
# (Parquet modular encryption); a Hash holds the keywords of EncryptionConfiguration.new.
|
|
146
|
+
# Keys are 16, 24 or 32-byte Strings; key metadata is stored as given, for readers to find
|
|
147
|
+
# the keys by.
|
|
148
|
+
# @option encryption [String] :footer_key key of the footer, and of columns without their own
|
|
149
|
+
# @option encryption [String, nil] :footer_key_metadata stored for the footer key
|
|
150
|
+
# @option encryption [Hash{String => String, Hash, Symbol}, nil] :columns column path or field
|
|
151
|
+
# name => key, +{key:, key_metadata:}+ or +:footer+. Columns not listed are not encrypted;
|
|
152
|
+
# without this option every column is, with the footer key.
|
|
153
|
+
# @option encryption [Boolean] :plaintext_footer (false) store the footer in the clear (signed),
|
|
154
|
+
# so readers without keys can read the plaintext columns
|
|
155
|
+
# @option encryption [Symbol] :algorithm (:aes_gcm) or :aes_gcm_ctr, which encrypts pages with
|
|
156
|
+
# AES-CTR (faster, but page contents are not authenticated)
|
|
157
|
+
# @option encryption [String, nil] :aad_prefix identity of the file, which readers can check
|
|
158
|
+
# @option encryption [Boolean] :store_aad_prefix (true) false leaves it out of the file, so
|
|
159
|
+
# readers must supply it
|
|
160
|
+
# @raise [ArgumentError] for an invalid IO, schema or option, or a compression level the codec
|
|
161
|
+
# does not take
|
|
135
162
|
# @raise [MissingCodecError] when the codec's optional gem is not loaded
|
|
136
163
|
# @raise [UnsupportedError] when the codec is not supported
|
|
137
|
-
def initialize(io, schema, compression: :snappy, row_group_bytes: 16 * 1024 * 1024, row_group_rows: nil,
|
|
164
|
+
def initialize(io, schema, compression: :snappy, compression_level: nil, row_group_bytes: 16 * 1024 * 1024, row_group_rows: nil,
|
|
138
165
|
page_bytes: 1024 * 1024, page_rows: 20_000, data_page_version: 1, dictionary: true, encodings: {},
|
|
139
|
-
metadata: {}, bloom_filters: nil)
|
|
166
|
+
metadata: {}, bloom_filters: nil, encryption: nil)
|
|
140
167
|
raise ArgumentError, "Expected a Herringbone::Schema, got #{schema.class}" unless schema.is_a?(Schema)
|
|
141
168
|
@schema = schema
|
|
142
169
|
@codec = Compression.codec_id(compression)
|
|
143
170
|
# Fail before creating any file if the codec's library is missing
|
|
144
171
|
Compression.ensure_available!(@codec)
|
|
172
|
+
@compression_level = Compression.check_level!(@codec, compression_level)
|
|
145
173
|
@row_group_bytes = Integer(row_group_bytes)
|
|
146
174
|
@row_group_rows = row_group_rows && Integer(row_group_rows)
|
|
147
175
|
@row_limit = @row_group_rows || ESTIMATE_AFTER_ROWS
|
|
@@ -159,7 +187,8 @@ module Herringbone
|
|
|
159
187
|
@encodings.each { |path, enc| check_encoding!(columns[path], enc) }
|
|
160
188
|
@metadata = metadata
|
|
161
189
|
@bloom_filters = bloom_filter_config(bloom_filters)
|
|
162
|
-
@pending_bloom_filters = [] # [ColumnMetaData, BloomFilter] for the row group being written
|
|
190
|
+
@pending_bloom_filters = [] # [ColumnMetaData, BloomFilter, ModuleCrypto] for the row group being written
|
|
191
|
+
@encryption = encryption ? Encryption::FileEncryptor.new(EncryptionConfiguration.from(encryption), schema) : nil
|
|
163
192
|
@row_groups = []
|
|
164
193
|
@total_rows = 0
|
|
165
194
|
@pos = 0
|
|
@@ -167,7 +196,7 @@ module Herringbone
|
|
|
167
196
|
@aborted = false
|
|
168
197
|
@bytes_per_row = nil
|
|
169
198
|
@io = check_io!(io)
|
|
170
|
-
write_raw(MAGIC)
|
|
199
|
+
write_raw(@encryption ? @encryption.magic : MAGIC)
|
|
171
200
|
reset_buffers
|
|
172
201
|
end
|
|
173
202
|
|
|
@@ -248,6 +277,7 @@ module Herringbone
|
|
|
248
277
|
#
|
|
249
278
|
# @param num_rows [Integer] rows in the row group
|
|
250
279
|
# @param copies [Hash{Integer => CopiedChunk}] column index => chunk to copy instead of encoding
|
|
280
|
+
# (only plaintext chunks, into columns this writer does not encrypt)
|
|
251
281
|
# @param codecs [Hash{Integer => Integer}] column index => codec id for an encoded chunk, instead
|
|
252
282
|
# of the +compression:+ option
|
|
253
283
|
# @param bloom_filters [Hash{Integer => Boolean}] column index => true to give an encoded chunk a
|
|
@@ -257,6 +287,9 @@ module Herringbone
|
|
|
257
287
|
# @raise [Error] when the writer is closed
|
|
258
288
|
def write_row_group(num_rows, copies: {}, codecs: {}, bloom_filters: {}, sorting_columns: nil)
|
|
259
289
|
raise Error, "Writer is closed" if @closed
|
|
290
|
+
if @encryption && (encrypted = copies.keys.find { |i| @encryption.encrypted?(i) })
|
|
291
|
+
raise ArgumentError, "Cannot copy a chunk into the encrypted column #{@schema.columns[encrypted].dotted_path}"
|
|
292
|
+
end
|
|
260
293
|
start = @pos
|
|
261
294
|
chunks = @schema.columns.map do |col|
|
|
262
295
|
if (copy = copies[col.index])
|
|
@@ -281,6 +314,12 @@ module Herringbone
|
|
|
281
314
|
reset_buffers
|
|
282
315
|
end
|
|
283
316
|
|
|
317
|
+
# Internal (used by Redaction): whether the column is encrypted in this file
|
|
318
|
+
#
|
|
319
|
+
# @param index [Integer] leaf column index
|
|
320
|
+
# @return [Boolean]
|
|
321
|
+
def encrypted_column?(index) = @encryption&.encrypted?(index) || false
|
|
322
|
+
|
|
284
323
|
# Internal (used by Redaction): shreds one value per row of a top-level field into the buffers,
|
|
285
324
|
# for #write_row_group. Other fields are left as they are, so the caller decides which columns
|
|
286
325
|
# are encoded and which are copied.
|
|
@@ -307,6 +346,7 @@ module Herringbone
|
|
|
307
346
|
return if @closed
|
|
308
347
|
flush_row_group
|
|
309
348
|
write_page_indexes
|
|
349
|
+
@encryption&.finish(@row_groups)
|
|
310
350
|
meta = Format::FileMetaData.new(
|
|
311
351
|
version: 2,
|
|
312
352
|
schema: @schema.to_elements,
|
|
@@ -316,10 +356,10 @@ module Herringbone
|
|
|
316
356
|
created_by: "herringbone-ruby #{VERSION}",
|
|
317
357
|
column_orders: @schema.columns.map { Format::ColumnOrder.new(type_order: Format::TypeDefinedOrder.new) }
|
|
318
358
|
)
|
|
319
|
-
footer = meta.encode
|
|
359
|
+
footer = @encryption ? @encryption.footer(meta) : meta.encode
|
|
320
360
|
write_raw(footer)
|
|
321
361
|
write_raw([footer.bytesize].pack("V"))
|
|
322
|
-
write_raw(MAGIC)
|
|
362
|
+
write_raw(@encryption ? @encryption.magic : MAGIC)
|
|
323
363
|
@io.flush if @io.respond_to?(:flush)
|
|
324
364
|
@closed = true
|
|
325
365
|
end
|
|
@@ -637,6 +677,7 @@ module Herringbone
|
|
|
637
677
|
@encodings[path] || E::PLAIN
|
|
638
678
|
end
|
|
639
679
|
|
|
680
|
+
crypto = @encryption&.chunk(@row_groups.size, col.index)
|
|
640
681
|
chunk_start = @pos
|
|
641
682
|
uncompressed_total = 0
|
|
642
683
|
dictionary_offset = nil
|
|
@@ -647,7 +688,7 @@ module Herringbone
|
|
|
647
688
|
type: Format::PageType::DICTIONARY_PAGE,
|
|
648
689
|
dictionary_page_header: Format::DictionaryPageHeader.new(num_values: dict_values.size, encoding: E::PLAIN)
|
|
649
690
|
)
|
|
650
|
-
uncompressed_total += write_page(header, plain, codec: codec)
|
|
691
|
+
uncompressed_total += write_page(header, plain, codec: codec, crypto: crypto)
|
|
651
692
|
end
|
|
652
693
|
|
|
653
694
|
data_offset = @pos
|
|
@@ -680,11 +721,12 @@ module Herringbone
|
|
|
680
721
|
encoded = encode_values(page_values, value_encoding, type, col.type_length, dict_values&.size)
|
|
681
722
|
rep_bytes = max_rep.positive? ? Encodings::RLE.encode_hybrid(reps, max_rep.bit_length) : "".b
|
|
682
723
|
def_bytes = max_def.positive? ? Encodings::RLE.encode_hybrid(defs, max_def.bit_length) : "".b
|
|
724
|
+
page_crypto = crypto && [crypto, pages.size]
|
|
683
725
|
uncompressed_total += if @data_page_version == 1
|
|
684
|
-
write_data_page_v1(n, rep_bytes, def_bytes, encoded, value_encoding, codec)
|
|
726
|
+
write_data_page_v1(n, rep_bytes, def_bytes, encoded, value_encoding, codec, page_crypto)
|
|
685
727
|
else
|
|
686
728
|
num_rows = max_rep.zero? ? n : reps.count(0)
|
|
687
|
-
write_data_page_v2(n, n - non_null, num_rows, rep_bytes, def_bytes, encoded, value_encoding, codec)
|
|
729
|
+
write_data_page_v2(n, n - non_null, num_rows, rep_bytes, def_bytes, encoded, value_encoding, codec, page_crypto)
|
|
688
730
|
end
|
|
689
731
|
pages << PageInfo.new(page_offset, @pos - page_offset, first_row, n - non_null, non_null, range)
|
|
690
732
|
first_row += max_rep.zero? ? n : reps.count(0)
|
|
@@ -707,9 +749,9 @@ module Herringbone
|
|
|
707
749
|
)
|
|
708
750
|
chunk = Format::ColumnChunk.new(file_offset: chunk_start, meta_data: meta)
|
|
709
751
|
if bloom
|
|
710
|
-
@pending_bloom_filters << [meta, build_bloom_filter(col, bloom, dict_values, values)]
|
|
752
|
+
@pending_bloom_filters << [meta, build_bloom_filter(col, bloom, dict_values, values), crypto]
|
|
711
753
|
end
|
|
712
|
-
@page_indexes << [chunk, column_index_for(col, pages, order), offset_index_for(pages)]
|
|
754
|
+
@page_indexes << [chunk, column_index_for(col, pages, order), offset_index_for(pages), crypto]
|
|
713
755
|
chunk
|
|
714
756
|
end
|
|
715
757
|
|
|
@@ -805,19 +847,22 @@ module Herringbone
|
|
|
805
847
|
|
|
806
848
|
# Page indexes go after the last row group: all column indexes, then all offset indexes.
|
|
807
849
|
# A copied chunk brings its ColumnIndex already encoded, and may come without either index.
|
|
850
|
+
# Those of encrypted columns are encrypted.
|
|
808
851
|
#
|
|
809
852
|
# @return [void]
|
|
810
853
|
def write_page_indexes
|
|
811
|
-
@page_indexes.each do |chunk, column_index, _|
|
|
854
|
+
@page_indexes.each do |chunk, column_index, _, crypto|
|
|
812
855
|
next unless column_index
|
|
813
856
|
bytes = column_index.is_a?(String) ? column_index : column_index.encode
|
|
857
|
+
bytes = crypto.encrypt(Encryption::COLUMN_INDEX, bytes) if crypto
|
|
814
858
|
chunk.column_index_offset = @pos
|
|
815
859
|
chunk.column_index_length = bytes.bytesize
|
|
816
860
|
write_raw(bytes)
|
|
817
861
|
end
|
|
818
|
-
@page_indexes.each do |chunk, _, offset_index|
|
|
862
|
+
@page_indexes.each do |chunk, _, offset_index, crypto|
|
|
819
863
|
next unless offset_index
|
|
820
864
|
bytes = offset_index.encode
|
|
865
|
+
bytes = crypto.encrypt(Encryption::OFFSET_INDEX, bytes) if crypto
|
|
821
866
|
chunk.offset_index_offset = @pos
|
|
822
867
|
chunk.offset_index_length = bytes.bytesize
|
|
823
868
|
write_raw(bytes)
|
|
@@ -893,12 +938,18 @@ module Herringbone
|
|
|
893
938
|
end
|
|
894
939
|
|
|
895
940
|
# Bloom filters go right after the row group's column chunks, in column order. A copied
|
|
896
|
-
# chunk brings its filter already encoded.
|
|
941
|
+
# chunk brings its filter already encoded. In an encrypted column the header and the bitset
|
|
942
|
+
# are encrypted separately.
|
|
897
943
|
#
|
|
898
944
|
# @return [void]
|
|
899
945
|
def write_bloom_filters
|
|
900
|
-
@pending_bloom_filters.each do |meta, filter|
|
|
901
|
-
bytes =
|
|
946
|
+
@pending_bloom_filters.each do |meta, filter, crypto|
|
|
947
|
+
bytes = if crypto
|
|
948
|
+
crypto.encrypt(Encryption::BLOOM_FILTER_HEADER, filter.header.encode) <<
|
|
949
|
+
crypto.encrypt(Encryption::BLOOM_FILTER_BITSET, filter.bitset)
|
|
950
|
+
else
|
|
951
|
+
filter.is_a?(String) ? filter : filter.encode
|
|
952
|
+
end
|
|
902
953
|
meta.bloom_filter_offset = @pos
|
|
903
954
|
meta.bloom_filter_length = bytes.bytesize
|
|
904
955
|
write_raw(bytes)
|
|
@@ -1015,18 +1066,40 @@ module Herringbone
|
|
|
1015
1066
|
|
|
1016
1067
|
# Writes a page, returning the uncompressed size including the header
|
|
1017
1068
|
#
|
|
1069
|
+
# Compresses with the configured level when +codec+ is the writer's own; chunks a redaction
|
|
1070
|
+
# keeps in their source codec use that codec's default
|
|
1071
|
+
#
|
|
1072
|
+
# @param codec [Integer] codec id
|
|
1073
|
+
# @param data [String] bytes to compress
|
|
1074
|
+
# @return [String] compressed bytes
|
|
1075
|
+
def compress(codec, data)
|
|
1076
|
+
Compression.compress(codec, data, (codec == @codec) ? @compression_level : nil)
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
# In an encrypted column the body and the header are encrypted; the CRC covers the body as
|
|
1080
|
+
# stored, encrypted.
|
|
1081
|
+
#
|
|
1018
1082
|
# @param header [Format::PageHeader] header; sizes and CRC32 are filled in here
|
|
1019
1083
|
# @param body [String] uncompressed page body
|
|
1020
1084
|
# @param compressed [String, nil] bytes to write as the page body, when already prepared (v2 data
|
|
1021
1085
|
# pages, whose levels stay uncompressed); +body+ is compressed otherwise
|
|
1022
1086
|
# @param codec [Integer] codec id to compress +body+ with
|
|
1087
|
+
# @param crypto [Encryption::ModuleCrypto, nil] encryption of the column's modules
|
|
1088
|
+
# @param ordinal [Integer, nil] data page ordinal within the chunk; nil for a dictionary page
|
|
1023
1089
|
# @return [Integer]
|
|
1024
|
-
def write_page(header, body, compressed = nil, codec: @codec)
|
|
1025
|
-
compressed ||=
|
|
1090
|
+
def write_page(header, body, compressed = nil, codec: @codec, crypto: nil, ordinal: nil)
|
|
1091
|
+
compressed ||= compress(codec, body)
|
|
1026
1092
|
header.uncompressed_page_size ||= body.bytesize
|
|
1093
|
+
dictionary = header.type == Format::PageType::DICTIONARY_PAGE
|
|
1094
|
+
if crypto
|
|
1095
|
+
compressed = crypto.encrypt(dictionary ? Encryption::DICTIONARY_PAGE : Encryption::DATA_PAGE, compressed, ordinal)
|
|
1096
|
+
end
|
|
1027
1097
|
header.compressed_page_size = compressed.bytesize
|
|
1028
1098
|
header.crc = Zlib.crc32(compressed).then { |c| (c >= 0x8000_0000) ? c - 0x1_0000_0000 : c }
|
|
1029
1099
|
encoded = header.encode
|
|
1100
|
+
if crypto
|
|
1101
|
+
encoded = crypto.encrypt(dictionary ? Encryption::DICTIONARY_PAGE_HEADER : Encryption::DATA_PAGE_HEADER, encoded, ordinal)
|
|
1102
|
+
end
|
|
1030
1103
|
write_raw(encoded)
|
|
1031
1104
|
write_raw(compressed)
|
|
1032
1105
|
encoded.bytesize + header.uncompressed_page_size
|
|
@@ -1040,8 +1113,10 @@ module Herringbone
|
|
|
1040
1113
|
# @param encoded [String] encoded values
|
|
1041
1114
|
# @param encoding [Integer] encoding id of the values
|
|
1042
1115
|
# @param codec [Integer] codec id to compress the page with
|
|
1116
|
+
# @param page_crypto [Array(Encryption::ModuleCrypto, Integer), nil] encryption of the column's
|
|
1117
|
+
# modules and the page's ordinal, for an encrypted column
|
|
1043
1118
|
# @return [Integer] uncompressed size including the header
|
|
1044
|
-
def write_data_page_v1(n, rep_bytes, def_bytes, encoded, encoding, codec)
|
|
1119
|
+
def write_data_page_v1(n, rep_bytes, def_bytes, encoded, encoding, codec, page_crypto)
|
|
1045
1120
|
body = String.new(encoding: Encoding::BINARY)
|
|
1046
1121
|
body << [rep_bytes.bytesize].pack("V") << rep_bytes unless rep_bytes.empty?
|
|
1047
1122
|
body << [def_bytes.bytesize].pack("V") << def_bytes unless def_bytes.empty?
|
|
@@ -1053,7 +1128,7 @@ module Herringbone
|
|
|
1053
1128
|
definition_level_encoding: E::RLE, repetition_level_encoding: E::RLE
|
|
1054
1129
|
)
|
|
1055
1130
|
)
|
|
1056
|
-
write_page(header, body, codec: codec)
|
|
1131
|
+
write_page(header, body, codec: codec, crypto: page_crypto&.first, ordinal: page_crypto&.last)
|
|
1057
1132
|
end
|
|
1058
1133
|
|
|
1059
1134
|
# A DATA_PAGE_V2: levels without length prefixes and uncompressed, then the compressed values
|
|
@@ -1066,9 +1141,11 @@ module Herringbone
|
|
|
1066
1141
|
# @param encoded [String] encoded values
|
|
1067
1142
|
# @param encoding [Integer] encoding id of the values
|
|
1068
1143
|
# @param codec [Integer] codec id to compress the values with
|
|
1144
|
+
# @param page_crypto [Array(Encryption::ModuleCrypto, Integer), nil] encryption of the column's
|
|
1145
|
+
# modules and the page's ordinal, for an encrypted column
|
|
1069
1146
|
# @return [Integer] uncompressed size including the header
|
|
1070
|
-
def write_data_page_v2(n, nulls, rows, rep_bytes, def_bytes, encoded, encoding, codec)
|
|
1071
|
-
compressed =
|
|
1147
|
+
def write_data_page_v2(n, nulls, rows, rep_bytes, def_bytes, encoded, encoding, codec, page_crypto)
|
|
1148
|
+
compressed = compress(codec, encoded)
|
|
1072
1149
|
header = Format::PageHeader.new(
|
|
1073
1150
|
type: Format::PageType::DATA_PAGE_V2,
|
|
1074
1151
|
uncompressed_page_size: rep_bytes.bytesize + def_bytes.bytesize + encoded.bytesize,
|
|
@@ -1079,7 +1156,8 @@ module Herringbone
|
|
|
1079
1156
|
is_compressed: codec != Format::Codec::UNCOMPRESSED
|
|
1080
1157
|
)
|
|
1081
1158
|
)
|
|
1082
|
-
write_page(header, "".b, rep_bytes + def_bytes + compressed, codec: codec
|
|
1159
|
+
write_page(header, "".b, rep_bytes + def_bytes + compressed, codec: codec, crypto: page_crypto&.first,
|
|
1160
|
+
ordinal: page_crypto&.last)
|
|
1083
1161
|
end
|
|
1084
1162
|
|
|
1085
1163
|
# Statistics and column index bounds longer than this are truncated, see #truncate_min
|
data/lib/herringbone.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
|
-
require "pathname"
|
|
5
4
|
require_relative "herringbone/version"
|
|
6
5
|
|
|
7
6
|
# Pure-Ruby reader and writer for Apache Parquet files
|
|
@@ -38,33 +37,76 @@ module Herringbone
|
|
|
38
37
|
class SchemaMismatch < EncodeError; end
|
|
39
38
|
|
|
40
39
|
# The file (or the writer configuration) uses a Parquet feature Herringbone does not implement,
|
|
41
|
-
# such as
|
|
40
|
+
# such as a codec whose library is unavailable
|
|
42
41
|
class UnsupportedError < Error; end
|
|
42
|
+
|
|
43
|
+
# An encrypted file or column cannot be read: its key was not given, the key or AAD prefix is
|
|
44
|
+
# wrong, or the encrypted bytes were changed
|
|
45
|
+
class DecryptionError < Error; end
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
48
|
+
module Herringbone
|
|
49
|
+
# Everything else loads on first use, so a process that only reads plain files never loads the
|
|
50
|
+
# writer, the inspector or OpenSSL. Herringbone.eager_load! loads it all (Ractors on Ruby < 3.4
|
|
51
|
+
# need that: they cannot autoload).
|
|
52
|
+
|
|
53
|
+
# Directory holding the files that are autoloaded
|
|
54
|
+
LIB = File.expand_path("herringbone", __dir__)
|
|
55
|
+
private_constant :LIB
|
|
56
|
+
|
|
57
|
+
autoload :BloomFilter, "#{LIB}/bloom_filter"
|
|
58
|
+
autoload :ByteValues, "#{LIB}/byte_values"
|
|
59
|
+
autoload :Compression, "#{LIB}/compression"
|
|
60
|
+
autoload :DecryptionConfiguration, "#{LIB}/encryption_configuration"
|
|
61
|
+
autoload :Encryption, "#{LIB}/encryption"
|
|
62
|
+
autoload :EncryptionConfiguration, "#{LIB}/encryption_configuration"
|
|
63
|
+
autoload :Format, "#{LIB}/format"
|
|
64
|
+
autoload :InferringWriter, "#{LIB}/inferring_writer"
|
|
65
|
+
autoload :Inspector, "#{LIB}/inspector"
|
|
66
|
+
autoload :IOBufferSupport, "#{LIB}/io_buffer_support"
|
|
67
|
+
autoload :Key, "#{LIB}/key"
|
|
68
|
+
autoload :MissingCodecError, "#{LIB}/compression"
|
|
69
|
+
autoload :Reader, "#{LIB}/reader"
|
|
70
|
+
autoload :Redaction, "#{LIB}/redaction"
|
|
71
|
+
autoload :Schema, "#{LIB}/schema"
|
|
72
|
+
autoload :SimpleWriter, "#{LIB}/simple_writer"
|
|
73
|
+
autoload :Thrift, "#{LIB}/thrift"
|
|
74
|
+
autoload :Types, "#{LIB}/types"
|
|
75
|
+
autoload :Visualizer, "#{LIB}/visualizer"
|
|
76
|
+
autoload :Writer, "#{LIB}/writer"
|
|
77
|
+
autoload :XXHash, "#{LIB}/xxhash"
|
|
78
|
+
|
|
79
|
+
module Codecs
|
|
80
|
+
autoload :LZ4, "#{LIB}/codecs/lz4"
|
|
81
|
+
autoload :LZO, "#{LIB}/codecs/lzo"
|
|
82
|
+
autoload :Snappy, "#{LIB}/codecs/snappy"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
module Encodings
|
|
86
|
+
autoload :ByteStreamSplit, "#{LIB}/encodings/delta"
|
|
87
|
+
autoload :Delta, "#{LIB}/encodings/delta"
|
|
88
|
+
autoload :Plain, "#{LIB}/encodings/plain"
|
|
89
|
+
autoload :RLE, "#{LIB}/encodings/rle"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Loads every part of Herringbone now instead of on first use. Ractors cannot autoload before
|
|
93
|
+
# Ruby 3.4, so call this before starting Ractors that use Herringbone there; it also moves the
|
|
94
|
+
# loading out of the first request in a forking server.
|
|
95
|
+
#
|
|
96
|
+
# @return [void]
|
|
97
|
+
def self.eager_load!
|
|
98
|
+
seen = {}
|
|
99
|
+
walk = lambda do |mod|
|
|
100
|
+
next if seen[mod]
|
|
101
|
+
seen[mod] = true
|
|
102
|
+
mod.constants(false).each do |name|
|
|
103
|
+
value = mod.const_get(name, false)
|
|
104
|
+
walk.call(value) if value.is_a?(Module) && value.name&.start_with?("Herringbone::")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
walk.call(self)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
68
110
|
|
|
69
111
|
module Herringbone
|
|
70
112
|
module_function
|
|
@@ -86,6 +128,7 @@ module Herringbone
|
|
|
86
128
|
# @param schema [Schema, nil] schema to write with; derived from +records+ when nil
|
|
87
129
|
# @param options [Hash{Symbol => Object}] passed to Writer.new
|
|
88
130
|
# @option options [Symbol] :compression (:snappy) codec, see Herringbone.codecs
|
|
131
|
+
# @option options [Integer, nil] :compression_level (nil) level for :zstd, :gzip or :brotli
|
|
89
132
|
# @option options [Integer] :row_group_bytes (16MB) approximate buffered size that triggers a row group
|
|
90
133
|
# @option options [Integer, nil] :row_group_rows (nil) also flush a row group after this many rows
|
|
91
134
|
# @option options [Integer] :page_bytes (1MB) approximate uncompressed data page size
|
|
@@ -147,6 +190,7 @@ module Herringbone
|
|
|
147
190
|
# @param writer_options [Hash{Symbol => Object}] Writer options for re-encoded column chunks, and
|
|
148
191
|
# +metadata:+ to replace the footer key/value metadata, see Redaction#apply
|
|
149
192
|
# @option writer_options [Symbol] :compression (codec of each source chunk) codec for re-encoded chunks
|
|
193
|
+
# @option writer_options [Integer, nil] :compression_level (nil) level for that codec, see Writer
|
|
150
194
|
# @option writer_options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
|
|
151
195
|
# columns whose re-encoded chunks get a bloom filter, besides those whose source chunk had one
|
|
152
196
|
# @option writer_options [Hash{String => String}] :metadata (the input's) footer key/value metadata
|
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.6.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-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bigdecimal
|
|
@@ -35,6 +35,7 @@ extra_rdoc_files: []
|
|
|
35
35
|
files:
|
|
36
36
|
- CHANGELOG.md
|
|
37
37
|
- LICENSE
|
|
38
|
+
- MANUAL.md
|
|
38
39
|
- README.md
|
|
39
40
|
- bin/herringbone
|
|
40
41
|
- lib/herringbone.rb
|
|
@@ -48,11 +49,15 @@ files:
|
|
|
48
49
|
- lib/herringbone/encodings/delta.rb
|
|
49
50
|
- lib/herringbone/encodings/plain.rb
|
|
50
51
|
- lib/herringbone/encodings/rle.rb
|
|
52
|
+
- lib/herringbone/encryption.rb
|
|
53
|
+
- lib/herringbone/encryption_configuration.rb
|
|
51
54
|
- lib/herringbone/format.rb
|
|
52
55
|
- lib/herringbone/inferring_writer.rb
|
|
53
56
|
- lib/herringbone/inspector.rb
|
|
54
57
|
- lib/herringbone/io_buffer_support.rb
|
|
58
|
+
- lib/herringbone/key.rb
|
|
55
59
|
- lib/herringbone/reader.rb
|
|
60
|
+
- lib/herringbone/reader/bloom_filters.rb
|
|
56
61
|
- lib/herringbone/reader/column_chunk_reader.rb
|
|
57
62
|
- lib/herringbone/reader/column_cursor.rb
|
|
58
63
|
- lib/herringbone/reader/numo.rb
|