herringbone 0.2.0 → 0.4.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.
@@ -3,9 +3,9 @@
3
3
  module Herringbone
4
4
  # XXH64 (seed 0), the hash Parquet bloom filters use.
5
5
  #
6
- # When the optional "xxhash" gem (a C extension) can be loaded it is used, which is 20-40x
7
- # faster; otherwise hashing is pure Ruby. The gem is only a speedup, so nothing fails without
8
- # it. +XXHash.backend = :ruby+ forces pure Ruby (for tests and benchmarks).
6
+ # When the optional "xxhash" gem (a C extension) is loaded (+require "xxhash"+) it is used,
7
+ # which is 20-40x faster; otherwise hashing is pure Ruby. The gem is only a speedup, so nothing
8
+ # fails without it. +XXHash.backend = :ruby+ forces pure Ruby (for tests and benchmarks).
9
9
  #
10
10
  # The pure-Ruby version keeps every 64-bit value as two 32-bit halves, and multiplies by the
11
11
  # XXH64 primes split into 16-bit pieces, so that no intermediate result leaves the Fixnum range.
@@ -15,13 +15,21 @@ module Herringbone
15
15
  # arithmetic is written once with the small code generators below, whose output is inlined into
16
16
  # the hashing methods: no method calls or allocations in the hot paths.
17
17
  module XXHash
18
+ # 64-bit mask
18
19
  M = 0xFFFF_FFFF_FFFF_FFFF
20
+ # XXH64 PRIME64_1
19
21
  P1 = 11_400_714_785_074_694_791
22
+ # XXH64 PRIME64_2
20
23
  P2 = 14_029_467_366_897_019_727
24
+ # XXH64 PRIME64_3
21
25
  P3 = 1_609_587_929_392_839_161
26
+ # XXH64 PRIME64_4
22
27
  P4 = 9_650_029_242_287_828_579
28
+ # XXH64 PRIME64_5
23
29
  P5 = 2_870_177_450_012_600_261
30
+ # 32-bit mask
24
31
  M32 = 0xFFFF_FFFF
32
+ # unpack format splitting the input into little-endian 32-bit words
25
33
  WORDS = "V*" # frozen, unlike a literal in the generated code
26
34
 
27
35
  # Gem providing the native implementation
@@ -35,8 +43,14 @@ module Herringbone
35
43
  # Shifts are written as multiplications and divisions by powers of two, which YARV has
36
44
  # specialized instructions for (<< and >> on Integers are method calls).
37
45
  #
38
- # (hi:lo) = (hi:lo) * c mod 2**64. Products are at most 48 bits wide: the low halves are
46
+ # (hi:lo) = (hi:lo) * c mod 2^64. Products are at most 48 bits wide: the low halves are
39
47
  # multiplied by 16-bit pieces of c, and only the low 32 bits of the cross terms are kept.
48
+ #
49
+ # @param hi [String] variable holding the high 32 bits
50
+ # @param lo [String] variable holding the low 32 bits
51
+ # @param c [Integer] unsigned 64-bit constant multiplier
52
+ # @param hi_zero [Boolean] whether +hi+ is known to be 0, which drops its cross terms
53
+ # @return [String] Ruby source (uses t0 and t1 as scratch)
40
54
  def mul(hi, lo, c, hi_zero: false)
41
55
  ch = c >> 32
42
56
  cl = c & M32
@@ -51,6 +65,11 @@ module Herringbone
51
65
  end
52
66
 
53
67
  # Rotate (hi:lo) left by r bits (0 < r < 32)
68
+ #
69
+ # @param hi [String] variable holding the high 32 bits
70
+ # @param lo [String] variable holding the low 32 bits
71
+ # @param r [Integer] rotation in bits, 1..31
72
+ # @return [String] Ruby source (uses t0 as scratch)
54
73
  def rotl(hi, lo, r)
55
74
  mask = (1 << (32 - r)) - 1
56
75
  <<~RUBY
@@ -61,6 +80,12 @@ module Herringbone
61
80
  end
62
81
 
63
82
  # (hi:lo) += (a_hi:a_lo), where the addend is two expressions (constants or variables)
83
+ #
84
+ # @param hi [String] variable holding the high 32 bits
85
+ # @param lo [String] variable holding the low 32 bits
86
+ # @param a_hi [String, Integer] expression for the addend's high 32 bits
87
+ # @param a_lo [String, Integer] expression for the addend's low 32 bits
88
+ # @return [String] Ruby source
64
89
  def add(hi, lo, a_hi, a_lo)
