herringbone 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +209 -0
- data/bin/herringbone +54 -0
- data/lib/herringbone/active_record.rb +131 -0
- data/lib/herringbone/byte_values.rb +144 -0
- data/lib/herringbone/codecs/lz4.rb +305 -0
- data/lib/herringbone/codecs/snappy.rb +375 -0
- data/lib/herringbone/compression.rb +75 -0
- data/lib/herringbone/encodings/delta.rb +153 -0
- data/lib/herringbone/encodings/plain.rb +87 -0
- data/lib/herringbone/encodings/rle.rb +203 -0
- data/lib/herringbone/format.rb +309 -0
- data/lib/herringbone/io_buffer_support.rb +25 -0
- data/lib/herringbone/reader.rb +469 -0
- data/lib/herringbone/schema.rb +551 -0
- data/lib/herringbone/thrift.rb +334 -0
- data/lib/herringbone/types.rb +454 -0
- data/lib/herringbone/version.rb +5 -0
- data/lib/herringbone/writer.rb +634 -0
- data/lib/herringbone.rb +45 -0
- metadata +104 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herringbone
|
|
4
|
+
# Minimal Thrift Compact Protocol implementation, just enough for Parquet metadata.
|
|
5
|
+
# Structs are described declaratively (see Herringbone::Thrift::Struct) so that
|
|
6
|
+
# both the reader and the writer are driven by the same field tables.
|
|
7
|
+
module Thrift
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Compact protocol wire types
|
|
11
|
+
T_STOP = 0
|
|
12
|
+
T_TRUE = 1
|
|
13
|
+
T_FALSE = 2
|
|
14
|
+
T_BYTE = 3
|
|
15
|
+
T_I16 = 4
|
|
16
|
+
T_I32 = 5
|
|
17
|
+
T_I64 = 6
|
|
18
|
+
T_DOUBLE = 7
|
|
19
|
+
T_BINARY = 8
|
|
20
|
+
T_LIST = 9
|
|
21
|
+
T_SET = 10
|
|
22
|
+
T_MAP = 11
|
|
23
|
+
T_STRUCT = 12
|
|
24
|
+
|
|
25
|
+
# Declared field types used in struct definitions
|
|
26
|
+
# :bool, :byte, :i16, :i32, :i64, :double, :binary, :string, [:list, elem], StructClass
|
|
27
|
+
WIRE_TYPES = {
|
|
28
|
+
bool: T_TRUE, byte: T_BYTE, i16: T_I16, i32: T_I32, i64: T_I64,
|
|
29
|
+
double: T_DOUBLE, binary: T_BINARY, string: T_BINARY
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def self.wire_type_for(type)
|
|
33
|
+
case type
|
|
34
|
+
when Symbol then WIRE_TYPES.fetch(type)
|
|
35
|
+
when Array then T_LIST
|
|
36
|
+
else T_STRUCT
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Whether a value of wire type +wire+ can be read as declared +type+
|
|
41
|
+
def self.compatible?(wire, type)
|
|
42
|
+
case type
|
|
43
|
+
when :bool then wire == T_TRUE || wire == T_FALSE
|
|
44
|
+
when :byte then wire == T_BYTE
|
|
45
|
+
when :i16, :i32, :i64 then wire == T_I16 || wire == T_I32 || wire == T_I64
|
|
46
|
+
when :double then wire == T_DOUBLE
|
|
47
|
+
when :binary, :string then wire == T_BINARY
|
|
48
|
+
when Array then wire == T_LIST || wire == T_SET
|
|
49
|
+
else wire == T_STRUCT
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Reader
|
|
54
|
+
attr_reader :pos
|
|
55
|
+
|
|
56
|
+
def initialize(buf, pos = 0)
|
|
57
|
+
@buf = buf
|
|
58
|
+
@pos = pos
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def read_byte
|
|
62
|
+
b = @buf.getbyte(@pos)
|
|
63
|
+
raise Error, "Unexpected end of Thrift data at #{@pos}" unless b
|
|
64
|
+
@pos += 1
|
|
65
|
+
b
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def read_varint
|
|
69
|
+
result = 0
|
|
70
|
+
shift = 0
|
|
71
|
+
while true
|
|
72
|
+
b = read_byte
|
|
73
|
+
result |= (b & 0x7F) << shift
|
|
74
|
+
return result if b < 0x80
|
|
75
|
+
shift += 7
|
|
76
|
+
raise Error, "Varint too long" if shift > 70
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def read_zigzag
|
|
81
|
+
n = read_varint
|
|
82
|
+
(n >> 1) ^ -(n & 1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def read_binary
|
|
86
|
+
len = read_varint
|
|
87
|
+
raise Error, "Binary length #{len} exceeds buffer" if @pos + len > @buf.bytesize
|
|
88
|
+
s = @buf.byteslice(@pos, len)
|
|
89
|
+
@pos += len
|
|
90
|
+
s
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def read_double
|
|
94
|
+
v = @buf.byteslice(@pos, 8).unpack1("E")
|
|
95
|
+
@pos += 8
|
|
96
|
+
v
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Reads a struct of the given class, returning an instance.
|
|
100
|
+
def read_struct(klass)
|
|
101
|
+
obj = klass.new
|
|
102
|
+
fields = klass.fields_by_id
|
|
103
|
+
last_id = 0
|
|
104
|
+
while true
|
|
105
|
+
header = read_byte
|
|
106
|
+
wire = header & 0x0F
|
|
107
|
+
break if wire == T_STOP
|
|
108
|
+
delta = header >> 4
|
|
109
|
+
fid = delta.zero? ? read_zigzag : last_id + delta
|
|
110
|
+
last_id = fid
|
|
111
|
+
field = fields[fid]
|
|
112
|
+
if field && Thrift.compatible?(wire, field.type)
|
|
113
|
+
obj.instance_variable_set(field.ivar, read_value(wire, field.type))
|
|
114
|
+
else
|
|
115
|
+
skip(wire)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
obj
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def read_value(wire, type)
|
|
122
|
+
case wire
|
|
123
|
+
when T_TRUE then true
|
|
124
|
+
when T_FALSE then false
|
|
125
|
+
when T_BYTE
|
|
126
|
+
b = read_byte
|
|
127
|
+
b >= 0x80 ? b - 0x100 : b
|
|
128
|
+
when T_I16, T_I32, T_I64 then read_zigzag
|
|
129
|
+
when T_DOUBLE then read_double
|
|
130
|
+
when T_BINARY
|
|
131
|
+
s = read_binary
|
|
132
|
+
type == :string ? s.force_encoding(Encoding::UTF_8) : s
|
|
133
|
+
when T_LIST, T_SET then read_list(type)
|
|
134
|
+
when T_STRUCT then read_struct(type)
|
|
135
|
+
else
|
|
136
|
+
raise Error, "Unsupported wire type #{wire}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def read_list(type)
|
|
141
|
+
header = read_byte
|
|
142
|
+
size = header >> 4
|
|
143
|
+
size = read_varint if size == 15
|
|
144
|
+
elem_wire = header & 0x0F
|
|
145
|
+
elem_type = type[1]
|
|
146
|
+
bool_elems = elem_wire == T_TRUE || elem_wire == T_FALSE
|
|
147
|
+
unless size.zero? || (bool_elems && elem_type == :bool) || Thrift.compatible?(elem_wire, elem_type)
|
|
148
|
+
size.times { bool_elems ? read_byte : skip(elem_wire) }
|
|
149
|
+
return nil
|
|
150
|
+
end
|
|
151
|
+
Array.new(size) do
|
|
152
|
+
if elem_wire == T_TRUE || elem_wire == T_FALSE
|
|
153
|
+
# Booleans inside lists are encoded as full bytes
|
|
154
|
+
read_byte == T_TRUE
|
|
155
|
+
else
|
|
156
|
+
read_value(elem_wire, elem_type)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def skip(wire)
|
|
162
|
+
case wire
|
|
163
|
+
when T_TRUE, T_FALSE then nil
|
|
164
|
+
when T_BYTE then read_byte
|
|
165
|
+
when T_I16, T_I32, T_I64 then read_varint
|
|
166
|
+
when T_DOUBLE then @pos += 8
|
|
167
|
+
when T_BINARY then read_binary
|
|
168
|
+
when T_LIST, T_SET
|
|
169
|
+
header = read_byte
|
|
170
|
+
size = header >> 4
|
|
171
|
+
size = read_varint if size == 15
|
|
172
|
+
elem = header & 0x0F
|
|
173
|
+
size.times { (elem == T_TRUE || elem == T_FALSE) ? read_byte : skip(elem) }
|
|
174
|
+
when T_MAP
|
|
175
|
+
size = read_varint
|
|
176
|
+
unless size.zero?
|
|
177
|
+
kv = read_byte
|
|
178
|
+
size.times do
|
|
179
|
+
[kv >> 4, kv & 0x0F].each { |w| (w == T_TRUE || w == T_FALSE) ? read_byte : skip(w) }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
when T_STRUCT
|
|
183
|
+
while true
|
|
184
|
+
header = read_byte
|
|
185
|
+
w = header & 0x0F
|
|
186
|
+
break if w == T_STOP
|
|
187
|
+
read_zigzag if (header >> 4).zero?
|
|
188
|
+
skip(w)
|
|
189
|
+
end
|
|
190
|
+
else
|
|
191
|
+
raise Error, "Cannot skip wire type #{wire}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
class Writer
|
|
197
|
+
attr_reader :buf
|
|
198
|
+
|
|
199
|
+
def initialize(buf = String.new(capacity: 1024, encoding: Encoding::BINARY))
|
|
200
|
+
@buf = buf
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def write_varint(n)
|
|
204
|
+
raise Error, "Negative varint" if n.negative?
|
|
205
|
+
while n >= 0x80
|
|
206
|
+
@buf << ((n & 0x7F) | 0x80)
|
|
207
|
+
n >>= 7
|
|
208
|
+
end
|
|
209
|
+
@buf << n
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def write_zigzag(n)
|
|
213
|
+
write_varint(n.negative? ? ((-n) << 1) - 1 : n << 1)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def write_binary(s)
|
|
217
|
+
write_varint(s.bytesize)
|
|
218
|
+
@buf << s.b
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def write_struct(obj)
|
|
222
|
+
last_id = 0
|
|
223
|
+
obj.class.fields.each do |field|
|
|
224
|
+
value = obj.instance_variable_get(field.ivar)
|
|
225
|
+
next if value.nil?
|
|
226
|
+
wire = if field.type == :bool
|
|
227
|
+
value ? T_TRUE : T_FALSE
|
|
228
|
+
else
|
|
229
|
+
Thrift.wire_type_for(field.type)
|
|
230
|
+
end
|
|
231
|
+
delta = field.id - last_id
|
|
232
|
+
if delta.positive? && delta <= 15
|
|
233
|
+
@buf << ((delta << 4) | wire)
|
|
234
|
+
else
|
|
235
|
+
@buf << wire
|
|
236
|
+
write_zigzag(field.id)
|
|
237
|
+
end
|
|
238
|
+
last_id = field.id
|
|
239
|
+
write_value(field.type, value) unless field.type == :bool
|
|
240
|
+
end
|
|
241
|
+
@buf << T_STOP
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def write_value(type, value)
|
|
245
|
+
case type
|
|
246
|
+
when :byte then @buf << (value & 0xFF)
|
|
247
|
+
when :i16, :i32, :i64 then write_zigzag(value)
|
|
248
|
+
when :double then @buf << [value].pack("E")
|
|
249
|
+
when :binary, :string then write_binary(value)
|
|
250
|
+
when Array then write_list(type[1], value)
|
|
251
|
+
else write_struct(value)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def write_list(elem_type, values)
|
|
256
|
+
elem_wire = Thrift.wire_type_for(elem_type)
|
|
257
|
+
if values.size < 15
|
|
258
|
+
@buf << ((values.size << 4) | elem_wire)
|
|
259
|
+
else
|
|
260
|
+
@buf << (0xF0 | elem_wire)
|
|
261
|
+
write_varint(values.size)
|
|
262
|
+
end
|
|
263
|
+
values.each do |v|
|
|
264
|
+
if elem_type == :bool
|
|
265
|
+
@buf << (v ? T_TRUE : T_FALSE)
|
|
266
|
+
else
|
|
267
|
+
write_value(elem_type, v)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
Field = ::Struct.new(:id, :name, :type, :ivar)
|
|
274
|
+
|
|
275
|
+
# Base class for Thrift structs. Subclasses declare fields with
|
|
276
|
+
# field 1, :name, :i32
|
|
277
|
+
class Struct
|
|
278
|
+
class << self
|
|
279
|
+
def fields
|
|
280
|
+
@fields ||= []
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def fields_by_id
|
|
284
|
+
@fields_by_id ||= fields.to_h { |f| [f.id, f] }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def field(id, name, type)
|
|
288
|
+
fields << Field.new(id, name, type, :"@#{name}")
|
|
289
|
+
fields.sort_by!(&:id)
|
|
290
|
+
@fields_by_id = nil
|
|
291
|
+
attr_accessor name
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def decode(buf, pos = 0)
|
|
295
|
+
reader = Reader.new(buf, pos)
|
|
296
|
+
[reader.read_struct(self), reader.pos]
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def initialize(**attrs)
|
|
301
|
+
attrs.each do |k, v|
|
|
302
|
+
raise ArgumentError, "Unknown field #{k} for #{self.class}" unless respond_to?(:"#{k}=")
|
|
303
|
+
public_send(:"#{k}=", v)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def encode
|
|
308
|
+
w = Writer.new
|
|
309
|
+
w.write_struct(self)
|
|
310
|
+
w.buf
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def to_h
|
|
314
|
+
self.class.fields.each_with_object({}) do |f, h|
|
|
315
|
+
v = instance_variable_get(f.ivar)
|
|
316
|
+
next if v.nil?
|
|
317
|
+
h[f.name] = case v
|
|
318
|
+
when Struct then v.to_h
|
|
319
|
+
when Array then v.map { |e| e.is_a?(Struct) ? e.to_h : e }
|
|
320
|
+
else v
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def ==(other)
|
|
326
|
+
other.class == self.class && other.to_h == to_h
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def inspect
|
|
330
|
+
"#<#{self.class.name.split("::").last} #{to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}>"
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|