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.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
|