65
90
  <<~RUBY
66
91
  #{lo} += #{a_lo}
@@ -69,43 +94,82 @@ module Herringbone
69
94
  RUBY
70
95
  end
71
96
 
97
+ # (hi:lo) += c, mod 2^64
98
+ #
99
+ # @param hi [String] variable holding the high 32 bits
100
+ # @param lo [String] variable holding the low 32 bits
101
+ # @param c [Integer] unsigned 64-bit constant addend
102
+ # @return [String] Ruby source
72
103
  def add_const(hi, lo, c)
73
104
  add(hi, lo, c >> 32, c & M32)
74
105
  end
75
106
 
76
107
  # Assigns the 64-bit constant c to (hi:lo)
108
+ #
109
+ # @param hi [String] variable for the high 32 bits
110
+ # @param lo [String] variable for the low 32 bits
111
+ # @param c [Integer] unsigned 64-bit constant
112
+ # @return [String] Ruby source
77
113
  def set(hi, lo, c)
78
114
  "#{hi} = #{c >> 32}\n#{lo} = #{c & M32}\n"
79
115
  end
80
116
 
81
117
  # XXH64 round with a zero accumulator: (hi:lo) = rotl(lane * P2, 31) * P1
118
+ #
119
+ # @param hi [String] variable holding the lane's high 32 bits, replaced by the result's
120
+ # @param lo [String] variable holding the lane's low 32 bits, replaced by the result's
121
+ # @return [String] Ruby source
82
122
  def round0(hi, lo)
83
123
  mul(hi, lo, P2) + rotl(hi, lo, 31) + mul(hi, lo, P1)
84
124
  end
85
125
 
86
126
  # Stripe round: acc = rotl(acc + lane * P2, 31) * P1, with the lane in (xh:xl)
127
+ #
128
+ # @param hi [String] variable holding the accumulator's high 32 bits
129
+ # @param lo [String] variable holding the accumulator's low 32 bits
130
+ # @param xh [String] variable holding the lane's high 32 bits (clobbered)
131
+ # @param xl [String] variable holding the lane's low 32 bits (clobbered)
132
+ # @return [String] Ruby source
87
133
  def round(hi, lo, xh, xl)
88
134
  mul(xh, xl, P2) + add(hi, lo, xh, xl) + rotl(hi, lo, 31) + mul(hi, lo, P1)
89
135
  end
90
136
 
91
137
  # h ^= rotl(v * P2, 31) * P1; h = h * P1 + P4, with v in (vh:vl) (left unchanged)
138
+ #
139
+ # @param hi [String] variable holding h's high 32 bits
140
+ # @param lo [String] variable holding h's low 32 bits
141
+ # @param vh [String] variable holding the accumulator v's high 32 bits
142
+ # @param vl [String] variable holding the accumulator v's low 32 bits
143
+ # @return [String] Ruby source (uses xh and xl as scratch)
92
144
  def merge_round(hi, lo, vh, vl)
93
145
  "xh = #{vh}\nxl = #{vl}\n" + round0("xh", "xl") +
94
146
  "#{hi} ^= xh\n#{lo} ^= xl\n" + mul(hi, lo, P1) + add_const(hi, lo, P4)
95
147
  end
96
148
 
97
149
  # Consumes an 8-byte lane in (xh:xl)
150
+ #
151
+ # @param hi [String] variable holding the hash's high 32 bits
152
+ # @param lo [String] variable holding the hash's low 32 bits
153
+ # @return [String] Ruby source
98
154
  def lane8(hi, lo)
99
155
  round0("xh", "xl") + "#{hi} ^= xh\n#{lo} ^= xl\n" + rotl(hi, lo, 27) + mul(hi, lo, P1) + add_const(hi, lo, P4)
100
156
  end
101
157
 
102
158
  # Consumes a 4-byte word in xl
159
+ #
160
+ # @param hi [String] variable holding the hash's high 32 bits
161
+ # @param lo [String] variable holding the hash's low 32 bits
162
+ # @return [String] Ruby source (sets xh)
103
163
  def lane4(hi, lo)
104
164
  "xh = 0\n" + mul("xh", "xl", P1, hi_zero: true) + "#{hi} ^= xh\n#{lo} ^= xl\n" +
105
165
  rotl(hi, lo, 23) + mul(hi, lo, P2) + add_const(hi, lo, P3)
