herringbone 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/README.md +88 -24
- data/bin/herringbone +40 -10
- data/lib/herringbone/codecs/lzo.rb +224 -0
- data/lib/herringbone/codecs/snappy.rb +13 -20
- data/lib/herringbone/compression.rb +26 -38
- data/lib/herringbone/encodings/plain.rb +2 -2
- data/lib/herringbone/inferring_writer.rb +161 -0
- data/lib/herringbone/reader/column_chunk_reader.rb +5 -8
- data/lib/herringbone/reader/numo.rb +18 -26
- data/lib/herringbone/reader.rb +3 -3
- data/lib/herringbone/schema.rb +7 -12
- data/lib/herringbone/simple_writer.rb +152 -0
- data/lib/herringbone/thrift.rb +5 -5
- data/lib/herringbone/version.rb +1 -1
- data/lib/herringbone/writer.rb +15 -13
- data/lib/herringbone/xxhash.rb +21 -36
- data/lib/herringbone.rb +52 -8
- metadata +6 -2
data/lib/herringbone/thrift.rb
CHANGED
|
@@ -401,12 +401,12 @@ module Herringbone
|
|
|
401
401
|
class << self
|
|
402
402
|
# @return [Array<Field>] declared fields, sorted by id
|
|
403
403
|
def fields
|
|
404
|
-
@fields
|
|
404
|
+
@fields || []
|
|
405
405
|
end
|
|
406
406
|
|
|
407
407
|
# @return [Hash{Integer => Field}] declared fields by id, for decoding
|
|
408
408
|
def fields_by_id
|
|
409
|
-
@fields_by_id
|
|
409
|
+
@fields_by_id || {}
|
|
410
410
|
end
|
|
411
411
|
|
|
412
412
|
# Declares a field and defines its accessor
|
|
@@ -417,9 +417,9 @@ module Herringbone
|
|
|
417
417
|
# or a Struct subclass
|
|
418
418
|
# @return [void]
|
|
419
419
|
def field(id, name, type)
|
|
420
|
-
|
|
421
|
-
fields.sort_by
|
|
422
|
-
@fields_by_id =
|
|
420
|
+
# Shareable, so that structs can be decoded in any Ractor
|
|
421
|
+
@fields = Ractor.make_shareable((fields + [Field.new(id, name, type, :"@#{name}")]).sort_by(&:id))
|
|
422
|
+
@fields_by_id = Ractor.make_shareable(@fields.to_h { |f| [f.id, f] })
|
|
423
423
|
attr_accessor name
|
|
424
424
|
end
|
|
425
425
|
|
data/lib/herringbone/version.rb
CHANGED
data/lib/herringbone/writer.rb
CHANGED
|
@@ -56,14 +56,14 @@ module Herringbone
|
|
|
56
56
|
}.freeze
|
|
57
57
|
|
|
58
58
|
# Encoding id => physical types it may be used for, per the Parquet encodings spec
|
|
59
|
-
VALID_ENCODINGS = {
|
|
59
|
+
VALID_ENCODINGS = Ractor.make_shareable({
|
|
60
60
|
E::PLAIN => T::NAMES.keys,
|
|
61
61
|
E::RLE => [T::BOOLEAN],
|
|
62
62
|
E::DELTA_BINARY_PACKED => [T::INT32, T::INT64],
|
|
63
63
|
E::DELTA_LENGTH_BYTE_ARRAY => [T::BYTE_ARRAY],
|
|
64
64
|
E::DELTA_BYTE_ARRAY => [T::BYTE_ARRAY, T::FIXED_LEN_BYTE_ARRAY],
|
|
65
65
|
E::BYTE_STREAM_SPLIT => [T::INT32, T::INT64, T::FLOAT, T::DOUBLE, T::FIXED_LEN_BYTE_ARRAY]
|
|
66
|
-
}
|
|
66
|
+
})
|
|
67
67
|
|
|
68
68
|
# Largest dictionary build_dictionary keeps; above it the chunk is written without a dictionary.
|
|
69
69
|
# Byte-array columns are dictionary-encoded by ByteValues, which applies its own
|
|
@@ -132,7 +132,7 @@ module Herringbone
|
|
|
132
132
|
# @param bloom_filters [Boolean, Array<String>, Hash{String => Boolean, Hash}, nil] see the class
|
|
133
133
|
# description
|
|
134
134
|
# @raise [ArgumentError] for an invalid IO, schema or option
|
|
135
|
-
# @raise [MissingCodecError] when the codec's optional gem
|
|
135
|
+
# @raise [MissingCodecError] when the codec's optional gem is not loaded
|
|
136
136
|
# @raise [UnsupportedError] when the codec is not supported
|
|
137
137
|
def initialize(io, schema, compression: :snappy, row_group_bytes: 16 * 1024 * 1024, row_group_rows: nil,
|
|
138
138
|
page_bytes: 1024 * 1024, page_rows: 20_000, data_page_version: 1, dictionary: true, encodings: {},
|
|
@@ -189,20 +189,20 @@ module Herringbone
|
|
|
189
189
|
if buf.nil?
|
|
190
190
|
shred(field, value, 0, 0)
|
|
191
191
|
elsif value.nil?
|
|
192
|
-
raise EncodeError
|
|
192
|
+
raise EncodeError.new("Field #{name} is required but got nil", column: name) unless field.optional
|
|
193
193
|
buf.defs << 0
|
|
194
194
|
else
|
|
195
195
|
begin
|
|
196
196
|
buf.values << encoder.call(value)
|
|
197
197
|
rescue ArgumentError, TypeError, NoMethodError, RangeError => e
|
|
198
|
-
raise EncodeError
|
|
198
|
+
raise EncodeError.new("Cannot write #{value.inspect} to #{name}: #{e.message}", column: name, value: value)
|
|
199
199
|
end
|
|
200
200
|
buf.defs << field.def_level
|
|
201
201
|
end
|
|
202
202
|
end
|
|
203
203
|
rescue EncodeError => e
|
|
204
204
|
rollback_row(marks)
|
|
205
|
-
raise EncodeError
|
|
205
|
+
raise EncodeError.new("Row #{@total_rows + @buffered_rows}: #{e.message}", row: @total_rows + @buffered_rows, column: e.column, value: e.value)
|
|
206
206
|
rescue
|
|
207
207
|
rollback_row(marks)
|
|
208
208
|
raise
|
|
@@ -314,10 +314,12 @@ module Herringbone
|
|
|
314
314
|
col.max_repetition_level.positive? ? String.new(encoding: Encoding::BINARY) : nil,
|
|
315
315
|
new_values_store(col))
|
|
316
316
|
end
|
|
317
|
+
# Column#encoder builds its Proc on every call
|
|
318
|
+
@encoders = @schema.columns.map(&:encoder)
|
|
317
319
|
# Top-level, non-repeated leaves are written directly, everything else is shredded
|
|
318
320
|
@plan = @schema.fields.map do |field|
|
|
319
321
|
flat = field.leaf? && field.column.max_repetition_level.zero?
|
|
320
|
-
[field, field.name, field.name.to_sym, flat ? @buffers[field.column.index] : nil, flat ? field.column.
|
|
322
|
+
[field, field.name, field.name.to_sym, flat ? @buffers[field.column.index] : nil, flat ? @encoders[field.column.index] : nil]
|
|
321
323
|
end
|
|
322
324
|
@nested_buffers = @plan.reject { |p| p[3] }.flat_map { |p| p[0].leaves.map { |c| @buffers[c.index] } }
|
|
323
325
|
@buffered_rows = 0
|
|
@@ -472,7 +474,7 @@ module Herringbone
|
|
|
472
474
|
# map key is nil, or a leaf value cannot be encoded
|
|
473
475
|
def shred(field, value, parent_def, rep)
|
|
474
476
|
if value.nil?
|
|
475
|
-
raise EncodeError
|
|
477
|
+
raise EncodeError.new("Field #{field.node.path.join(".")} is required but got nil", column: field.node.path.join(".")) unless field.optional
|
|
476
478
|
field.leaves.each do |col|
|
|
477
479
|
buf = @buffers[col.index]
|
|
478
480
|
buf.defs << parent_def
|
|
@@ -488,14 +490,14 @@ module Herringbone
|
|
|
488
490
|
buf.defs << d
|
|
489
491
|
buf.reps&.<< rep
|
|
490
492
|
begin
|
|
491
|
-
buf.values << field.column.
|
|
493
|
+
buf.values << @encoders[field.column.index].call(value)
|
|
492
494
|
rescue ArgumentError, TypeError, NoMethodError, RangeError => e
|
|
493
|
-
raise EncodeError
|
|
495
|
+
raise EncodeError.new("Cannot write #{value.inspect} to #{field.column.dotted_path}: #{e.message}", column: field.column.dotted_path, value: value)
|
|
494
496
|
end
|
|
495
497
|
when :struct
|
|
496
498
|
field.children.each { |ch| shred(ch, lookup(value, ch.name), d, rep) }
|
|
497
499
|
when :list
|
|
498
|
-
raise EncodeError
|
|
500
|
+
raise EncodeError.new("Expected an Array for #{field.node.path.join(".")}, got #{value.class}", column: field.node.path.join("."), value: value) unless value.respond_to?(:each_with_index)
|
|
499
501
|
if value.empty?
|
|
500
502
|
field.leaves.each do |col|
|
|
501
503
|
buf = @buffers[col.index]
|
|
@@ -508,7 +510,7 @@ module Herringbone
|
|
|
508
510
|
end
|
|
509
511
|
end
|
|
510
512
|
when :map
|
|
511
|
-
raise EncodeError
|
|
513
|
+
raise EncodeError.new("Expected a Hash for #{field.node.path.join(".")}, got #{value.class}", column: field.node.path.join("."), value: value) unless value.respond_to?(:each_pair) || value.is_a?(Array)
|
|
512
514
|
if value.empty?
|
|
513
515
|
field.leaves.each do |col|
|
|
514
516
|
buf = @buffers[col.index]
|
|
@@ -968,7 +970,7 @@ module Herringbone
|
|
|
968
970
|
STAT_TRUNCATE_BYTES = 64
|
|
969
971
|
# Sort key for types whose Ruby ordering already matches Parquet's; compared by identity to
|
|
970
972
|
# skip calling it
|
|
971
|
-
IDENTITY = ->(v) { v }
|
|
973
|
+
IDENTITY = Ractor.make_shareable(->(v) { v })
|
|
972
974
|
|
|
973
975
|
# Chunk statistics: null count, plus min/max (flagged exact unless truncated) when the column
|
|
974
976
|
# has a sort order and non-NaN values
|
data/lib/herringbone/xxhash.rb
CHANGED
|
@@ -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)
|
|
7
|
-
# faster; otherwise hashing is pure Ruby. The gem is only a speedup, so nothing
|
|
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.
|
|
@@ -286,8 +286,7 @@ module Herringbone
|
|
|
286
286
|
|
|
287
287
|
module_eval(Generator.source, __FILE__, __LINE__)
|
|
288
288
|
|
|
289
|
-
@native = nil # nil:
|
|
290
|
-
@native_lib = nil
|
|
289
|
+
@native = nil # nil: native when loaded, false: pure Ruby, else the native module
|
|
291
290
|
|
|
292
291
|
class << self
|
|
293
292
|
# XXH64 of a String's bytes, as an unsigned 64-bit Integer
|
|
@@ -296,7 +295,7 @@ module Herringbone
|
|
|
296
295
|
# @return [Integer] the hash, 0...2^64
|
|
297
296
|
def xxh64(bytes)
|
|
298
297
|
native = @native
|
|
299
|
-
native =
|
|
298
|
+
native = native_library if native.nil?
|
|
300
299
|
native ? native.xxh64(bytes, 0) : ruby_xxh64(bytes)
|
|
301
300
|
end
|
|
302
301
|
|
|
@@ -307,7 +306,7 @@ module Herringbone
|
|
|
307
306
|
# @return [Integer] the hash, 0...2^64
|
|
308
307
|
def xxh64_u64(lane)
|
|
309
308
|
native = @native
|
|
310
|
-
native =
|
|
309
|
+
native = native_library if native.nil?
|
|
311
310
|
return native.xxh64([lane].pack("Q<"), 0) if native
|
|
312
311
|
ruby_xxh64_lane((lane >> 32) & M32, lane & M32)
|
|
313
312
|
end
|
|
@@ -318,7 +317,7 @@ module Herringbone
|
|
|
318
317
|
# @return [Integer] the hash, 0...2^64
|
|
319
318
|
def xxh64_u32(word)
|
|
320
319
|
native = @native
|
|
321
|
-
native =
|
|
320
|
+
native = native_library if native.nil?
|
|
322
321
|
return native.xxh64([word].pack("L<"), 0) if native
|
|
323
322
|
ruby_xxh64_u32(word & M32)
|
|
324
323
|
end
|
|
@@ -329,7 +328,7 @@ module Herringbone
|
|
|
329
328
|
# @return [Array<Integer>] the hashes, in the same order
|
|
330
329
|
def xxh64_u64_all(lanes)
|
|
331
330
|
native = @native
|
|
332
|
-
native =
|
|
331
|
+
native = native_library if native.nil?
|
|
333
332
|
if native
|
|
334
333
|
packed = lanes.pack("Q<*")
|
|
335
334
|
Array.new(lanes.size) { |i| native.xxh64(packed.byteslice(i << 3, 8), 0) }
|
|
@@ -344,7 +343,7 @@ module Herringbone
|
|
|
344
343
|
# @return [Array<Integer>] the hashes, in the same order
|
|
345
344
|
def xxh64_u32_all(words)
|
|
346
345
|
native = @native
|
|
347
|
-
native =
|
|
346
|
+
native = native_library if native.nil?
|
|
348
347
|
if native
|
|
349
348
|
packed = words.pack("L<*")
|
|
350
349
|
Array.new(words.size) { |i| native.xxh64(packed.byteslice(i << 2, 4), 0) }
|
|
@@ -359,7 +358,7 @@ module Herringbone
|
|
|
359
358
|
# @return [Array<Integer>] the hashes, in the same order
|
|
360
359
|
def xxh64_all(strings)
|
|
361
360
|
native = @native
|
|
362
|
-
native =
|
|
361
|
+
native = native_library if native.nil?
|
|
363
362
|
native ? strings.map { |s| native.xxh64(s, 0) } : strings.map { |s| ruby_xxh64(s) }
|
|
364
363
|
end
|
|
365
364
|
|
|
@@ -368,57 +367,43 @@ module Herringbone
|
|
|
368
367
|
# @return [Symbol] +:native+ or +:ruby+
|
|
369
368
|
def backend
|
|
370
369
|
native = @native
|
|
371
|
-
native =
|
|
370
|
+
native = native_library if native.nil?
|
|
372
371
|
native ? :native : :ruby
|
|
373
372
|
end
|
|
374
373
|
|
|
375
|
-
# :ruby forces pure Ruby, :native
|
|
376
|
-
#
|
|
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
|
|
377
376
|
#
|
|
378
377
|
# @param name [Symbol, nil] +:ruby+, +:native+ or nil
|
|
379
378
|
# @return [void]
|
|
380
|
-
# @raise [UnsupportedError] for +:native+ when the gem
|
|
379
|
+
# @raise [UnsupportedError] for +:native+ when the gem is not loaded
|
|
381
380
|
# @raise [ArgumentError] for any other name
|
|
382
381
|
def backend=(name)
|
|
383
382
|
@native = case name
|
|
384
383
|
when :ruby then false
|
|
385
384
|
when :native
|
|
386
|
-
native_library || raise(UnsupportedError, "The \"#{NATIVE_GEM}\" gem
|
|
385
|
+
native_library || raise(UnsupportedError, "The \"#{NATIVE_GEM}\" gem is not loaded (require \"#{NATIVE_GEM}\")")
|
|
387
386
|
when nil then nil
|
|
388
387
|
else raise ArgumentError, "Unknown XXHash backend #{name.inspect} (expected :ruby, :native or nil)"
|
|
389
388
|
end
|
|
390
389
|
end
|
|
391
390
|
|
|
392
|
-
# Whether the native xxhash gem
|
|
391
|
+
# Whether the native xxhash gem is loaded (whatever the selected backend)
|
|
393
392
|
#
|
|
394
|
-
# @return [Boolean] true when the gem is
|
|
393
|
+
# @return [Boolean] true when the gem is loaded
|
|
395
394
|
def native_available?
|
|
396
395
|
!!native_library
|
|
397
396
|
end
|
|
398
397
|
|
|
399
398
|
private
|
|
400
399
|
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
# @return [Module, false] the native module, or false for pure Ruby
|
|
404
|
-
def resolve_backend
|
|
405
|
-
@native = native_library || false
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
# Requires the xxhash gem once and memoizes the result (a failed require is not retried)
|
|
400
|
+
# Not memoized, so that it works the same in every Ractor and once the gem is required later
|
|
409
401
|
#
|
|
410
402
|
# @return [Module, false] the gem's module to call +xxh64(data, seed)+ on, or false when it
|
|
411
|
-
#
|
|
403
|
+
# is not loaded
|
|
412
404
|
def native_library
|
|
413
|
-
if
|
|
414
|
-
|
|
415
|
-
require NATIVE_GEM
|
|
416
|
-
defined?(::XXhash::XXhashInternal) ? ::XXhash::XXhashInternal : ::XXhash
|
|
417
|
-
rescue LoadError
|
|
418
|
-
false
|
|
419
|
-
end
|
|
420
|
-
end
|
|
421
|
-
@native_lib
|
|
405
|
+
return ::XXhash::XXhashInternal if defined?(::XXhash::XXhashInternal)
|
|
406
|
+
defined?(::XXhash.xxh64) ? ::XXhash : false
|
|
422
407
|
end
|
|
423
408
|
end
|
|
424
409
|
end
|
data/lib/herringbone.rb
CHANGED
|
@@ -10,8 +10,33 @@ module Herringbone
|
|
|
10
10
|
class Error < StandardError; end
|
|
11
11
|
# The file is not valid Parquet (bad metadata, corrupt pages...)
|
|
12
12
|
class FormatError < Error; end
|
|
13
|
+
|
|
13
14
|
# A value cannot be written to its column
|
|
14
|
-
class EncodeError < Error
|
|
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
|
+
|
|
15
40
|
# The file (or the writer configuration) uses a Parquet feature Herringbone does not implement,
|
|
16
41
|
# such as encryption or a codec whose library is unavailable
|
|
17
42
|
class UnsupportedError < Error; end
|
|
@@ -20,6 +45,7 @@ end
|
|
|
20
45
|
require_relative "herringbone/io_buffer_support"
|
|
21
46
|
require_relative "herringbone/codecs/snappy"
|
|
22
47
|
require_relative "herringbone/codecs/lz4"
|
|
48
|
+
require_relative "herringbone/codecs/lzo"
|
|
23
49
|
require_relative "herringbone/thrift"
|
|
24
50
|
require_relative "herringbone/format"
|
|
25
51
|
require_relative "herringbone/encodings/rle"
|
|
@@ -32,6 +58,8 @@ require_relative "herringbone/active_record"
|
|
|
32
58
|
require_relative "herringbone/reader"
|
|
33
59
|
require_relative "herringbone/byte_values"
|
|
34
60
|
require_relative "herringbone/writer"
|
|
61
|
+
require_relative "herringbone/inferring_writer"
|
|
62
|
+
require_relative "herringbone/simple_writer"
|
|
35
63
|
require_relative "herringbone/xxhash"
|
|
36
64
|
require_relative "herringbone/bloom_filter"
|
|
37
65
|
require_relative "herringbone/inspector"
|
|
@@ -43,10 +71,13 @@ module Herringbone
|
|
|
43
71
|
# Writes +records+ to +io+ (any IO responding to #write; Herringbone never opens files by path)
|
|
44
72
|
# and returns the number of rows written. +records+ is an Enumerable of rows, or an ActiveRecord
|
|
45
73
|
# model or relation, which is read with find_each. Without +schema+, the schema comes from the
|
|
46
|
-
# model's columns (Schema.from_active_record) or is inferred from the first rows (Schema.infer)
|
|
47
|
-
#
|
|
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.
|
|
48
78
|
#
|
|
49
79
|
# 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 }
|
|
50
81
|
#
|
|
51
82
|
# @param io [IO, #write] destination; written sequentially, never closed
|
|
52
83
|
# @param records [Enumerable<Hash, Array, Object>, Class, #find_each] rows (Hashes, Arrays in schema
|
|
@@ -66,27 +97,40 @@ module Herringbone
|
|
|
66
97
|
# @option options [Hash{String => String}] :metadata ({}) key/value metadata for the footer
|
|
67
98
|
# @option options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
|
|
68
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
|
|
69
102
|
# @return [Integer] number of rows written
|
|
70
103
|
# @raise [ArgumentError] when the schema has to be inferred and +records+ is empty or holds Array
|
|
71
104
|
# rows, or an option is invalid
|
|
72
105
|
# @raise [EncodeError] when a row does not fit the schema
|
|
73
|
-
|
|
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)
|
|
74
108
|
model = if records.respond_to?(:klass) then records.klass
|
|
75
109
|
elsif records.respond_to?(:columns) && records.respond_to?(:find_each) then records
|
|
76
110
|
end
|
|
77
|
-
schema ||=
|
|
78
|
-
|
|
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
|
|
79
119
|
if records.respond_to?(:find_each)
|
|
80
120
|
records.find_each { |record| writer << record }
|
|
81
121
|
else
|
|
82
122
|
records.each { |record| writer << record }
|
|
83
123
|
end
|
|
84
|
-
|
|
124
|
+
rescue Exception # rubocop:disable Lint/RescueException -- also abort on Interrupt
|
|
125
|
+
writer.abort
|
|
126
|
+
raise
|
|
85
127
|
end
|
|
128
|
+
writer.close
|
|
129
|
+
writer.rows_written
|
|
86
130
|
end
|
|
87
131
|
|
|
88
132
|
# Compression codecs this process can read and write, e.g. [:none, :snappy, :gzip, :lz4, :lz4_hadoop, :zstd].
|
|
89
|
-
# :zstd and :brotli are listed when the zstd-ruby / brotli gems
|
|
133
|
+
# :zstd and :brotli are listed when the zstd-ruby / brotli gems are loaded.
|
|
90
134
|
#
|
|
91
135
|
# @return [Array<Symbol>] codec names accepted by the writer's +compression:+ option
|
|
92
136
|
def codecs
|
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.4.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-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,12 +42,14 @@ 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
|
|
@@ -56,6 +59,7 @@ files:
|
|
|
56
59
|
- lib/herringbone/reader/page_stream.rb
|
|
57
60
|
- lib/herringbone/reader/scan.rb
|
|
58
61
|
- lib/herringbone/schema.rb
|
|
62
|
+
- lib/herringbone/simple_writer.rb
|
|
59
63
|
- lib/herringbone/thrift.rb
|
|
60
64
|
- lib/herringbone/types.rb
|
|
61
65
|
- lib/herringbone/version.rb
|