herringbone 0.1.0 → 0.3.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/README.md +198 -74
- data/bin/herringbone +38 -26
- data/lib/herringbone/active_record.rb +48 -19
- data/lib/herringbone/bloom_filter.rb +370 -0
- data/lib/herringbone/byte_values.rb +29 -0
- data/lib/herringbone/codecs/lz4.rb +66 -3
- data/lib/herringbone/codecs/snappy.rb +132 -3
- data/lib/herringbone/compression.rb +121 -16
- data/lib/herringbone/encodings/delta.rb +76 -19
- data/lib/herringbone/encodings/plain.rb +29 -7
- data/lib/herringbone/encodings/rle.rb +55 -3
- data/lib/herringbone/format.rb +203 -0
- data/lib/herringbone/inspector.rb +1784 -0
- data/lib/herringbone/io_buffer_support.rb +3 -1
- data/lib/herringbone/reader/column_chunk_reader.rb +388 -0
- data/lib/herringbone/reader/column_cursor.rb +225 -0
- data/lib/herringbone/reader/numo.rb +365 -0
- data/lib/herringbone/reader/page_stream.rb +615 -0
- data/lib/herringbone/reader/scan.rb +350 -0
- data/lib/herringbone/reader.rb +585 -272
- data/lib/herringbone/schema.rb +326 -90
- data/lib/herringbone/thrift.rb +155 -4
- data/lib/herringbone/types.rb +177 -42
- data/lib/herringbone/version.rb +2 -1
- data/lib/herringbone/visualizer.rb +1136 -0
- data/lib/herringbone/writer.rb +550 -105
- data/lib/herringbone/xxhash.rb +425 -0
- data/lib/herringbone.rb +64 -9
- metadata +14 -33
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herringbone
|
|
4
|
+
# XXH64 (seed 0), the hash Parquet bloom filters use.
|
|
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).
|
|
9
|
+
#
|
|
10
|
+
# The pure-Ruby version keeps every 64-bit value as two 32-bit halves, and multiplies by the
|
|
11
|
+
# XXH64 primes split into 16-bit pieces, so that no intermediate result leaves the Fixnum range.
|
|
12
|
+
# Masked 64-bit Integer arithmetic is shorter, but allocates a Bignum for almost every operation
|
|
13
|
+
# (about 25 per hash), and with a large live heap (a writer holding a row group) those
|
|
14
|
+
# allocations trigger so many garbage collections that hashing becomes up to 10x slower. The
|
|
15
|
+
# arithmetic is written once with the small code generators below, whose output is inlined into
|
|
16
|
+
# the hashing methods: no method calls or allocations in the hot paths.
|
|
17
|
+
module XXHash
|
|
18
|
+
# 64-bit mask
|
|
19
|
+
M = 0xFFFF_FFFF_FFFF_FFFF
|
|
20
|
+
# XXH64 PRIME64_1
|
|
21
|
+
P1 = 11_400_714_785_074_694_791
|
|
22
|
+
# XXH64 PRIME64_2
|
|
23
|
+
P2 = 14_029_467_366_897_019_727
|
|
24
|
+
# XXH64 PRIME64_3
|
|
25
|
+
P3 = 1_609_587_929_392_839_161
|
|
26
|
+
# XXH64 PRIME64_4
|
|
27
|
+
P4 = 9_650_029_242_287_828_579
|
|
28
|
+
# XXH64 PRIME64_5
|
|
29
|
+
P5 = 2_870_177_450_012_600_261
|
|
30
|
+
# 32-bit mask
|
|
31
|
+
M32 = 0xFFFF_FFFF
|
|
32
|
+
# unpack format splitting the input into little-endian 32-bit words
|
|
33
|
+
WORDS = "V*" # frozen, unlike a literal in the generated code
|
|
34
|
+
|
|
35
|
+
# Gem providing the native implementation
|
|
36
|
+
NATIVE_GEM = "xxhash"
|
|
37
|
+
|
|
38
|
+
# Code generators for the pure-Ruby hash: each returns Ruby source operating on a 64-bit
|
|
39
|
+
# value held in two local variables (+hi+ and +lo+, 32 bits each), using t0/t1 as scratch
|
|
40
|
+
module Generator
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# Shifts are written as multiplications and divisions by powers of two, which YARV has
|
|
44
|
+
# specialized instructions for (<< and >> on Integers are method calls).
|
|
45
|
+
#
|
|
46
|
+
# (hi:lo) = (hi:lo) * c mod 2^64. Products are at most 48 bits wide: the low halves are
|
|
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)
|
|
54
|
+
def mul(hi, lo, c, hi_zero: false)
|
|
55
|
+
ch = c >> 32
|
|
56
|
+
cl = c & M32
|
|
57
|
+
cross = "#{lo} * #{ch & 0xFFFF} + (#{lo} * #{ch >> 16} & 0xFFFF) * 65536"
|
|
58
|
+
cross += " + #{hi} * #{cl & 0xFFFF} + (#{hi} * #{cl >> 16} & 0xFFFF) * 65536" unless hi_zero
|
|
59
|
+
<<~RUBY
|
|
60
|
+
t1 = #{lo} * #{cl >> 16}
|
|
61
|
+
t0 = #{lo} * #{cl & 0xFFFF} + (t1 & 0xFFFF) * 65536
|
|
62
|
+
#{hi} = (t0 / 4294967296 + t1 / 65536 + #{cross}) & 0xFFFFFFFF
|
|
63
|
+
#{lo} = t0 & 0xFFFFFFFF
|
|
64
|
+
RUBY
|
|
65
|
+
end
|
|
66
|
+
|
|
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)
|
|
73
|
+
def rotl(hi, lo, r)
|
|
74
|
+
mask = (1 << (32 - r)) - 1
|
|
75
|
+
<<~RUBY
|
|
76
|
+
t0 = #{hi}
|
|
77
|
+
#{hi} = (#{hi} & #{mask}) * #{1 << r} | #{lo} / #{1 << (32 - r)}
|
|
78
|
+
#{lo} = (#{lo} & #{mask}) * #{1 << r} | t0 / #{1 << (32 - r)}
|
|
79
|
+
RUBY
|
|
80
|
+
end
|
|
81
|
+
|
|
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
|
|
89
|
+
def add(hi, lo, a_hi, a_lo)
|
|
90
|
+
<<~RUBY
|
|
91
|
+
#{lo} += #{a_lo}
|
|
92
|
+
#{hi} = (#{hi} + #{a_hi} + #{lo} / 4294967296) & 0xFFFFFFFF
|
|
93
|
+
#{lo} &= 0xFFFFFFFF
|
|
94
|
+
RUBY
|
|
95
|
+
end
|
|
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
|
|
103
|
+
def add_const(hi, lo, c)
|
|
104
|
+
add(hi, lo, c >> 32, c & M32)
|
|
105
|
+
end
|
|
106
|
+
|
|
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
|
|
113
|
+
def set(hi, lo, c)
|
|
114
|
+
"#{hi} = #{c >> 32}\n#{lo} = #{c & M32}\n"
|
|
115
|
+
end
|
|
116
|
+
|
|
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
|
|
122
|
+
def round0(hi, lo)
|
|
123
|
+
mul(hi, lo, P2) + rotl(hi, lo, 31) + mul(hi, lo, P1)
|
|
124
|
+
end
|
|
125
|
+
|
|
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
|
|
133
|
+
def round(hi, lo, xh, xl)
|
|
134
|
+
mul(xh, xl, P2) + add(hi, lo, xh, xl) + rotl(hi, lo, 31) + mul(hi, lo, P1)
|
|
135
|
+
end
|
|
136
|
+
|
|
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)
|
|
144
|
+
def merge_round(hi, lo, vh, vl)
|
|
145
|
+
"xh = #{vh}\nxl = #{vl}\n" + round0("xh", "xl") +
|
|
146
|
+
"#{hi} ^= xh\n#{lo} ^= xl\n" + mul(hi, lo, P1) + add_const(hi, lo, P4)
|
|
147
|
+
end
|
|
148
|
+
|
|
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
|
|
154
|
+
def lane8(hi, lo)
|
|
155
|
+
round0("xh", "xl") + "#{hi} ^= xh\n#{lo} ^= xl\n" + rotl(hi, lo, 27) + mul(hi, lo, P1) + add_const(hi, lo, P4)
|
|
156
|
+
end
|
|
157
|
+
|
|
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)
|
|
163
|
+
def lane4(hi, lo)
|
|
164
|
+
"xh = 0\n" + mul("xh", "xl", P1, hi_zero: true) + "#{hi} ^= xh\n#{lo} ^= xl\n" +
|
|
165
|
+
rotl(hi, lo, 23) + mul(hi, lo, P2) + add_const(hi, lo, P3)
|
|
166
|
+
end
|
|
167
|
+
|
|
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
|
|
173
|
+
def lane1(hi, lo)
|
|
174
|
+
<<~RUBY + rotl(hi, lo, 11) + mul(hi, lo, P1)
|
|
175
|
+
t0 = xl * #{P5 & M32}
|
|
176
|
+
#{hi} ^= (t0 / 4294967296 + xl * #{P5 >> 32}) & 0xFFFFFFFF
|
|
177
|
+
#{lo} ^= t0 & 0xFFFFFFFF
|
|
178
|
+
RUBY
|
|
179
|
+
end
|
|
180
|
+
|
|
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
|
|
186
|
+
def avalanche(hi, lo)
|
|
187
|
+
"#{lo} ^= #{hi} / 2\n" + mul(hi, lo, P2) +
|
|
188
|
+
"#{lo} ^= (#{hi} & 0x1FFFFFFF) * 8 | #{lo} / 536870912\n#{hi} ^= #{hi} / 536870912\n" +
|
|
189
|
+
mul(hi, lo, P3) + "#{lo} ^= #{hi}\n(#{hi} << 32) | #{lo}\n"
|
|
190
|
+
end
|
|
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
|
|
196
|
+
def source
|
|
197
|
+
g = self
|
|
198
|
+
<<~RUBY
|
|
199
|
+
# XXH64 of 8 bytes given as two little-endian 32-bit halves
|
|
200
|
+
def self.ruby_xxh64_lane(xh, xl)
|
|
201
|
+
#{g.set("h", "l", P5 + 8)}
|
|
202
|
+
#{g.lane8("h", "l")}
|
|
203
|
+
#{g.avalanche("h", "l")}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# XXH64 of 4 bytes given as a little-endian 32-bit word
|
|
207
|
+
def self.ruby_xxh64_u32(xl)
|
|
208
|
+
#{g.set("h", "l", P5 + 4)}
|
|
209
|
+
#{g.lane4("h", "l")}
|
|
210
|
+
#{g.avalanche("h", "l")}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def self.ruby_xxh64(bytes)
|
|
214
|
+
len = bytes.bytesize
|
|
215
|
+
words = bytes.unpack(WORDS)
|
|
216
|
+
i = 0
|
|
217
|
+
if len >= 32
|
|
218
|
+
#{g.set("ah", "al", (P1 + P2) & M)}
|
|
219
|
+
#{g.set("bh", "bl", P2)}
|
|
220
|
+
ch = 0
|
|
221
|
+
cl = 0
|
|
222
|
+
#{g.set("dh", "dl", (-P1) & M)}
|
|
223
|
+
limit = (len >> 5) << 3
|
|
224
|
+
while i < limit
|
|
225
|
+
xl = words[i]
|
|
226
|
+
xh = words[i + 1]
|
|
227
|
+
#{g.round("ah", "al", "xh", "xl")}
|
|
228
|
+
xl = words[i + 2]
|
|
229
|
+
xh = words[i + 3]
|
|
230
|
+
#{g.round("bh", "bl", "xh", "xl")}
|
|
231
|
+
xl = words[i + 4]
|
|
232
|
+
xh = words[i + 5]
|
|
233
|
+
#{g.round("ch", "cl", "xh", "xl")}
|
|
234
|
+
xl = words[i + 6]
|
|
235
|
+
xh = words[i + 7]
|
|
236
|
+
#{g.round("dh", "dl", "xh", "xl")}
|
|
237
|
+
i += 8
|
|
238
|
+
end
|
|
239
|
+
h = ah
|
|
240
|
+
l = al
|
|
241
|
+
#{g.rotl("h", "l", 1)}
|
|
242
|
+
yh = bh
|
|
243
|
+
yl = bl
|
|
244
|
+
#{g.rotl("yh", "yl", 7)}
|
|
245
|
+
#{g.add("h", "l", "yh", "yl")}
|
|
246
|
+
yh = ch
|
|
247
|
+
yl = cl
|
|
248
|
+
#{g.rotl("yh", "yl", 12)}
|
|
249
|
+
#{g.add("h", "l", "yh", "yl")}
|
|
250
|
+
yh = dh
|
|
251
|
+
yl = dl
|
|
252
|
+
#{g.rotl("yh", "yl", 18)}
|
|
253
|
+
#{g.add("h", "l", "yh", "yl")}
|
|
254
|
+
#{g.merge_round("h", "l", "ah", "al")}
|
|
255
|
+
#{g.merge_round("h", "l", "bh", "bl")}
|
|
256
|
+
#{g.merge_round("h", "l", "ch", "cl")}
|
|
257
|
+
#{g.merge_round("h", "l", "dh", "dl")}
|
|
258
|
+
#{g.add("h", "l", 0, "len")}
|
|
259
|
+
else
|
|
260
|
+
#{g.set("h", "l", P5)}
|
|
261
|
+
#{g.add("h", "l", 0, "len")}
|
|
262
|
+
end
|
|
263
|
+
nwords = words.size
|
|
264
|
+
while i + 2 <= nwords
|
|
265
|
+
xl = words[i]
|
|
266
|
+
xh = words[i + 1]
|
|
267
|
+
#{g.lane8("h", "l")}
|
|
268
|
+
i += 2
|
|
269
|
+
end
|
|
270
|
+
if i < nwords
|
|
271
|
+
xl = words[i]
|
|
272
|
+
#{g.lane4("h", "l")}
|
|
273
|
+
i += 1
|
|
274
|
+
end
|
|
275
|
+
pos = i << 2
|
|
276
|
+
while pos < len
|
|
277
|
+
xl = bytes.getbyte(pos)
|
|
278
|
+
#{g.lane1("h", "l")}
|
|
279
|
+
pos += 1
|
|
280
|
+
end
|
|
281
|
+
#{g.avalanche("h", "l")}
|
|
282
|
+
end
|
|
283
|
+
RUBY
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
module_eval(Generator.source, __FILE__, __LINE__)
|
|
288
|
+
|
|
289
|
+
@native = nil # nil: not resolved yet, false: pure Ruby, else the native module
|
|
290
|
+
@native_lib = nil
|
|
291
|
+
|
|
292
|
+
class << self
|
|
293
|
+
# XXH64 of a String's bytes, as an unsigned 64-bit Integer
|
|
294
|
+
#
|
|
295
|
+
# @param bytes [String] data to hash (its encoding is ignored)
|
|
296
|
+
# @return [Integer] the hash, 0...2^64
|
|
297
|
+
def xxh64(bytes)
|
|
298
|
+
native = @native
|
|
299
|
+
native = resolve_backend if native.nil?
|
|
300
|
+
native ? native.xxh64(bytes, 0) : ruby_xxh64(bytes)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# XXH64 of 8 bytes given as a little-endian 64-bit Integer (an INT64 or DOUBLE's PLAIN
|
|
304
|
+
# encoding), signed or unsigned: only its low 64 bits are used
|
|
305
|
+
#
|
|
306
|
+
# @param lane [Integer] value whose low 64 bits are hashed
|
|
307
|
+
# @return [Integer] the hash, 0...2^64
|
|
308
|
+
def xxh64_u64(lane)
|
|
309
|
+
native = @native
|
|
310
|
+
native = resolve_backend if native.nil?
|
|
311
|
+
return native.xxh64([lane].pack("Q<"), 0) if native
|
|
312
|
+
ruby_xxh64_lane((lane >> 32) & M32, lane & M32)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# XXH64 of 4 bytes given as a little-endian 32-bit Integer (INT32, FLOAT), signed or unsigned
|
|
316
|
+
#
|
|
317
|
+
# @param word [Integer] value whose low 32 bits are hashed
|
|
318
|
+
# @return [Integer] the hash, 0...2^64
|
|
319
|
+
def xxh64_u32(word)
|
|
320
|
+
native = @native
|
|
321
|
+
native = resolve_backend if native.nil?
|
|
322
|
+
return native.xxh64([word].pack("L<"), 0) if native
|
|
323
|
+
ruby_xxh64_u32(word & M32)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Hashes of many 64-bit Integers (low 64 bits of each)
|
|
327
|
+
#
|
|
328
|
+
# @param lanes [Array<Integer>] values to hash, signed or unsigned
|
|
329
|
+
# @return [Array<Integer>] the hashes, in the same order
|
|
330
|
+
def xxh64_u64_all(lanes)
|
|
331
|
+
native = @native
|
|
332
|
+
native = resolve_backend if native.nil?
|
|
333
|
+
if native
|
|
334
|
+
packed = lanes.pack("Q<*")
|
|
335
|
+
Array.new(lanes.size) { |i| native.xxh64(packed.byteslice(i << 3, 8), 0) }
|
|
336
|
+
else
|
|
337
|
+
lanes.map { |v| ruby_xxh64_lane(v / 4_294_967_296 & M32, v & M32) }
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Hashes of many 32-bit Integers (low 32 bits of each)
|
|
342
|
+
#
|
|
343
|
+
# @param words [Array<Integer>] values to hash, signed or unsigned
|
|
344
|
+
# @return [Array<Integer>] the hashes, in the same order
|
|
345
|
+
def xxh64_u32_all(words)
|
|
346
|
+
native = @native
|
|
347
|
+
native = resolve_backend if native.nil?
|
|
348
|
+
if native
|
|
349
|
+
packed = words.pack("L<*")
|
|
350
|
+
Array.new(words.size) { |i| native.xxh64(packed.byteslice(i << 2, 4), 0) }
|
|
351
|
+
else
|
|
352
|
+
words.map { |v| ruby_xxh64_u32(v & M32) }
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Hashes of many Strings
|
|
357
|
+
#
|
|
358
|
+
# @param strings [Array<String>] values whose bytes are hashed
|
|
359
|
+
# @return [Array<Integer>] the hashes, in the same order
|
|
360
|
+
def xxh64_all(strings)
|
|
361
|
+
native = @native
|
|
362
|
+
native = resolve_backend if native.nil?
|
|
363
|
+
native ? strings.map { |s| native.xxh64(s, 0) } : strings.map { |s| ruby_xxh64(s) }
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# :native when the xxhash gem is used, :ruby otherwise
|
|
367
|
+
#
|
|
368
|
+
# @return [Symbol] +:native+ or +:ruby+
|
|
369
|
+
def backend
|
|
370
|
+
native = @native
|
|
371
|
+
native = resolve_backend if native.nil?
|
|
372
|
+
native ? :native : :ruby
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# :ruby forces pure Ruby, :native requires the xxhash gem (UnsupportedError if it cannot be
|
|
376
|
+
# loaded), nil goes back to the default: native when available
|
|
377
|
+
#
|
|
378
|
+
# @param name [Symbol, nil] +:ruby+, +:native+ or nil
|
|
379
|
+
# @return [void]
|
|
380
|
+
# @raise [UnsupportedError] for +:native+ when the gem cannot be loaded
|
|
381
|
+
# @raise [ArgumentError] for any other name
|
|
382
|
+
def backend=(name)
|
|
383
|
+
@native = case name
|
|
384
|
+
when :ruby then false
|
|
385
|
+
when :native
|
|
386
|
+
native_library || raise(UnsupportedError, "The \"#{NATIVE_GEM}\" gem could not be loaded")
|
|
387
|
+
when nil then nil
|
|
388
|
+
else raise ArgumentError, "Unknown XXHash backend #{name.inspect} (expected :ruby, :native or nil)"
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Whether the native xxhash gem can be loaded (whatever the selected backend)
|
|
393
|
+
#
|
|
394
|
+
# @return [Boolean] true when the gem is loadable
|
|
395
|
+
def native_available?
|
|
396
|
+
!!native_library
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
private
|
|
400
|
+
|
|
401
|
+
# Picks the default backend on first use: native when the gem loads, pure Ruby otherwise
|
|
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)
|
|
409
|
+
#
|
|
410
|
+
# @return [Module, false] the gem's module to call +xxh64(data, seed)+ on, or false when it
|
|
411
|
+
# cannot be loaded
|
|
412
|
+
def native_library
|
|
413
|
+
if @native_lib.nil?
|
|
414
|
+
@native_lib = begin
|
|
415
|
+
require NATIVE_GEM
|
|
416
|
+
defined?(::XXhash::XXhashInternal) ? ::XXhash::XXhashInternal : ::XXhash
|
|
417
|
+
rescue LoadError
|
|
418
|
+
false
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
@native_lib
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
data/lib/herringbone.rb
CHANGED
|
@@ -6,10 +6,14 @@ 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
|
|
11
|
+
# The file is not valid Parquet (bad metadata, corrupt pages...)
|
|
10
12
|
class FormatError < Error; end
|
|
11
|
-
|
|
13
|
+
# A value cannot be written to its column
|
|
12
14
|
class EncodeError < Error; end
|
|
15
|
+
# The file (or the writer configuration) uses a Parquet feature Herringbone does not implement,
|
|
16
|
+
# such as encryption or a codec whose library is unavailable
|
|
13
17
|
class UnsupportedError < Error; end
|
|
14
18
|
end
|
|
15
19
|
|
|
@@ -28,18 +32,69 @@ require_relative "herringbone/active_record"
|
|
|
28
32
|
require_relative "herringbone/reader"
|
|
29
33
|
require_relative "herringbone/byte_values"
|
|
30
34
|
require_relative "herringbone/writer"
|
|
35
|
+
require_relative "herringbone/xxhash"
|
|
36
|
+
require_relative "herringbone/bloom_filter"
|
|
37
|
+
require_relative "herringbone/inspector"
|
|
38
|
+
require_relative "herringbone/visualizer"
|
|
31
39
|
|
|
32
40
|
module Herringbone
|
|
33
|
-
Delta = Encodings::Delta
|
|
34
|
-
|
|
35
41
|
module_function
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
# Writes +records+ to +io+ (any IO responding to #write; Herringbone never opens files by path)
|
|
44
|
+
# and returns the number of rows written. +records+ is an Enumerable of rows, or an ActiveRecord
|
|
45
|
+
# 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
|
+
# Other options go to Writer.
|
|
48
|
+
#
|
|
49
|
+
# File.open("orders.parquet", "wb") { |f| Herringbone.write(f, Order.where(created_at: 1.year.ago..)) }
|
|
50
|
+
#
|
|
51
|
+
# @param io [IO, #write] destination; written sequentially, never closed
|
|
52
|
+
# @param records [Enumerable<Hash, Array, Object>, Class, #find_each] rows (Hashes, Arrays in schema
|
|
53
|
+
# order, or objects responding to #attributes or #to_h), or an ActiveRecord model or relation
|
|
54
|
+
# @param schema [Schema, nil] schema to write with; derived from +records+ when nil
|
|
55
|
+
# @param options [Hash{Symbol => Object}] passed to Writer.new
|
|
56
|
+
# @option options [Symbol] :compression (:snappy) codec, see Herringbone.codecs
|
|
57
|
+
# @option options [Integer] :row_group_bytes (16MB) approximate buffered size that triggers a row group
|
|
58
|
+
# @option options [Integer, nil] :row_group_rows (nil) also flush a row group after this many rows
|
|
59
|
+
# @option options [Integer] :page_bytes (1MB) approximate uncompressed data page size
|
|
60
|
+
# @option options [Integer] :page_rows (20_000) maximum rows per data page
|
|
61
|
+
# @option options [Integer] :data_page_version (1) 1 or 2
|
|
62
|
+
# @option options [Boolean, Array<String>] :dictionary (true) dictionary-encode all eligible columns,
|
|
63
|
+
# none, or only the listed dotted column paths
|
|
64
|
+
# @option options [Hash{String => Symbol}] :encodings ({}) dotted column path => value encoding
|
|
65
|
+
# for non-dictionary pages
|
|
66
|
+
# @option options [Hash{String => String}] :metadata ({}) key/value metadata for the footer
|
|
67
|
+
# @option options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
|
|
68
|
+
# columns to write split block bloom filters for, see Writer
|
|
69
|
+
# @return [Integer] number of rows written
|
|
70
|
+
# @raise [ArgumentError] when the schema has to be inferred and +records+ is empty or holds Array
|
|
71
|
+
# rows, or an option is invalid
|
|
72
|
+
# @raise [EncodeError] when a row does not fit the schema
|
|
73
|
+
def write(io, records, schema: nil, **options)
|
|
74
|
+
model = if records.respond_to?(:klass) then records.klass
|
|
75
|
+
elsif records.respond_to?(:columns) && records.respond_to?(:find_each) then records
|
|
76
|
+
end
|
|
77
|
+
schema ||= model ? Schema.from_active_record(model) : Schema.infer(records)
|
|
78
|
+
Writer.open(io, schema, **options) do |writer|
|
|
79
|
+
if records.respond_to?(:find_each)
|
|
80
|
+
records.find_each { |record| writer << record }
|
|
81
|
+
else
|
|
82
|
+
records.each { |record| writer << record }
|
|
83
|
+
end
|
|
84
|
+
writer.rows_written
|
|
85
|
+
end
|
|
86
|
+
end
|
|
39
87
|
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
88
|
+
# 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 can be loaded.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<Symbol>] codec names accepted by the writer's +compression:+ option
|
|
92
|
+
def codecs
|
|
93
|
+
Compression::NAMES.values.select do |name|
|
|
94
|
+
Compression.ensure_available!(name)
|
|
95
|
+
true
|
|
96
|
+
rescue UnsupportedError
|
|
97
|
+
false
|
|
98
|
+
end
|
|
44
99
|
end
|
|
45
100
|
end
|
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.3.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-01 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bigdecimal
|
|
@@ -23,37 +23,9 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: zstd-ruby
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0'
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - ">="
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0'
|
|
54
|
-
description: Reads and writes Apache Parquet files without native extensions or Thrift.
|
|
55
|
-
Snappy and LZ4 are implemented in Ruby; ZSTD and Brotli are used when their gems
|
|
56
|
-
are present.
|
|
26
|
+
description: Reads and writes Apache Parquet files in Ruby, without Thrift or native
|
|
27
|
+
extensions of its own. Snappy and LZ4 are implemented in Ruby and GZIP uses zlib;
|
|
28
|
+
ZSTD and Brotli are available when the zstd-ruby / brotli gems are installed.
|
|
57
29
|
email:
|
|
58
30
|
- me@julik.nl
|
|
59
31
|
executables:
|
|
@@ -66,6 +38,7 @@ files:
|
|
|
66
38
|
- bin/herringbone
|
|
67
39
|
- lib/herringbone.rb
|
|
68
40
|
- lib/herringbone/active_record.rb
|
|
41
|
+
- lib/herringbone/bloom_filter.rb
|
|
69
42
|
- lib/herringbone/byte_values.rb
|
|
70
43
|
- lib/herringbone/codecs/lz4.rb
|
|
71
44
|
- lib/herringbone/codecs/snappy.rb
|
|
@@ -74,13 +47,21 @@ files:
|
|
|
74
47
|
- lib/herringbone/encodings/plain.rb
|
|
75
48
|
- lib/herringbone/encodings/rle.rb
|
|
76
49
|
- lib/herringbone/format.rb
|
|
50
|
+
- lib/herringbone/inspector.rb
|
|
77
51
|
- lib/herringbone/io_buffer_support.rb
|
|
78
52
|
- lib/herringbone/reader.rb
|
|
53
|
+
- lib/herringbone/reader/column_chunk_reader.rb
|
|
54
|
+
- lib/herringbone/reader/column_cursor.rb
|
|
55
|
+
- lib/herringbone/reader/numo.rb
|
|
56
|
+
- lib/herringbone/reader/page_stream.rb
|
|
57
|
+
- lib/herringbone/reader/scan.rb
|
|
79
58
|
- lib/herringbone/schema.rb
|
|
80
59
|
- lib/herringbone/thrift.rb
|
|
81
60
|
- lib/herringbone/types.rb
|
|
82
61
|
- lib/herringbone/version.rb
|
|
62
|
+
- lib/herringbone/visualizer.rb
|
|
83
63
|
- lib/herringbone/writer.rb
|
|
64
|
+
- lib/herringbone/xxhash.rb
|
|
84
65
|
licenses:
|
|
85
66
|
- MIT
|
|
86
67
|
metadata: {}
|