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
|
@@ -300,8 +300,6 @@ module Herringbone
|
|
|
300
300
|
"#<#{self.class.name} #{num_bytes} bytes#{" for #{@column.dotted_path}" if @column}>"
|
|
301
301
|
end
|
|
302
302
|
|
|
303
|
-
private
|
|
304
|
-
|
|
305
303
|
# Header for this filter: split block algorithm, XXH64 hash, uncompressed
|
|
306
304
|
#
|
|
307
305
|
# @return [Format::BloomFilterHeader] the header
|
|
@@ -314,57 +312,4 @@ module Herringbone
|
|
|
314
312
|
)
|
|
315
313
|
end
|
|
316
314
|
end
|
|
317
|
-
|
|
318
|
-
class Reader
|
|
319
|
-
# Internal (used by reads with where:): the bloom filter of a column chunk. +column+ is a
|
|
320
|
-
# dotted path ("a.b"), an Array path or a Schema::Column. Returns a BloomFilter, or nil when
|
|
321
|
-
# the chunk has none (or one of an unknown kind).
|
|
322
|
-
#
|
|
323
|
-
# @param row_group_index [Integer] index of the row group
|
|
324
|
-
# @param column [String, Array<String, Symbol>, Symbol, Schema::Column] the leaf column
|
|
325
|
-
# @return [BloomFilter, nil] the filter, or nil when there is none or it is unsupported
|
|
326
|
-
# @raise [IndexError] when there is no such row group
|
|
327
|
-
# @raise [ArgumentError] when there is no such column
|
|
328
|
-
# @raise [FormatError] when the filter is truncated or its header cannot be decoded
|
|
329
|
-
def bloom_filter(row_group_index, column)
|
|
330
|
-
col = bloom_filter_column(column)
|
|
331
|
-
rg = row_groups.fetch(row_group_index) { raise IndexError, "No row group #{row_group_index}" }
|
|
332
|
-
meta = rg.columns.fetch(col.index).meta_data
|
|
333
|
-
offset = meta&.bloom_filter_offset
|
|
334
|
-
return nil unless offset
|
|
335
|
-
length = meta.bloom_filter_length
|
|
336
|
-
@io.seek(offset)
|
|
337
|
-
if length
|
|
338
|
-
buf = @io.read(length)
|
|
339
|
-
raise FormatError, "Truncated bloom filter" if buf.nil? || buf.bytesize < length
|
|
340
|
-
return BloomFilter.decode(buf, column: col)
|
|
341
|
-
end
|
|
342
|
-
# Without a length (older writers), read the header first, then the bitset it announces
|
|
343
|
-
head = @io.read(256) || "".b
|
|
344
|
-
reader = Thrift::Reader.new(head)
|
|
345
|
-
header = reader.read_struct(Format::BloomFilterHeader)
|
|
346
|
-
return nil unless BloomFilter.supported_header?(header)
|
|
347
|
-
@io.seek(offset + reader.pos)
|
|
348
|
-
bitset = @io.read(header.num_bytes)
|
|
349
|
-
raise FormatError, "Truncated bloom filter" if bitset.nil? || bitset.bytesize != header.num_bytes
|
|
350
|
-
BloomFilter.new(bitset: bitset, column: col)
|
|
351
|
-
rescue Thrift::Error => e
|
|
352
|
-
raise FormatError, "Corrupt bloom filter header for #{col.dotted_path}: #{e.message}"
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
private
|
|
356
|
-
|
|
357
|
-
# Resolves the +column+ argument of #bloom_filter to a leaf column
|
|
358
|
-
#
|
|
359
|
-
# @param column [String, Array<String, Symbol>, Symbol, Schema::Column] dotted path, path
|
|
360
|
-
# Array or column
|
|
361
|
-
# @return [Schema::Column] the column
|
|
362
|
-
# @raise [ArgumentError] when the schema has no such column
|
|
363
|
-
def bloom_filter_column(column)
|
|
364
|
-
return column if column.is_a?(Schema::Column)
|
|
365
|
-
col = schema.column(column.is_a?(Array) ? column.map(&:to_s) : column.to_s)
|
|
366
|
-
raise ArgumentError, "No column #{column.inspect}" unless col
|
|
367
|
-
col
|
|
368
|
-
end
|
|
369
|
-
end
|
|
370
315
|
end
|
|
@@ -45,6 +45,15 @@ module Herringbone
|
|
|
45
45
|
# @return [Boolean] true while still dictionary-encoding (not yet switched to raw bytes)
|
|
46
46
|
def dictionary? = !@indices.nil?
|
|
47
47
|
|
|
48
|
+
# Short summary for the console, without the values
|
|
49
|
+
#
|
|
50
|
+
# @return [String] value count, the dictionary size (or "raw" after switching to raw bytes),
|
|
51
|
+
# the width for FIXED_LEN_BYTE_ARRAY and the estimated memory
|
|
52
|
+
def inspect
|
|
53
|
+
mode = dictionary? ? "dictionary=#{@dictionary.size}" : "raw"
|
|
54
|
+
"#<#{self.class.name} size=#{size} #{mode}#{" width=#{@width}" if @width} memory_bytes=#{memory_bytes}>"
|
|
55
|
+
end
|
|
56
|
+
|
|
48
57
|
# Appends a value, switching to raw bytes when the dictionary limits are exceeded.
|
|
49
58
|
# @param value [String] bytes of the value; for FIXED_LEN_BYTE_ARRAY it must be +width+ bytes
|
|
50
59
|
# long (not checked here)
|
|
@@ -83,6 +83,28 @@ module Herringbone
|
|
|
83
83
|
# Codec name => codec id, the inverse of NAMES
|
|
84
84
|
CODECS_BY_NAME = NAMES.invert.freeze
|
|
85
85
|
|
|
86
|
+
# Codec id => compression levels it accepts, for the writer's compression_level: option
|
|
87
|
+
LEVELS = Ractor.make_shareable({
|
|
88
|
+
Format::Codec::ZSTD => (-(1 << 17))..22,
|
|
89
|
+
Format::Codec::GZIP => 0..9,
|
|
90
|
+
Format::Codec::BROTLI => 0..11
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
# Checks that +codec+ takes a compression level and that +level+ is in its range
|
|
94
|
+
#
|
|
95
|
+
# @param codec [Integer] Format::Codec id
|
|
96
|
+
# @param level [Integer, nil] compression level, nil for the codec's default
|
|
97
|
+
# @return [Integer, nil] the level
|
|
98
|
+
# @raise [ArgumentError] when the codec has no levels or +level+ is out of its range
|
|
99
|
+
def check_level!(codec, level)
|
|
100
|
+
return if level.nil?
|
|
101
|
+
name = NAMES.fetch(codec, codec).inspect
|
|
102
|
+
range = LEVELS.fetch(codec) { raise ArgumentError, "#{name} compression has no levels" }
|
|
103
|
+
level = Integer(level)
|
|
104
|
+
raise ArgumentError, "#{name} compression level must be in #{range}, got #{level}" unless range.cover?(level)
|
|
105
|
+
level
|
|
106
|
+
end
|
|
107
|
+
|
|
86
108
|
# Codec id for a codec name; Integers are taken to be ids already and returned unchecked
|
|
87
109
|
#
|
|
88
110
|
# @param name [Integer, Symbol, String] codec id, or a name from NAMES (case-insensitive)
|
|
@@ -129,18 +151,19 @@ module Herringbone
|
|
|
129
151
|
#
|
|
130
152
|
# @param codec [Integer] Format::Codec id
|
|
131
153
|
# @param data [String] bytes to compress
|
|
154
|
+
# @param level [Integer, nil] compression level checked with check_level!, nil for the default
|
|
132
155
|
# @return [String] compressed bytes (binary)
|
|
133
156
|
# @raise [UnsupportedError] for a codec herringbone does not implement
|
|
134
157
|
# @raise [MissingCodecError] when the codec's gem is not loaded
|
|
135
|
-
def compress(codec, data)
|
|
158
|
+
def compress(codec, data, level = nil)
|
|
136
159
|
case codec
|
|
137
160
|
when Format::Codec::UNCOMPRESSED then data
|
|
138
161
|
when Format::Codec::SNAPPY then Codecs::Snappy.compress(data)
|
|
139
|
-
when Format::Codec::GZIP then Zlib.gzip(data)
|
|
162
|
+
when Format::Codec::GZIP then level ? Zlib.gzip(data, level: level) : Zlib.gzip(data)
|
|
140
163
|
when Format::Codec::LZ4_RAW then Codecs::LZ4.compress_block(data)
|
|
141
164
|
when Format::Codec::LZ4 then Codecs::LZ4.compress_hadoop(data)
|
|
142
|
-
when Format::Codec::ZSTD then library(codec).compress(data)
|
|
143
|
-
when Format::Codec::BROTLI then library(codec).deflate(data)
|
|
165
|
+
when Format::Codec::ZSTD then level ? library(codec).compress(data, level: level) : library(codec).compress(data)
|
|
166
|
+
when Format::Codec::BROTLI then level ? library(codec).deflate(data, quality: level) : library(codec).deflate(data)
|
|
144
167
|
else
|
|
145
168
|
raise UnsupportedError, "Unsupported compression codec #{Format::Codec::NAMES[codec] || codec}"
|
|
146
169
|
end.b
|