106
166
  end
107
167
 
108
- # Consumes one byte in xl (as it is below 2**16, byte * P5 needs no splitting)
168
+ # Consumes one byte in xl (as it is below 2^16, byte * P5 needs no splitting)
169
+ #
170
+ # @param hi [String] variable holding the hash's high 32 bits
171
+ # @param lo [String] variable holding the hash's low 32 bits
172
+ # @return [String] Ruby source
109
173
  def lane1(hi, lo)
110
174
  <<~RUBY + rotl(hi, lo, 11) + mul(hi, lo, P1)
111
175
  t0 = xl * #{P5 & M32}
@@ -115,12 +179,20 @@ module Herringbone
115
179
  end
116
180
 
117
181
  # Final mix; evaluates to the hash as one Integer
182
+ #
183
+ # @param hi [String] variable holding the hash's high 32 bits
184
+ # @param lo [String] variable holding the hash's low 32 bits
185
+ # @return [String] Ruby source whose last expression is the 64-bit hash
118
186
  def avalanche(hi, lo)
119
187
  "#{lo} ^= #{hi} / 2\n" + mul(hi, lo, P2) +
120
188
  "#{lo} ^= (#{hi} & 0x1FFFFFFF) * 8 | #{lo} / 536870912\n#{hi} ^= #{hi} / 536870912\n" +
121
189
  mul(hi, lo, P3) + "#{lo} ^= #{hi}\n(#{hi} << 32) | #{lo}\n"
122
190
  end
123
191
 
192
+ # Source of the pure-Ruby hashing methods, evaluated into XXHash: +ruby_xxh64(bytes)+,
193
+ # +ruby_xxh64_lane(xh, xl)+ (8 bytes as two 32-bit halves) and +ruby_xxh64_u32(xl)+ (4 bytes)
194
+ #
195
+ # @return [String] Ruby source defining the three singleton methods
124
196
  def source
125
197
  g = self
126
198
  <<~RUBY
@@ -214,38 +286,49 @@ module Herringbone
214
286
 
215
287
  module_eval(Generator.source, __FILE__, __LINE__)
216
288
 
217
- @native = nil # nil: not resolved yet, false: pure Ruby, else the native module
218
- @native_lib = nil
289
+ @native = nil # nil: native when loaded, false: pure Ruby, else the native module
219
290
 
220
291
  class << self
221
292
  # XXH64 of a String's bytes, as an unsigned 64-bit Integer
293
+ #
294
+ # @param bytes [String] data to hash (its encoding is ignored)
295
+ # @return [Integer] the hash, 0...2^64
222
296
  def xxh64(bytes)
223
297
  native = @native
224
- native = resolve_backend if native.nil?
298
+ native = native_library if native.nil?
225
299
  native ? native.xxh64(bytes, 0) : ruby_xxh64(bytes)
226
300
  end
227
301
 
228
302
  # XXH64 of 8 bytes given as a little-endian 64-bit Integer (an INT64 or DOUBLE's PLAIN
229
303
  # encoding), signed or unsigned: only its low 64 bits are used
304
+ #
305
+ # @param lane [Integer] value whose low 64 bits are hashed
306
+ # @return [Integer] the hash, 0...2^64
230
307
  def xxh64_u64(lane)
231
308
  native = @native
232
- native = resolve_backend if native.nil?
309
+ native = native_library if native.nil?
233
310
  return native.xxh64([lane].pack("Q<"), 0) if native
234
311
  ruby_xxh64_lane((lane >> 32) & M32, lane & M32)
235
312
  end
236
313
 
237
314
  # XXH64 of 4 bytes given as a little-endian 32-bit Integer (INT32, FLOAT), signed or unsigned
315
+ #
316
+ # @param word [Integer] value whose low 32 bits are hashed
317
+ # @return [Integer] the hash, 0...2^64
238
318
  def xxh64_u32(word)
239
319
  native = @native
240
- native = resolve_backend if native.nil?
320
+ native = native_library if native.nil?
241
321
  return native.xxh64([word].pack("L<"), 0) if native
242
322
  ruby_xxh64_u32(word & M32)
243
323
  end
244
324
 
245
325
  # Hashes of many 64-bit Integers (low 64 bits of each)
326
+ #
327
+ # @param lanes [Array<Integer>] values to hash, signed or unsigned
328
+ # @return [Array<Integer>] the hashes, in the same order
246
329
  def xxh64_u64_all(lanes)
247
330
  native = @native
