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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/README.md +137 -21
- data/bin/herringbone +40 -10
- data/lib/herringbone/active_record.rb +44 -12
- data/lib/herringbone/bloom_filter.rb +112 -12
- data/lib/herringbone/byte_values.rb +29 -0
- data/lib/herringbone/codecs/lz4.rb +66 -3
- data/lib/herringbone/codecs/lzo.rb +224 -0
- data/lib/herringbone/codecs/snappy.rb +125 -3
- data/lib/herringbone/compression.rb +63 -27
- data/lib/herringbone/encodings/delta.rb +70 -13
- data/lib/herringbone/encodings/plain.rb +24 -2
- data/lib/herringbone/encodings/rle.rb +53 -1
- data/lib/herringbone/format.rb +150 -0
- data/lib/herringbone/inferring_writer.rb +161 -0
- data/lib/herringbone/inspector.rb +477 -81
- data/lib/herringbone/io_buffer_support.rb +3 -1
- data/lib/herringbone/reader/column_chunk_reader.rb +102 -13
- data/lib/herringbone/reader/column_cursor.rb +46 -2
- data/lib/herringbone/reader/numo.rb +357 -0
- data/lib/herringbone/reader/page_stream.rb +280 -4
- data/lib/herringbone/reader/scan.rb +103 -9
- data/lib/herringbone/reader.rb +308 -17
- data/lib/herringbone/schema.rb +311 -25
- data/lib/herringbone/simple_writer.rb +152 -0
- data/lib/herringbone/thrift.rb +160 -9
- data/lib/herringbone/types.rb +176 -41
- data/lib/herringbone/version.rb +2 -1
- data/lib/herringbone/visualizer.rb +51 -11
- data/lib/herringbone/writer.rb +322 -43
- data/lib/herringbone/xxhash.rb +122 -31
- data/lib/herringbone.rb +80 -8
- metadata +7 -2
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.
|
|
@@ -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
|
|
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
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
370
|
+
native = native_library if native.nil?
|
|
280
371
|
native ? :native : :ruby
|
|
281
372
|
end
|
|
282
373
|
|
|
283
|
-
# :ruby forces pure Ruby, :native
|
|
284
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
308
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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 ||=
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
+
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
|