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.
- checksums.yaml +4 -4
- data/README.md +56 -4
- 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/snappy.rb +132 -3
- data/lib/herringbone/compression.rb +48 -0
- data/lib/herringbone/encodings/delta.rb +70 -13
- data/lib/herringbone/encodings/plain.rb +22 -0
- data/lib/herringbone/encodings/rle.rb +53 -1
- data/lib/herringbone/format.rb +150 -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 +98 -6
- data/lib/herringbone/reader/column_cursor.rb +46 -2
- data/lib/herringbone/reader/numo.rb +365 -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 +306 -15
- data/lib/herringbone/thrift.rb +155 -4
- 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 +308 -31
- data/lib/herringbone/xxhash.rb +108 -2
- data/lib/herringbone.rb +28 -0
- metadata +3 -2
|
@@ -17,10 +17,28 @@ module Herringbone
|
|
|
17
17
|
# OffsetIndex), leaving ranges of rows to read; and every row that is read is checked, so
|
|
18
18
|
# results are exact.
|
|
19
19
|
class Filter
|
|
20
|
+
# One where: entry, resolved against the schema
|
|
21
|
+
#
|
|
22
|
+
# @!attribute [rw] name
|
|
23
|
+
# @return [String] the key as given (dotted path of a leaf column)
|
|
24
|
+
# @!attribute [rw] column
|
|
25
|
+
# @return [Schema::Column] the leaf column the condition reads
|
|
26
|
+
# @!attribute [rw] field
|
|
27
|
+
# @return [Schema::Field] the top-level field holding that column
|
|
28
|
+
# @!attribute [rw] path
|
|
29
|
+
# @return [Array<String>] member names from the top-level field down to the column
|
|
30
|
+
# (empty for a top-level column)
|
|
31
|
+
# @!attribute [rw] test
|
|
32
|
+
# @return [Object] the condition, with Symbols turned into Strings
|
|
20
33
|
Condition = Struct.new(:name, :column, :field, :path, :test)
|
|
21
34
|
|
|
35
|
+
# @return [Array<Condition>] one per where: entry, in the order given
|
|
22
36
|
attr_reader :conditions
|
|
23
37
|
|
|
38
|
+
# @param schema [Schema] schema of the file being read
|
|
39
|
+
# @param where [Hash{String, Symbol => Object}] column => condition (see the class docs)
|
|
40
|
+
# @raise [ArgumentError] when +where+ is not a Hash, names an unknown column, or a column
|
|
41
|
+
# inside a list or map
|
|
24
42
|
def initialize(schema, where)
|
|
25
43
|
raise ArgumentError, "where: must be a Hash of column => condition" unless where.is_a?(Hash)
|
|
26
44
|
@conditions = where.map do |key, test|
|
|
@@ -35,6 +53,8 @@ module Herringbone
|
|
|
35
53
|
end
|
|
36
54
|
|
|
37
55
|
# Top-level fields the conditions need to read
|
|
56
|
+
#
|
|
57
|
+
# @return [Array<Schema::Field>] distinct fields, in condition order
|
|
38
58
|
def fields
|
|
39
59
|
@conditions.map(&:field).uniq
|
|
40
60
|
end
|
|
@@ -43,6 +63,10 @@ module Herringbone
|
|
|
43
63
|
|
|
44
64
|
# Whether any row of row group +rg_index+ may match, judging by chunk statistics and bloom
|
|
45
65
|
# filters
|
|
66
|
+
#
|
|
67
|
+
# @param reader [Reader] reader of the file (for its footer and bloom filters)
|
|
68
|
+
# @param rg_index [Integer] position of the row group in the footer
|
|
69
|
+
# @return [Boolean] false only when no row of the row group can match
|
|
46
70
|
def row_group_may_match?(reader, rg_index)
|
|
47
71
|
chunks = reader.row_groups[rg_index].columns
|
|
48
72
|
@conditions.all? do |c|
|
|
@@ -58,6 +82,10 @@ module Herringbone
|
|
|
58
82
|
# Sorted, non-overlapping [first_row, end_row) ranges of row group +rg_index+ that may hold
|
|
59
83
|
# matching rows, judging by the page index. Columns without a page index do not narrow
|
|
60
84
|
# the ranges.
|
|
85
|
+
#
|
|
86
|
+
# @param reader [Reader] reader of the file (for its page index)
|
|
87
|
+
# @param rg_index [Integer] position of the row group in the footer
|
|
88
|
+
# @return [Array<Array(Integer, Integer)>] half-open row ranges within the row group
|
|
61
89
|
def page_ranges(reader, rg_index)
|
|
62
90
|
n = reader.row_groups[rg_index].num_rows
|
|
63
91
|
ranges = [[0, n]]
|
|
@@ -75,7 +103,7 @@ module Herringbone
|
|
|
75
103
|
end
|
|
76
104
|
nulls = column_index.null_counts&.[](i)
|
|
77
105
|
next unless Filter.may_match?(c.test, min, max, nulls, column_index.null_pages[i])
|
|
78
|
-
stop = i + 1 < locs.size ? locs[i + 1].first_row_index : n
|
|
106
|
+
stop = (i + 1 < locs.size) ? locs[i + 1].first_row_index : n
|
|
79
107
|
candidates << [loc.first_row_index, stop]
|
|
80
108
|
end
|
|
81
109
|
ranges = Filter.intersect(ranges, Filter.merge(candidates))
|
|
@@ -88,6 +116,12 @@ module Herringbone
|
|
|
88
116
|
|
|
89
117
|
# Indexes (0...k) of the rows of an assembled batch that match. +data+ holds one Array per
|
|
90
118
|
# field in +fields+; +symbolize+ says how struct Hashes are keyed.
|
|
119
|
+
#
|
|
120
|
+
# @param data [Array<Array>] assembled values, one Array of +k+ entries per field
|
|
121
|
+
# @param fields [Array<Schema::Field>] the fields +data+ holds, in the same order
|
|
122
|
+
# @param k [Integer] number of rows in the batch
|
|
123
|
+
# @param symbolize [Boolean] whether struct Hashes are keyed by Symbol
|
|
124
|
+
# @return [Array<Integer>] indexes of matching rows, ascending
|
|
91
125
|
def matching_rows(data, fields, k, symbolize)
|
|
92
126
|
columns = @conditions.map do |c|
|
|
93
127
|
values = data.fetch(fields.index(c.field))
|
|
@@ -109,11 +143,22 @@ module Herringbone
|
|
|
109
143
|
|
|
110
144
|
# --- condition evaluation ---
|
|
111
145
|
|
|
146
|
+
# Whether a value satisfies a condition: nil matches nulls, an Array any of its elements,
|
|
147
|
+
# a Range by #cover? (never nil, incomparable values do not match), a String by bytes
|
|
148
|
+
# (ignoring the encoding), a callable by its result, anything else by ==.
|
|
149
|
+
#
|
|
150
|
+
# @param test [Object] the condition
|
|
151
|
+
# @param v [Object] the row's value
|
|
152
|
+
# @return [Boolean] true when the value matches
|
|
112
153
|
def self.matches?(test, v)
|
|
113
154
|
case test
|
|
114
155
|
when nil then v.nil?
|
|
115
156
|
when Array then test.any? { |t| matches?(t, v) }
|
|
116
|
-
when Range then !v.nil? &&
|
|
157
|
+
when Range then !v.nil? && begin
|
|
158
|
+
test.cover?(v)
|
|
159
|
+
rescue
|
|
160
|
+
false
|
|
161
|
+
end
|
|
117
162
|
when String then v.is_a?(String) && (v == test || (v.bytesize == test.bytesize && v.b == test.b))
|
|
118
163
|
else
|
|
119
164
|
if test.respond_to?(:call) then test.call(v)
|
|
@@ -124,6 +169,13 @@ module Herringbone
|
|
|
124
169
|
|
|
125
170
|
# Whether a set of values with the given bounds may contain a match. Unknown bounds or
|
|
126
171
|
# values that cannot be compared never rule anything out.
|
|
172
|
+
#
|
|
173
|
+
# @param test [Object] the condition
|
|
174
|
+
# @param min [Object, nil] lower bound of the values, nil when unknown
|
|
175
|
+
# @param max [Object, nil] upper bound of the values, nil when unknown
|
|
176
|
+
# @param nulls [Integer, nil] number of nulls, nil when unknown
|
|
177
|
+
# @param all_null [Boolean, nil] whether every value is null
|
|
178
|
+
# @return [Boolean] false only when no value can match
|
|
127
179
|
def self.may_match?(test, min, max, nulls, all_null)
|
|
128
180
|
case test
|
|
129
181
|
when nil then nulls.nil? || nulls.positive?
|
|
@@ -150,19 +202,29 @@ module Herringbone
|
|
|
150
202
|
end
|
|
151
203
|
end
|
|
152
204
|
|
|
153
|
-
|
|
205
|
+
# Booleans compare as integers, false before true (the Parquet sort order)
|
|
206
|
+
BOOLEAN_ORDER = {false => 0, true => 1}.freeze
|
|
154
207
|
|
|
208
|
+
# Compares two values in Parquet order: Strings byte-wise, false before true
|
|
209
|
+
#
|
|
210
|
+
# @param a [Object] left-hand value
|
|
211
|
+
# @param b [Object] right-hand value
|
|
212
|
+
# @return [Integer, nil] -1, 0 or 1, or nil when the values cannot be compared
|
|
155
213
|
def self.compare(a, b)
|
|
156
214
|
a = a.b if a.is_a?(String)
|
|
157
215
|
b = b.b if b.is_a?(String)
|
|
158
216
|
a = BOOLEAN_ORDER.fetch(a, a)
|
|
159
217
|
b = BOOLEAN_ORDER.fetch(b, b)
|
|
160
218
|
a <=> b
|
|
161
|
-
rescue
|
|
219
|
+
rescue
|
|
162
220
|
nil
|
|
163
221
|
end
|
|
164
222
|
|
|
165
223
|
# [min, max] of a chunk's statistics as Ruby values, or [nil, nil] when unusable
|
|
224
|
+
#
|
|
225
|
+
# @param column [Schema::Column] the chunk's column, for decoding the bounds
|
|
226
|
+
# @param stats [Format::Statistics, nil] the chunk's statistics
|
|
227
|
+
# @return [Array(Object, Object)] min and max, each nil when unknown
|
|
166
228
|
def self.stat_range(column, stats)
|
|
167
229
|
return [nil, nil] unless stats
|
|
168
230
|
if stats.min_value && stats.max_value
|
|
@@ -176,6 +238,9 @@ module Herringbone
|
|
|
176
238
|
|
|
177
239
|
# The deprecated min/max fields were written with signed comparisons, which only agree
|
|
178
240
|
# with the logical order for signed numbers and booleans
|
|
241
|
+
#
|
|
242
|
+
# @param column [Schema::Column] column whose statistics are being read
|
|
243
|
+
# @return [Boolean] true when the legacy min/max fields can be trusted
|
|
179
244
|
def self.legacy_order_ok?(column)
|
|
180
245
|
kind, _, signed = Types.logical_of(column.node)
|
|
181
246
|
case column.type
|
|
@@ -185,6 +250,12 @@ module Herringbone
|
|
|
185
250
|
end
|
|
186
251
|
end
|
|
187
252
|
|
|
253
|
+
# Decodes a min/max bound (PLAIN encoding of one value) into a Ruby value
|
|
254
|
+
#
|
|
255
|
+
# @param column [Schema::Column] the column the bound belongs to
|
|
256
|
+
# @param bytes [String, nil] the encoded bound
|
|
257
|
+
# @return [Object, nil] the value, or nil when it is absent or cannot be used (INT96,
|
|
258
|
+
# truncated FIXED_LEN_BYTE_ARRAY, too short, undecodable)
|
|
188
259
|
def self.decode_stat(column, bytes)
|
|
189
260
|
return nil if bytes.nil?
|
|
190
261
|
type = column.type
|
|
@@ -192,22 +263,31 @@ module Herringbone
|
|
|
192
263
|
when Format::Type::INT96 then return nil # no defined order
|
|
193
264
|
when Format::Type::BYTE_ARRAY, Format::Type::FIXED_LEN_BYTE_ARRAY
|
|
194
265
|
# Truncated bounds (shorter than the type length) still bound byte-wise
|
|
195
|
-
return type == Format::Type::FIXED_LEN_BYTE_ARRAY && bytes.bytesize != column.type_length ? nil : convert(column, bytes.dup)
|
|
266
|
+
return (type == Format::Type::FIXED_LEN_BYTE_ARRAY && bytes.bytesize != column.type_length) ? nil : convert(column, bytes.dup)
|
|
196
267
|
when Format::Type::BOOLEAN then bytes.getbyte(0) == 1
|
|
197
268
|
else
|
|
198
|
-
return nil if bytes.bytesize < {
|
|
269
|
+
return nil if bytes.bytesize < {Format::Type::INT32 => 4, Format::Type::FLOAT => 4}.fetch(type, 8)
|
|
199
270
|
Encodings::Plain.decode(bytes, 0, 1, type).first.first
|
|
200
271
|
end
|
|
201
272
|
convert(column, value)
|
|
202
|
-
rescue
|
|
273
|
+
rescue
|
|
203
274
|
nil
|
|
204
275
|
end
|
|
205
276
|
|
|
277
|
+
# Applies the column's converter, so bounds compare with the values rows hold
|
|
278
|
+
#
|
|
279
|
+
# @param column [Schema::Column] the column the value belongs to
|
|
280
|
+
# @param value [Object] physical value
|
|
281
|
+
# @return [Object] the Ruby value
|
|
206
282
|
def self.convert(column, value)
|
|
207
283
|
conv = column.converter
|
|
208
284
|
conv ? conv.call(value) : value
|
|
209
285
|
end
|
|
210
286
|
|
|
287
|
+
# Sorts ranges and merges overlapping or touching ones
|
|
288
|
+
#
|
|
289
|
+
# @param ranges [Array<Array(Integer, Integer)>] half-open row ranges
|
|
290
|
+
# @return [Array<Array(Integer, Integer)>] sorted, non-overlapping ranges
|
|
211
291
|
def self.merge(ranges)
|
|
212
292
|
ranges.sort.each_with_object([]) do |(s, e), out|
|
|
213
293
|
if out.any? && s <= out.last[1]
|
|
@@ -218,6 +298,11 @@ module Herringbone
|
|
|
218
298
|
end
|
|
219
299
|
end
|
|
220
300
|
|
|
301
|
+
# Intersection of two sorted, non-overlapping lists of half-open ranges
|
|
302
|
+
#
|
|
303
|
+
# @param a [Array<Array(Integer, Integer)>] first list of ranges
|
|
304
|
+
# @param b [Array<Array(Integer, Integer)>] second list of ranges
|
|
305
|
+
# @return [Array<Array(Integer, Integer)>] the rows in both, sorted
|
|
221
306
|
def self.intersect(a, b)
|
|
222
307
|
out = []
|
|
223
308
|
i = j = 0
|
|
@@ -225,13 +310,17 @@ module Herringbone
|
|
|
225
310
|
s = [a[i][0], b[j][0]].max
|
|
226
311
|
e = [a[i][1], b[j][1]].min
|
|
227
312
|
out << [s, e] if s < e
|
|
228
|
-
a[i][1] < b[j][1] ? i += 1 : j += 1
|
|
313
|
+
(a[i][1] < b[j][1]) ? i += 1 : j += 1
|
|
229
314
|
end
|
|
230
315
|
out
|
|
231
316
|
end
|
|
232
317
|
|
|
233
318
|
private
|
|
234
319
|
|
|
320
|
+
# Symbols (also inside Arrays) become Strings, since columns never hold Symbols
|
|
321
|
+
#
|
|
322
|
+
# @param test [Object] a condition as given in where:
|
|
323
|
+
# @return [Object] the condition to store in Condition#test
|
|
235
324
|
def normalize(test)
|
|
236
325
|
case test
|
|
237
326
|
when Symbol then test.to_s
|
|
@@ -241,6 +330,11 @@ module Herringbone
|
|
|
241
330
|
end
|
|
242
331
|
|
|
243
332
|
# Bloom filters answer equality lookups (a value or a list of values)
|
|
333
|
+
#
|
|
334
|
+
# @param reader [Reader] reader of the file (for its bloom filters)
|
|
335
|
+
# @param rg_index [Integer] position of the row group in the footer
|
|
336
|
+
# @param condition [Condition] the condition to check
|
|
337
|
+
# @return [Boolean] false only when the bloom filter rules out every value of the condition
|
|
244
338
|
def bloom_may_match?(reader, rg_index, condition)
|
|
245
339
|
values = Array(condition.test.is_a?(Array) ? condition.test : [condition.test])
|
|
246
340
|
return true if values.empty? || values.any? { |v| v.nil? || v.is_a?(Range) || v.respond_to?(:call) }
|
|
@@ -248,7 +342,7 @@ module Herringbone
|
|
|
248
342
|
filter = reader.bloom_filter(rg_index, condition.column.path)
|
|
249
343
|
return true unless filter
|
|
250
344
|
values.any? { |v| filter.might_contain?(v) }
|
|
251
|
-
rescue
|
|
345
|
+
rescue
|
|
252
346
|
true # values the column cannot store never rule a row group out here; rows are checked anyway
|
|
253
347
|
end
|
|
254
348
|
end
|