248
- native = resolve_backend if native.nil?
331
+ native = native_library if native.nil?
249
332
  if native
250
333
  packed = lanes.pack("Q<*")
251
334
  Array.new(lanes.size) { |i| native.xxh64(packed.byteslice(i << 3, 8), 0) }
@@ -255,9 +338,12 @@ module Herringbone
255
338
  end
256
339
 
257
340
  # Hashes of many 32-bit Integers (low 32 bits of each)
341
+ #
342
+ # @param words [Array<Integer>] values to hash, signed or unsigned
343
+ # @return [Array<Integer>] the hashes, in the same order
258
344
  def xxh64_u32_all(words)
259
345
  native = @native
260
- native = resolve_backend if native.nil?
346
+ native = native_library if native.nil?
261
347
  if native
262
348
  packed = words.pack("L<*")
263
349
  Array.new(words.size) { |i| native.xxh64(packed.byteslice(i << 2, 4), 0) }
@@ -267,52 +353,57 @@ module Herringbone
267
353
  end
268
354
 
269
355
  # Hashes of many Strings
356
+ #
357
+ # @param strings [Array<String>] values whose bytes are hashed
358
+ # @return [Array<Integer>] the hashes, in the same order
270
359
  def xxh64_all(strings)
271
360
  native = @native
272
- native = resolve_backend if native.nil?
361
+ native = native_library if native.nil?
273
362
  native ? strings.map { |s| native.xxh64(s, 0) } : strings.map { |s| ruby_xxh64(s) }
274
363
  end
275
364
 
276
365
  # :native when the xxhash gem is used, :ruby otherwise
366
+ #
367
+ # @return [Symbol] +:native+ or +:ruby+
277
368
  def backend
278
369
  native = @native
279
- native = resolve_backend if native.nil?
370
+ native = native_library if native.nil?
280
371
  native ? :native : :ruby
281
372
  end
282
373
 
283
- # :ruby forces pure Ruby, :native requires the xxhash gem (UnsupportedError if it cannot be
284
- # loaded), nil goes back to the default: native when available
374
+ # :ruby forces pure Ruby, :native the xxhash gem (UnsupportedError if it is not loaded), nil
375
+ # goes back to the default: native when loaded
376
+ #
377
+ # @param name [Symbol, nil] +:ruby+, +:native+ or nil
378
+ # @return [void]
379
+ # @raise [UnsupportedError] for +:native+ when the gem is not loaded
380
+ # @raise [ArgumentError] for any other name
285
381
  def backend=(name)
286
382
  @native = case name
287
383
  when :ruby then false
288
384
  when :native
289
- native_library || raise(UnsupportedError, "The \"#{NATIVE_GEM}\" gem could not be loaded")
385
+ native_library || raise(UnsupportedError, "The \"#{NATIVE_GEM}\" gem is not loaded (require \"#{NATIVE_GEM}\")")
290
386
  when nil then nil
291
387
  else raise ArgumentError, "Unknown XXHash backend #{name.inspect} (expected :ruby, :native or nil)"
292
388
  end
293
389
  end
294
390
 
295
- # Whether the native xxhash gem can be loaded (whatever the selected backend)
391
+ # Whether the native xxhash gem is loaded (whatever the selected backend)
392
+ #
393
+ # @return [Boolean] true when the gem is loaded
296
394
  def native_available?
297
395
  !!native_library
298
396
  end
299
397
 
300
398
  private
301
399
 
302
- def resolve_backend
303
- @native = native_library || false
304
- end
305
-
400
+ # Not memoized, so that it works the same in every Ractor and once the gem is required later
401
+ #
402
+ # @return [Module, false] the gem's module to call +xxh64(data, seed)+ on, or false when it
403
+ # is not loaded
306
404
  def native_library
307
- if @native_lib.nil?
308
- @native_lib = begin
309
- require NATIVE_GEM
310
- defined?(::XXhash::XXhashInternal) ? ::XXhash::XXhashInternal : ::XXhash
311
- rescue LoadError
312
- false
313
- end
314
- end
315
- @native_lib
405
+ return ::XXhash::XXhashInternal if defined?(::XXhash::XXhashInternal)
406
+ defined?(::XXhash.xxh64) ? ::XXhash : false
316
407
  end
317
408
  end
318
409
  end
data/lib/herringbone.rb CHANGED
@@ -6,17 +6,46 @@ require_relative "herringbone/version"
6
6
 
