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