herringbone 0.4.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 +42 -0
- data/MANUAL.md +773 -0
- data/README.md +81 -316
- data/bin/herringbone +86 -7
- data/lib/herringbone/active_record.rb +1 -1
- data/lib/herringbone/bloom_filter.rb +0 -55
- data/lib/herringbone/byte_values.rb +9 -0
- 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 +24 -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 +585 -0
- data/lib/herringbone/redaction.rb +302 -0
- data/lib/herringbone/schema.rb +123 -57
- data/lib/herringbone/simple_writer.rb +42 -4
- data/lib/herringbone/version.rb +1 -1
- data/lib/herringbone/writer.rb +231 -37
- data/lib/herringbone.rb +115 -29
- metadata +9 -2
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,32 +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
|
-
|
|
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
|
|
67
110
|
|
|
68
111
|
module Herringbone
|
|
69
112
|
module_function
|
|
@@ -77,7 +120,7 @@ module Herringbone
|
|
|
77
120
|
# to Writer.
|
|
78
121
|
#
|
|
79
122
|
# File.open("orders.parquet", "wb") { |f| Herringbone.write(f, Order.where(created_at: 1.year.ago..)) }
|
|
80
|
-
# Herringbone.write(io, events.lazy.map(&:to_h)) { json :payload }
|
|
123
|
+
# Herringbone.write(io, events.lazy.map(&:to_h)) { |s| s.json :payload }
|
|
81
124
|
#
|
|
82
125
|
# @param io [IO, #write] destination; written sequentially, never closed
|
|
83
126
|
# @param records [Enumerable<Hash, Array, Object>, Class, #find_each] rows (Hashes, Arrays in schema
|
|
@@ -85,6 +128,7 @@ module Herringbone
|
|
|
85
128
|
# @param schema [Schema, nil] schema to write with; derived from +records+ when nil
|
|
86
129
|
# @param options [Hash{Symbol => Object}] passed to Writer.new
|
|
87
130
|
# @option options [Symbol] :compression (:snappy) codec, see Herringbone.codecs
|
|
131
|
+
# @option options [Integer, nil] :compression_level (nil) level for :zstd, :gzip or :brotli
|
|
88
132
|
# @option options [Integer] :row_group_bytes (16MB) approximate buffered size that triggers a row group
|
|
89
133
|
# @option options [Integer, nil] :row_group_rows (nil) also flush a row group after this many rows
|
|
90
134
|
# @option options [Integer] :page_bytes (1MB) approximate uncompressed data page size
|
|
@@ -97,14 +141,17 @@ module Herringbone
|
|
|
97
141
|
# @option options [Hash{String => String}] :metadata ({}) key/value metadata for the footer
|
|
98
142
|
# @option options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
|
|
99
143
|
# columns to write split block bloom filters for, see Writer
|
|
100
|
-
# @yield optional
|
|
101
|
-
#
|
|
144
|
+
# @yield [s] optional, declares fields that replace inferred ones (see Schema.infer); ignored when
|
|
145
|
+
# the schema is not inferred
|
|
146
|
+
# @yieldparam s [Schema::Builder] the builder to declare fields on
|
|
147
|
+
# @yieldreturn [void]
|
|
102
148
|
# @return [Integer] number of rows written
|
|
103
149
|
# @raise [ArgumentError] when the schema has to be inferred and +records+ is empty or holds Array
|
|
104
|
-
# rows,
|
|
150
|
+
# rows, an option is invalid, or the block takes no parameter
|
|
105
151
|
# @raise [EncodeError] when a row does not fit the schema
|
|
106
152
|
# @raise [SchemaMismatch] when a row does not fit the inferred schema; the file is left unfinished
|
|
107
153
|
def write(io, records, schema: nil, **options, &overrides)
|
|
154
|
+
Schema::Builder.check_block!(overrides, "Herringbone.write(io, rows) { |s| s.json :payload }")
|
|
108
155
|
model = if records.respond_to?(:klass) then records.klass
|
|
109
156
|
elsif records.respond_to?(:columns) && records.respond_to?(:find_each) then records
|
|
110
157
|
end
|
|
@@ -112,7 +159,7 @@ module Herringbone
|
|
|
112
159
|
writer = if schema
|
|
113
160
|
Writer.new(io, schema, **options)
|
|
114
161
|
else
|
|
115
|
-
fix = "Herringbone.write(io, rows) {
|
|
162
|
+
fix = "Herringbone.write(io, rows) { |s| s.%s }"
|
|
116
163
|
InferringWriter.new(io, fix: fix, **options) { |sample| Schema.infer(sample, &overrides) }
|
|
117
164
|
end
|
|
118
165
|
begin
|
|
@@ -129,6 +176,45 @@ module Herringbone
|
|
|
129
176
|
writer.rows_written
|
|
130
177
|
end
|
|
131
178
|
|
|
179
|
+
# Rewrites +input+ into +output+ with rows removed or values replaced, building the Redaction from
|
|
180
|
+
# the block (or taking +redaction+). A shortcut for Redaction#apply.
|
|
181
|
+
#
|
|
182
|
+
# Herringbone.redact(input, output) do |r|
|
|
183
|
+
# r.where(user_id: 42).delete
|
|
184
|
+
# r.replace(:email) { |email| email && OpenSSL::HMAC.hexdigest("SHA256", KEY, email) }
|
|
185
|
+
# end
|
|
186
|
+
#
|
|
187
|
+
# @param input [IO, StringIO] the Parquet file, read with #seek and #read; not closed
|
|
188
|
+
# @param output [IO, #write] destination, written sequentially; not closed
|
|
189
|
+
# @param redaction [Redaction, nil] the redaction to apply, instead of a block
|
|
190
|
+
# @param writer_options [Hash{Symbol => Object}] Writer options for re-encoded column chunks, and
|
|
191
|
+
# +metadata:+ to replace the footer key/value metadata, see Redaction#apply
|
|
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
|
|
194
|
+
# @option writer_options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
|
|
195
|
+
# columns whose re-encoded chunks get a bloom filter, besides those whose source chunk had one
|
|
196
|
+
# @option writer_options [Hash{String => String}] :metadata (the input's) footer key/value metadata
|
|
197
|
+
# @option writer_options [Integer] :page_bytes (1MB) approximate uncompressed data page size
|
|
198
|
+
# @option writer_options [Integer] :page_rows (20_000) maximum rows per data page
|
|
199
|
+
# @option writer_options [Integer] :data_page_version (1) 1 or 2
|
|
200
|
+
# @option writer_options [Boolean, Array<String>] :dictionary (true) see Writer
|
|
201
|
+
# @option writer_options [Hash{String => Symbol}] :encodings ({}) see Writer
|
|
202
|
+
# @yield [r] declares the statements on a new Redaction (see Redaction.new)
|
|
203
|
+
# @yieldparam r [Redaction] the redaction being built
|
|
204
|
+
# @yieldreturn [void]
|
|
205
|
+
# @return [Redaction::Report] what was done
|
|
206
|
+
# @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
|
|
208
|
+
# @raise [EncodeError] when a replacement value cannot be written to its column
|
|
209
|
+
def redact(input, output, redaction = nil, **writer_options, &block)
|
|
210
|
+
raise ArgumentError, "Herringbone.redact takes a Redaction or a block, not both" if redaction && block
|
|
211
|
+
raise ArgumentError, "Herringbone.redact needs a Redaction or a block" unless redaction || block
|
|
212
|
+
if block&.arity&.zero?
|
|
213
|
+
raise ArgumentError, "The block receives the redaction: Herringbone.redact(input, output) { |r| r.where(user_id: 42).delete }"
|
|
214
|
+
end
|
|
215
|
+
(redaction || Redaction.new(&block)).apply(input, output, **writer_options)
|
|
216
|
+
end
|
|
217
|
+
|
|
132
218
|
# Compression codecs this process can read and write, e.g. [:none, :snappy, :gzip, :lz4, :lz4_hadoop, :zstd].
|
|
133
219
|
# :zstd and :brotli are listed when the zstd-ruby / brotli gems are loaded.
|
|
134
220
|
#
|
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,16 +49,22 @@ 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
|
|
59
64
|
- lib/herringbone/reader/page_stream.rb
|
|
60
65
|
- lib/herringbone/reader/scan.rb
|
|
66
|
+
- lib/herringbone/redaction.rb
|
|
67
|
+
- lib/herringbone/redaction/rewriter.rb
|
|
61
68
|
- lib/herringbone/schema.rb
|
|
62
69
|
- lib/herringbone/simple_writer.rb
|
|
63
70
|
- lib/herringbone/thrift.rb
|