7
7
  # Pure-Ruby reader and writer for Apache Parquet files
8
8
  module Herringbone
9
+ # Base class of every error Herringbone raises on purpose
9
10
  class Error < StandardError; end
10
11
  # The file is not valid Parquet (bad metadata, corrupt pages...)
11
12
  class FormatError < Error; end
13
+
12
14
  # A value cannot be written to its column
13
- class EncodeError < Error; end
15
+ class EncodeError < Error
16
+ # @return [Integer, nil] index of the row being written (0 for the first), when known
17
+ attr_reader :row
18
+ # @return [String, nil] dotted path of the column the value was meant for, when known
19
+ attr_reader :column
20
+ # @return [Object] the value that could not be written (nil when it was a missing required value)
21
+ attr_reader :value
22
+
23
+ # @param message [String, nil] the error message
24
+ # @param row [Integer, nil] index of the row being written
25
+ # @param column [String, nil] dotted path of the column
26
+ # @param value [Object] the value that could not be written
27
+ def initialize(message = nil, row: nil, column: nil, value: nil)
28
+ super(message)
29
+ @row = row
30
+ @column = column
31
+ @value = value
32
+ end
33
+ end
34
+
35
+ # A row does not fit a schema that was inferred from earlier rows (Herringbone.write without
36
+ # +schema:+, SimpleWriter). The message says what was inferred, from how many rows, and how to
37
+ # declare the column instead.
38
+ class SchemaMismatch < EncodeError; end
39
+
40
+ # 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
14
42
  class UnsupportedError < Error; end
15
43
  end
16
44
 
17
45
  require_relative "herringbone/io_buffer_support"
18
46
  require_relative "herringbone/codecs/snappy"
19
47
  require_relative "herringbone/codecs/lz4"
48
+ require_relative "herringbone/codecs/lzo"
20
49
  require_relative "herringbone/thrift"
21
50
  require_relative "herringbone/format"
22
51
  require_relative "herringbone/encodings/rle"
@@ -29,6 +58,8 @@ require_relative "herringbone/active_record"
29
58
  require_relative "herringbone/reader"
30
59
  require_relative "herringbone/byte_values"
31
60
  require_relative "herringbone/writer"
61
+ require_relative "herringbone/inferring_writer"
62
+ require_relative "herringbone/simple_writer"
32
63
  require_relative "herringbone/xxhash"
33
64
  require_relative "herringbone/bloom_filter"
34
65
  require_relative "herringbone/inspector"
@@ -40,27 +71,68 @@ module Herringbone
40
71
  # Writes +records+ to +io+ (any IO responding to #write; Herringbone never opens files by path)
41
72
  # and returns the number of rows written. +records+ is an Enumerable of rows, or an ActiveRecord
42
73
  # model or relation, which is read with find_each. Without +schema+, the schema comes from the
43
- # model's columns (Schema.from_active_record) or is inferred from the first rows (Schema.infer).
44
- # Other options go to Writer.
74
+ # model's columns (Schema.from_active_record) or is inferred from the first rows (Schema.infer);
75
+ # fields declared in the block replace inferred ones. +records+ is iterated once, so a source
76
+ # that can only be read once (a cursor, a lazy Enumerator over an IO) works too. Other options go
77
+ # to Writer.
45
78
  #
46
79
  # File.open("orders.parquet", "wb") { |f| Herringbone.write(f, Order.where(created_at: 1.year.ago..)) }
47
- def write(io, records, schema: nil, **options)
80
+ # Herringbone.write(io, events.lazy.map(&:to_h)) { json :payload }
81
+ #
82
+ # @param io [IO, #write] destination; written sequentially, never closed
83
+ # @param records [Enumerable<Hash, Array, Object>, Class, #find_each] rows (Hashes, Arrays in schema
84
+ # order, or objects responding to #attributes or #to_h), or an ActiveRecord model or relation
85
+ # @param schema [Schema, nil] schema to write with; derived from +records+ when nil
86
+ # @param options [Hash{Symbol => Object}] passed to Writer.new
87
+ # @option options [Symbol] :compression (:snappy) codec, see Herringbone.codecs
88
+ # @option options [Integer] :row_group_bytes (16MB) approximate buffered size that triggers a row group
89
+ # @option options [Integer, nil] :row_group_rows (nil) also flush a row group after this many rows
90
+ # @option options [Integer] :page_bytes (1MB) approximate uncompressed data page size
91
+ # @option options [Integer] :page_rows (20_000) maximum rows per data page
92
+ # @option options [Integer] :data_page_version (1) 1 or 2
93
+ # @option options [Boolean, Array<String>] :dictionary (true) dictionary-encode all eligible columns,
94
+ # none, or only the listed dotted column paths
95
+ # @option options [Hash{String => Symbol}] :encodings ({}) dotted column path => value encoding
96
+ # for non-dictionary pages
97
+ # @option options [Hash{String => String}] :metadata ({}) key/value metadata for the footer
98
+ # @option options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
99
+ # 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
102
+ # @return [Integer] number of rows written
103
+ # @raise [ArgumentError] when the schema has to be inferred and +records+ is empty or holds Array
104
+ # rows, or an option is invalid
105
+ # @raise [EncodeError] when a row does not fit the schema
106
+ # @raise [SchemaMismatch] when a row does not fit the inferred schema; the file is left unfinished
107
+ def write(io, records, schema: nil, **options, &overrides)
48
108
  model = if records.respond_to?(:klass) then records.klass
