herringbone 0.2.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.
@@ -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
@@ -219,6 +291,9 @@ module Herringbone
219
291
 
220
292
  class << self
221
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
222
297
  def xxh64(bytes)
223
298
  native = @native
224
299
  native = resolve_backend if native.nil?
@@ -227,6 +302,9 @@ module Herringbone
227
302
 
228
303
  # XXH64 of 8 bytes given as a little-endian 64-bit Integer (an INT64 or DOUBLE's PLAIN
229
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
230
308
  def xxh64_u64(lane)
231
309
  native = @native
232
310
  native = resolve_backend if native.nil?
@@ -235,6 +313,9 @@ module Herringbone
235
313
  end
236
314
 
237
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
238
319
  def xxh64_u32(word)
239
320
  native = @native
240
321
  native = resolve_backend if native.nil?
@@ -243,6 +324,9 @@ module Herringbone
243
324
  end
244
325
 
245
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
246
330
  def xxh64_u64_all(lanes)
247
331
  native = @native
248
332
  native = resolve_backend if native.nil?
@@ -255,6 +339,9 @@ module Herringbone
255
339
  end
256
340
 
257
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
258
345
  def xxh64_u32_all(words)
259
346
  native = @native
260
347
  native = resolve_backend if native.nil?
@@ -267,6 +354,9 @@ module Herringbone
267
354
  end
268
355
 
269
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
270
360
  def xxh64_all(strings)
271
361
  native = @native
272
362
  native = resolve_backend if native.nil?
@@ -274,6 +364,8 @@ module Herringbone
274
364
  end
275
365
 
276
366
  # :native when the xxhash gem is used, :ruby otherwise
367
+ #
368
+ # @return [Symbol] +:native+ or +:ruby+
277
369
  def backend
278
370
  native = @native
279
371
  native = resolve_backend if native.nil?
@@ -282,6 +374,11 @@ module Herringbone
282
374
 
283
375
  # :ruby forces pure Ruby, :native requires the xxhash gem (UnsupportedError if it cannot be
284
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
285
382
  def backend=(name)
286
383
  @native = case name
287
384
  when :ruby then false
@@ -293,16 +390,25 @@ module Herringbone
293
390
  end
294
391
 
295
392
  # Whether the native xxhash gem can be loaded (whatever the selected backend)
393
+ #
394
+ # @return [Boolean] true when the gem is loadable
296
395
  def native_available?
297
396
  !!native_library
298
397
  end
299
398
 
300
399
  private
301
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
302
404
  def resolve_backend
303
405
  @native = native_library || false
304
406
  end
305
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
306
412
  def native_library
307
413
  if @native_lib.nil?
308
414
  @native_lib = begin
data/lib/herringbone.rb CHANGED
@@ -6,11 +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
10
11
  # The file is not valid Parquet (bad metadata, corrupt pages...)
11
12
  class FormatError < Error; end
12
13
  # A value cannot be written to its column
13
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
14
17
  class UnsupportedError < Error; end
15
18
  end
16
19
 
@@ -44,6 +47,29 @@ module Herringbone
44
47
  # Other options go to Writer.
45
48
  #
46
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
47
73
  def write(io, records, schema: nil, **options)
48
74
  model = if records.respond_to?(:klass) then records.klass
49
75
  elsif records.respond_to?(:columns) && records.respond_to?(:find_each) then records
@@ -61,6 +87,8 @@ module Herringbone
61
87
 
62
88
  # Compression codecs this process can read and write, e.g. [:none, :snappy, :gzip, :lz4, :lz4_hadoop, :zstd].
63
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
64
92
  def codecs
65
93
  Compression::NAMES.values.select do |name|
66
94
  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.3.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-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -52,6 +52,7 @@ files:
52
52
  - lib/herringbone/reader.rb
53
53
  - lib/herringbone/reader/column_chunk_reader.rb
54
54
  - lib/herringbone/reader/column_cursor.rb
55
+ - lib/herringbone/reader/numo.rb
55
56
  - lib/herringbone/reader/page_stream.rb
56
57
  - lib/herringbone/reader/scan.rb
57
58
  - lib/herringbone/schema.rb