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.
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 encryption or a codec whose library is unavailable
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
- require_relative "herringbone/io_buffer_support"
46
- require_relative "herringbone/codecs/snappy"
47
- require_relative "herringbone/codecs/lz4"
48
- require_relative "herringbone/codecs/lzo"
49
- require_relative "herringbone/thrift"
50
- require_relative "herringbone/format"
51
- require_relative "herringbone/encodings/rle"
52
- require_relative "herringbone/encodings/plain"
53
- require_relative "herringbone/encodings/delta"
54
- require_relative "herringbone/compression"
55
- require_relative "herringbone/types"
56
- require_relative "herringbone/schema"
57
- require_relative "herringbone/active_record"
58
- require_relative "herringbone/reader"
59
- require_relative "herringbone/byte_values"
60
- require_relative "herringbone/writer"
61
- require_relative "herringbone/inferring_writer"
62
- require_relative "herringbone/simple_writer"
63
- require_relative "herringbone/xxhash"
64
- require_relative "herringbone/bloom_filter"
65
- require_relative "herringbone/inspector"
66
- require_relative "herringbone/visualizer"
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 block for Schema.infer (Builder DSL), declaring fields that replace inferred ones;
101
- # ignored when the schema is not inferred
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, or an option is invalid
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) { %s }"
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.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-02 00:00:00.000000000 Z
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