49
109
  elsif records.respond_to?(:columns) && records.respond_to?(:find_each) then records
50
110
  end
51
- schema ||= model ? Schema.from_active_record(model) : Schema.infer(records)
52
- Writer.open(io, schema, **options) do |writer|
111
+ schema ||= Schema.from_active_record(model) if model
112
+ writer = if schema
113
+ Writer.new(io, schema, **options)
114
+ else
115
+ fix = "Herringbone.write(io, rows) { %s }"
116
+ InferringWriter.new(io, fix: fix, **options) { |sample| Schema.infer(sample, &overrides) }
117
+ end
118
+ begin
53
119
  if records.respond_to?(:find_each)
54
120
  records.find_each { |record| writer << record }
55
121
  else
56
122
  records.each { |record| writer << record }
57
123
  end
58
- writer.rows_written
124
+ rescue Exception # rubocop:disable Lint/RescueException -- also abort on Interrupt
125
+ writer.abort
126
+ raise
59
127
  end
128
+ writer.close
129
+ writer.rows_written
60
130
  end
61
131
 
62
132
  # Compression codecs this process can read and write, e.g. [:none, :snappy, :gzip, :lz4, :lz4_hadoop, :zstd].
63
- # :zstd and :brotli are listed when the zstd-ruby / brotli gems can be loaded.
133
+ # :zstd and :brotli are listed when the zstd-ruby / brotli gems are loaded.
134
+ #
135
+ # @return [Array<Symbol>] codec names accepted by the writer's +compression:+ option
64
136
  def codecs
65
137
  Compression::NAMES.values.select do |name|
66
138
  Compression.ensure_available!(name)
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.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-09-30 00:00:00.000000000 Z
10
+ date: 2026-10-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -33,6 +33,7 @@ executables:
33
33
  extensions: []
34
34
  extra_rdoc_files: []
35
35
  files:
36
+ - CHANGELOG.md
36
37
  - LICENSE
37
38
  - README.md
38
39
  - bin/herringbone
@@ -41,20 +42,24 @@ files:
41
42
  - lib/herringbone/bloom_filter.rb
42
43
  - lib/herringbone/byte_values.rb
43
44
  - lib/herringbone/codecs/lz4.rb
45
+ - lib/herringbone/codecs/lzo.rb
44
46
  - lib/herringbone/codecs/snappy.rb
45
47
  - lib/herringbone/compression.rb
46
48
  - lib/herringbone/encodings/delta.rb
47
49
  - lib/herringbone/encodings/plain.rb
48
50
  - lib/herringbone/encodings/rle.rb
49
51
  - lib/herringbone/format.rb
52
+ - lib/herringbone/inferring_writer.rb
50
53
  - lib/herringbone/inspector.rb
51
54
  - lib/herringbone/io_buffer_support.rb
52
55
  - lib/herringbone/reader.rb
53
56
  - lib/herringbone/reader/column_chunk_reader.rb
54
57
  - lib/herringbone/reader/column_cursor.rb
58
+ - lib/herringbone/reader/numo.rb
55
59
  - lib/herringbone/reader/page_stream.rb
56
60
  - lib/herringbone/reader/scan.rb
57
61
  - lib/herringbone/schema.rb
62
+ - lib/herringbone/simple_writer.rb
58
63
  - lib/herringbone/thrift.rb
59
64
  - lib/herringbone/types.rb
60
65
  - lib/herringbone/version.rb