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
data/lib/herringbone/schema.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Herringbone
|
|
|
8
8
|
# * the logical tree of Schema::Field, which interprets LIST/MAP annotations and is
|
|
9
9
|
# what rows are assembled from (and shredded into) when reading and writing.
|
|
10
10
|
class Schema
|
|
11
|
+
# Thrift FieldRepetitionType value => the Symbol used on Node#repetition
|
|
11
12
|
REPETITIONS = {
|
|
12
13
|
Format::Repetition::REQUIRED => :required,
|
|
13
14
|
Format::Repetition::OPTIONAL => :optional,
|
|
@@ -16,11 +17,25 @@ module Herringbone
|
|
|
16
17
|
|
|
17
18
|
# A node of the physical schema tree.
|
|
18
19
|
class Node
|
|
20
|
+
# Fields of the SchemaElement this node maps to (+type+ and +type_length+ are nil for groups);
|
|
21
|
+
# +children+ is nil for leaves and +parent+ is nil for the root
|
|
19
22
|
attr_accessor :name, :repetition, :type, :type_length, :converted_type, :logical_type,
|
|
20
23
|
:scale, :precision, :field_id, :children, :parent
|
|
21
24
|
# Allowed values of a string/enum column (writer-side validation only, not stored in the file)
|
|
22
25
|
attr_accessor :enum_values
|
|
23
26
|
|
|
27
|
+
# @param name [String, Symbol] field name, stored as a String
|
|
28
|
+
# @param repetition [Symbol] +:required+, +:optional+ or +:repeated+
|
|
29
|
+
# @param type [Integer, nil] physical type (Format::Type), nil for groups
|
|
30
|
+
# @param type_length [Integer, nil] byte width of FIXED_LEN_BYTE_ARRAY columns
|
|
31
|
+
# @param converted_type [Integer, nil] legacy annotation (Format::ConvertedType)
|
|
32
|
+
# @param logical_type [Format::LogicalType, nil] logical type annotation
|
|
33
|
+
# @param scale [Integer, nil] decimal scale
|
|
34
|
+
# @param precision [Integer, nil] decimal precision
|
|
35
|
+
# @param field_id [Integer, nil] optional field id, as used by Iceberg
|
|
36
|
+
# @param children [Array<Node>, nil] child nodes of a group (their +parent+ is set to this node);
|
|
37
|
+
# nil for a leaf
|
|
38
|
+
# @param enum_values [Array<String>, Hash{String => Object}, nil] see #enum_values
|
|
24
39
|
def initialize(name:, repetition: :optional, type: nil, type_length: nil, converted_type: nil,
|
|
25
40
|
logical_type: nil, scale: nil, precision: nil, field_id: nil, children: nil, enum_values: nil)
|
|
26
41
|
@name = name.to_s
|
|
@@ -37,28 +52,44 @@ module Herringbone
|
|
|
37
52
|
@children&.each { |c| c.parent = self }
|
|
38
53
|
end
|
|
39
54
|
|
|
55
|
+
# @return [Boolean] true for a group (a node with children, possibly none)
|
|
40
56
|
def group? = !@children.nil?
|
|
57
|
+
# @return [Boolean] true for a primitive column node
|
|
41
58
|
def leaf? = @children.nil?
|
|
59
|
+
# @return [Boolean] true when the repetition is +:repeated+
|
|
42
60
|
def repeated? = @repetition == :repeated
|
|
61
|
+
# @return [Boolean] true when the repetition is +:optional+
|
|
43
62
|
def optional? = @repetition == :optional
|
|
44
63
|
|
|
64
|
+
# @return [Symbol, nil] the LogicalType union member that is set (+:string+, +:list+,
|
|
65
|
+
# +:decimal+...), or nil without a logical type
|
|
45
66
|
def logical_kind
|
|
46
67
|
@logical_type&.kind&.first
|
|
47
68
|
end
|
|
48
69
|
|
|
70
|
+
# @return [Boolean] whether the node carries a LIST logical or converted type
|
|
49
71
|
def list_annotated?
|
|
50
72
|
logical_kind == :list || @converted_type == Format::ConvertedType::LIST
|
|
51
73
|
end
|
|
52
74
|
|
|
75
|
+
# @return [Boolean] whether the node carries a MAP logical type, or a MAP / MAP_KEY_VALUE
|
|
76
|
+
# converted type
|
|
53
77
|
def map_annotated?
|
|
54
78
|
logical_kind == :map || @converted_type == Format::ConvertedType::MAP ||
|
|
55
79
|
@converted_type == Format::ConvertedType::MAP_KEY_VALUE
|
|
56
80
|
end
|
|
57
81
|
|
|
82
|
+
# @return [Array<String>] names from the top-level field down to this node, without the
|
|
83
|
+
# root's name (the +path_in_schema+ of a column)
|
|
58
84
|
def path
|
|
59
85
|
parent&.parent ? parent.path + [name] : [name]
|
|
60
86
|
end
|
|
61
87
|
|
|
88
|
+
# Builds a node without children from a footer SchemaElement; Schema.from_elements attaches them.
|
|
89
|
+
#
|
|
90
|
+
# @param el [Format::SchemaElement] element read from the footer
|
|
91
|
+
# @return [Node] node with +children+ nil; a missing repetition is taken as required
|
|
92
|
+
# @raise [KeyError] when the repetition type is not a known value
|
|
62
93
|
def self.from_element(el)
|
|
63
94
|
new(
|
|
64
95
|
name: el.name,
|
|
@@ -73,6 +104,8 @@ module Herringbone
|
|
|
73
104
|
)
|
|
74
105
|
end
|
|
75
106
|
|
|
107
|
+
# @param root [Boolean] true for the schema root, which is written without a repetition type
|
|
108
|
+
# @return [Format::SchemaElement] element for the footer's flattened schema list
|
|
76
109
|
def to_element(root: false)
|
|
77
110
|
Format::SchemaElement.new(
|
|
78
111
|
name: name,
|
|
@@ -91,8 +124,14 @@ module Herringbone
|
|
|
91
124
|
|
|
92
125
|
# A leaf column (one column chunk per row group)
|
|
93
126
|
class Column
|
|
127
|
+
# Position among the leaf columns (the column chunk order in a row group), the leaf Node,
|
|
128
|
+
# its path (Node#path), and the maximum definition / repetition levels of its values
|
|
94
129
|
attr_reader :index, :node, :path, :max_definition_level, :max_repetition_level
|
|
95
130
|
|
|
131
|
+
# @param index [Integer] position among the schema's leaf columns
|
|
132
|
+
# @param node [Node] leaf node of the physical tree
|
|
133
|
+
# @param max_def [Integer] maximum definition level (number of non-required nodes on the path)
|
|
134
|
+
# @param max_rep [Integer] maximum repetition level (number of repeated nodes on the path)
|
|
96
135
|
def initialize(index, node, max_def, max_rep)
|
|
97
136
|
@index = index
|
|
98
137
|
@node = node
|
|
@@ -101,14 +140,25 @@ module Herringbone
|
|
|
101
140
|
@max_repetition_level = max_rep
|
|
102
141
|
end
|
|
103
142
|
|
|
143
|
+
# @return [Integer] physical type (Format::Type)
|
|
104
144
|
def type = @node.type
|
|
145
|
+
# @return [Integer, nil] byte width for FIXED_LEN_BYTE_ARRAY columns
|
|
105
146
|
def type_length = @node.type_length
|
|
147
|
+
# @return [String] path joined with dots, as used for column names in options
|
|
106
148
|
def dotted_path = @path.join(".")
|
|
107
149
|
|
|
150
|
+
# Memoized Types.reader_for of the node.
|
|
151
|
+
#
|
|
152
|
+
# @return [Proc, Method, nil] converts a physical value into its Ruby value, or nil when the physical
|
|
153
|
+
# value is used as-is
|
|
108
154
|
def converter
|
|
109
155
|
@converter ||= Types.reader_for(@node)
|
|
110
156
|
end
|
|
111
157
|
|
|
158
|
+
# Memoized Types.writer_for of the node.
|
|
159
|
+
#
|
|
160
|
+
# @return [Proc, Method] converts a Ruby value into the physical value to store, raising
|
|
161
|
+
# ArgumentError / TypeError / RangeError for values that do not fit
|
|
112
162
|
def encoder
|
|
113
163
|
@encoder ||= Types.writer_for(@node)
|
|
114
164
|
end
|
|
@@ -123,9 +173,23 @@ module Herringbone
|
|
|
123
173
|
# item_def: definition level at which the repeated node has at least one entry
|
|
124
174
|
# element: element Field (lists); key/value Fields (maps, value may be nil)
|
|
125
175
|
class Field
|
|
176
|
+
# As described for the class; +children+ are the Fields of a :struct, +column+ is the Column of a :leaf,
|
|
177
|
+
# +leaves+ are all Columns under this field, and +node+ is the physical Node it was built from
|
|
126
178
|
attr_reader :kind, :name, :optional, :def_level, :rep_level, :item_def,
|
|
127
179
|
:children, :element, :key, :value, :column, :leaves, :node
|
|
128
180
|
|
|
181
|
+
# @param kind [Symbol] +:leaf+, +:struct+, +:list+ or +:map+
|
|
182
|
+
# @param name [String] field name
|
|
183
|
+
# @param optional [Boolean] whether this field itself may be null
|
|
184
|
+
# @param def_level [Integer] definition level at which this field counts as present
|
|
185
|
+
# @param node [Node] physical node the field was built from
|
|
186
|
+
# @param rep_level [Integer, nil] repetition level of the repeated node (lists and maps)
|
|
187
|
+
# @param item_def [Integer, nil] definition level at which a list or map has an entry
|
|
188
|
+
# @param children [Array<Field>, nil] fields of a struct
|
|
189
|
+
# @param element [Field, nil] element of a list
|
|
190
|
+
# @param key [Field, nil] key of a map
|
|
191
|
+
# @param value [Field, nil] value of a map
|
|
192
|
+
# @param column [Column, nil] column of a leaf
|
|
129
193
|
def initialize(kind:, name:, optional:, def_level:, node:, rep_level: nil, item_def: nil,
|
|
130
194
|
children: nil, element: nil, key: nil, value: nil, column: nil)
|
|
131
195
|
@kind = kind
|
|
@@ -148,16 +212,27 @@ module Herringbone
|
|
|
148
212
|
end
|
|
149
213
|
end
|
|
150
214
|
|
|
215
|
+
# @return [Column] first leaf column under this field
|
|
151
216
|
def first_leaf = @leaves.first
|
|
217
|
+
# @return [Boolean] true for a primitive (+:leaf+) field
|
|
152
218
|
def leaf? = @kind == :leaf
|
|
153
219
|
|
|
220
|
+
# Memoized lookup of a struct's children; only valid for +:struct+ fields.
|
|
221
|
+
#
|
|
222
|
+
# @return [Hash{String => Field}] child fields by name
|
|
154
223
|
def children_by_name
|
|
155
224
|
@children_by_name ||= @children.to_h { |c| [c.name, c] }
|
|
156
225
|
end
|
|
157
226
|
end
|
|
158
227
|
|
|
228
|
+
# The root Node, the leaf Columns in file order, and the top-level Fields of the logical tree
|
|
159
229
|
attr_reader :root, :columns, :fields
|
|
160
230
|
|
|
231
|
+
# Rebuilds the tree from the depth-first list of SchemaElements stored in the footer.
|
|
232
|
+
#
|
|
233
|
+
# @param elements [Array<Format::SchemaElement>] footer schema, root first
|
|
234
|
+
# @return [Schema]
|
|
235
|
+
# @raise [FormatError] when the list is empty or ends before all declared children are read
|
|
161
236
|
def self.from_elements(elements)
|
|
162
237
|
raise FormatError, "Empty schema" if elements.empty?
|
|
163
238
|
pos = 0
|
|
@@ -177,6 +252,10 @@ module Herringbone
|
|
|
177
252
|
end
|
|
178
253
|
|
|
179
254
|
# Builds a schema with the DSL, see Schema::Builder
|
|
255
|
+
#
|
|
256
|
+
# @yield block evaluated with +instance_eval+ on a Builder, declaring the top-level fields
|
|
257
|
+
# @return [Schema]
|
|
258
|
+
# @raise [ArgumentError] when no field is declared, or a declaration is invalid
|
|
180
259
|
def self.define(&block)
|
|
181
260
|
builder = Builder.new
|
|
182
261
|
builder.instance_eval(&block) if block
|
|
@@ -184,6 +263,7 @@ module Herringbone
|
|
|
184
263
|
new(Node.new(name: "schema", repetition: :required, children: builder.nodes))
|
|
185
264
|
end
|
|
186
265
|
|
|
266
|
+
# Number of leading rows Schema.infer looks at
|
|
187
267
|
INFER_SAMPLE = 1000
|
|
188
268
|
|
|
189
269
|
# Infers a schema from the first 1000 rows (Hashes, or objects responding to #attributes or
|
|
@@ -193,6 +273,13 @@ module Herringbone
|
|
|
193
273
|
# Hash -> struct, Array -> list. Columns that are nil in every sampled row become strings.
|
|
194
274
|
# Fields declared in the block (Builder DSL) replace the inferred ones of the same name:
|
|
195
275
|
# Schema.infer(rows) { json :payload }
|
|
276
|
+
#
|
|
277
|
+
# @param rows [Enumerable<Hash, Object>] rows to sample; only the first INFER_SAMPLE are read
|
|
278
|
+
# @yield optional block evaluated with +instance_eval+ on a Builder, declaring fields that
|
|
279
|
+
# replace inferred ones (or are added after them)
|
|
280
|
+
# @return [Schema]
|
|
281
|
+
# @raise [ArgumentError] when there are no rows, a row is not Hash-like, or a column mixes
|
|
282
|
+
# values that map to no single Parquet type
|
|
196
283
|
def self.infer(rows, &block)
|
|
197
284
|
sample_rows = rows.first(INFER_SAMPLE).map { |r| Inference.row_hash(r) }
|
|
198
285
|
raise ArgumentError, "Cannot infer a schema from zero rows" if sample_rows.empty?
|
|
@@ -206,16 +293,29 @@ module Herringbone
|
|
|
206
293
|
new(Node.new(name: "schema", repetition: :required, children: nodes + declared.values))
|
|
207
294
|
end
|
|
208
295
|
|
|
296
|
+
# Type inference behind Schema.infer
|
|
209
297
|
module Inference
|
|
210
298
|
module_function
|
|
211
299
|
|
|
300
|
+
# @param row [Hash, #attributes, #to_h] sampled row
|
|
301
|
+
# @return [Hash] the row as a Hash keyed by field name (String or Symbol keys)
|
|
302
|
+
# @raise [ArgumentError] when the row is positional (an Array), or neither a Hash nor converts
|
|
303
|
+
# to one
|
|
212
304
|
def row_hash(row)
|
|
213
305
|
return row if row.is_a?(Hash)
|
|
306
|
+
raise ArgumentError, "Cannot infer a schema from Array rows (values in schema order); pass a schema" if row.is_a?(Array)
|
|
214
307
|
return row.attributes if row.respond_to?(:attributes)
|
|
215
308
|
return row.to_h if row.respond_to?(:to_h)
|
|
216
309
|
raise ArgumentError, "Cannot infer a schema from a #{row.class}"
|
|
217
310
|
end
|
|
218
311
|
|
|
312
|
+
# Hashes become groups, Arrays become 3-level LIST groups, other values a primitive column;
|
|
313
|
+
# nil values are ignored and an all-nil column becomes a string.
|
|
314
|
+
#
|
|
315
|
+
# @param name [String] field name
|
|
316
|
+
# @param values [Array<Object>] the field's values across the sampled rows
|
|
317
|
+
# @return [Node] optional node for the field
|
|
318
|
+
# @raise [ArgumentError] when the values have no common Parquet type
|
|
219
319
|
def node_for(name, values)
|
|
220
320
|
present = values.compact
|
|
221
321
|
return Node.new(name: name, **Types.physical_attributes(:string)) if present.empty?
|
|
@@ -233,6 +333,11 @@ module Herringbone
|
|
|
233
333
|
end
|
|
234
334
|
end
|
|
235
335
|
|
|
336
|
+
# @param name [String] field name, for the error message
|
|
337
|
+
# @param values [Array<Object>] non-nil values of the field
|
|
338
|
+
# @return [Array(Symbol), Array(Symbol, Hash{Symbol => Object})] DSL type, plus its options
|
|
339
|
+
# for types that take some (decimal, timestamp)
|
|
340
|
+
# @raise [ArgumentError] when the values have no common Parquet type
|
|
236
341
|
def scalar_type(name, values)
|
|
237
342
|
all = ->(*classes) { values.all? { |v| classes.any? { |c| v.is_a?(c) } } }
|
|
238
343
|
if all.call(Integer)
|
|
@@ -241,15 +346,15 @@ module Herringbone
|
|
|
241
346
|
[:boolean]
|
|
242
347
|
elsif all.call(Integer, BigDecimal)
|
|
243
348
|
scale = values.map { |v| v.is_a?(BigDecimal) ? v.to_s("F").split(".")[1].to_s.sub(/0+\z/, "").size : 0 }.max
|
|
244
|
-
[:decimal, {
|
|
349
|
+
[:decimal, {precision: 38, scale: scale}]
|
|
245
350
|
elsif all.call(Numeric)
|
|
246
351
|
[:double]
|
|
247
352
|
elsif all.call(String)
|
|
248
|
-
values.all? { |v| v.encoding != Encoding::BINARY && v.valid_encoding? } ? [:string] : [:binary]
|
|
353
|
+
(values.all? { |v| v.encoding != Encoding::BINARY && v.valid_encoding? }) ? [:string] : [:binary]
|
|
249
354
|
elsif all.call(String, Symbol)
|
|
250
355
|
[:string]
|
|
251
356
|
elsif all.call(Time, DateTime)
|
|
252
|
-
[:timestamp, {
|
|
357
|
+
[:timestamp, {unit: :micros}]
|
|
253
358
|
elsif all.call(Date)
|
|
254
359
|
[:date]
|
|
255
360
|
else
|
|
@@ -260,6 +365,7 @@ module Herringbone
|
|
|
260
365
|
end
|
|
261
366
|
end
|
|
262
367
|
|
|
368
|
+
# @param root [Node] root group of the physical tree, with +children+ set
|
|
263
369
|
def initialize(root)
|
|
264
370
|
@root = root
|
|
265
371
|
@columns = []
|
|
@@ -268,6 +374,8 @@ module Herringbone
|
|
|
268
374
|
@fields = root.children.map { |child| build_field(child, 0, 0) }
|
|
269
375
|
end
|
|
270
376
|
|
|
377
|
+
# @return [Array<Format::SchemaElement>] the tree flattened depth-first, root first, as stored
|
|
378
|
+
# in the footer
|
|
271
379
|
def to_elements
|
|
272
380
|
out = []
|
|
273
381
|
walk = lambda do |node, is_root|
|
|
@@ -278,15 +386,21 @@ module Herringbone
|
|
|
278
386
|
out
|
|
279
387
|
end
|
|
280
388
|
|
|
389
|
+
# @param name [String, Symbol] top-level field name
|
|
390
|
+
# @return [Field, nil] the top-level field, or nil when there is none by that name
|
|
281
391
|
def field(name)
|
|
282
392
|
@fields.find { |f| f.name == name.to_s }
|
|
283
393
|
end
|
|
284
394
|
|
|
395
|
+
# @param path [String, Array<String>] dotted path or path components of a leaf column
|
|
396
|
+
# @return [Column, nil] the leaf column, or nil when there is none at that path
|
|
285
397
|
def column(path)
|
|
286
398
|
path = path.split(".") if path.is_a?(String)
|
|
287
399
|
@columns.find { |c| c.path == path }
|
|
288
400
|
end
|
|
289
401
|
|
|
402
|
+
# @return [String] one line per node, indented by depth: repetition, physical type, name and
|
|
403
|
+
# annotation
|
|
290
404
|
def inspect
|
|
291
405
|
lines = []
|
|
292
406
|
walk = lambda do |node, depth|
|
|
@@ -301,7 +415,7 @@ module Herringbone
|
|
|
301
415
|
elsif node.logical_type then "UNKNOWN LOGICAL TYPE"
|
|
302
416
|
else Format::ConvertedType::NAMES[node.converted_type]
|
|
303
417
|
end
|
|
304
|
-
lines << "#{" " * depth}#{node.repetition} #{desc} #{node.name}#{
|
|
418
|
+
lines << "#{" " * depth}#{node.repetition} #{desc} #{node.name}#{" (#{ann})" if ann}"
|
|
305
419
|
node.children&.each { |c| walk.call(c, depth + 1) }
|
|
306
420
|
end
|
|
307
421
|
@root.children.each { |c| walk.call(c, 0) }
|
|
@@ -311,9 +425,16 @@ module Herringbone
|
|
|
311
425
|
|
|
312
426
|
private
|
|
313
427
|
|
|
428
|
+
# Appends a Column for every leaf under +node+, depth-first. A non-required node adds one
|
|
429
|
+
# definition level and a repeated node one repetition level.
|
|
430
|
+
#
|
|
431
|
+
# @param node [Node] group whose descendants are walked
|
|
432
|
+
# @param max_def [Integer] definition level of +node+ itself
|
|
433
|
+
# @param max_rep [Integer] repetition level of +node+ itself
|
|
434
|
+
# @return [void]
|
|
314
435
|
def collect_columns(node, max_def, max_rep)
|
|
315
436
|
node.children.each do |child|
|
|
316
|
-
d = child.repetition == :required ? max_def : max_def + 1
|
|
437
|
+
d = (child.repetition == :required) ? max_def : max_def + 1
|
|
317
438
|
r = child.repeated? ? max_rep + 1 : max_rep
|
|
318
439
|
if child.leaf?
|
|
319
440
|
@columns << Column.new(@columns.size, child, d, r)
|
|
@@ -323,6 +444,15 @@ module Herringbone
|
|
|
323
444
|
end
|
|
324
445
|
end
|
|
325
446
|
|
|
447
|
+
# Builds the logical Field for +node+, recognizing LIST and MAP annotations (including the
|
|
448
|
+
# legacy 2-level forms) and bare repeated fields.
|
|
449
|
+
#
|
|
450
|
+
# @param node [Node] physical node to interpret
|
|
451
|
+
# @param parent_def [Integer] definition level of the enclosing field
|
|
452
|
+
# @param parent_rep [Integer] repetition level of the enclosing field
|
|
453
|
+
# @param as_element [Boolean] true when +node+ is itself the repeated node of a list, so its
|
|
454
|
+
# repetition is already accounted for and it is not optional
|
|
455
|
+
# @return [Field]
|
|
326
456
|
def build_field(node, parent_def, parent_rep, as_element: false)
|
|
327
457
|
if node.repeated? && !as_element
|
|
328
458
|
# A repeated field outside of a LIST/MAP annotation is a list of required elements
|
|
@@ -357,8 +487,10 @@ module Herringbone
|
|
|
357
487
|
rr = parent_rep + 1
|
|
358
488
|
key = build_field(kv.children[0], rd, rr)
|
|
359
489
|
# A map without values is read as a list of its keys, like Arrow does
|
|
360
|
-
|
|
361
|
-
|
|
490
|
+
unless kv.children[1]
|
|
491
|
+
return Field.new(kind: :list, name: node.name, optional: optional, def_level: d,
|
|
492
|
+
rep_level: rr, item_def: rd, element: key, node: node)
|
|
493
|
+
end
|
|
362
494
|
value = build_field(kv.children[1], rd, rr)
|
|
363
495
|
Field.new(kind: :map, name: node.name, optional: optional, def_level: d,
|
|
364
496
|
rep_level: rr, item_def: rd, key: key, value: value, node: node)
|
|
@@ -370,6 +502,11 @@ module Herringbone
|
|
|
370
502
|
end
|
|
371
503
|
|
|
372
504
|
# Backward-compatibility rules from the Parquet LogicalTypes spec
|
|
505
|
+
#
|
|
506
|
+
# @param list_node [Node] LIST-annotated group
|
|
507
|
+
# @param repeated [Node] its only (repeated) child
|
|
508
|
+
# @return [Boolean] true when +repeated+ is the element itself (2-level list), false when its
|
|
509
|
+
# single child is the element (standard 3-level list)
|
|
373
510
|
def list_element_is_repeated_node?(list_node, repeated)
|
|
374
511
|
return true if repeated.leaf?
|
|
375
512
|
return true if repeated.children.size > 1
|
|
@@ -393,12 +530,16 @@ module Herringbone
|
|
|
393
530
|
#
|
|
394
531
|
# Fields are nullable unless null: false is given.
|
|
395
532
|
class Builder
|
|
533
|
+
# @return [Array<Node>] fields declared so far, in declaration order
|
|
396
534
|
attr_reader :nodes
|
|
397
535
|
|
|
536
|
+
# Starts with no fields; Schema.define evaluates the block against this builder
|
|
398
537
|
def initialize
|
|
399
538
|
@nodes = []
|
|
400
539
|
end
|
|
401
540
|
|
|
541
|
+
# Types that need no options; each gets a DSL method taking a name and the options of #column,
|
|
542
|
+
# e.g. +int64 :id, null: false+
|
|
402
543
|
PRIMITIVES = %i[
|
|
403
544
|
boolean int8 int16 int32 int64 uint8 uint16 uint32 uint64 float double float16
|
|
404
545
|
string binary json bson uuid date int96
|
|
@@ -408,22 +549,90 @@ module Herringbone
|
|
|
408
549
|
define_method(t) { |name, **opts| column(name, t, **opts) }
|
|
409
550
|
end
|
|
410
551
|
|
|
552
|
+
# A TIME column; millis are stored as INT32, micros and nanos as INT64.
|
|
553
|
+
#
|
|
554
|
+
# @param name [String, Symbol] field name
|
|
555
|
+
# @param unit [Symbol] +:millis+, +:micros+ or +:nanos+
|
|
556
|
+
# @param utc [Boolean] the isAdjustedToUTC flag of the logical type
|
|
557
|
+
# @param opts [Hash{Symbol => Object}] options of #column
|
|
558
|
+
# @option opts [Boolean] :null (true) whether the field is nullable
|
|
559
|
+
# @option opts [Integer, nil] :field_id (nil) field id to store in the schema
|
|
560
|
+
# @return [Node] the added node
|
|
561
|
+
# @raise [ArgumentError] for an unknown unit or a duplicate name
|
|
411
562
|
def time(name, unit: :micros, utc: true, **opts) = column(name, :time, unit: unit, utc: utc, **opts)
|
|
563
|
+
# A TIMESTAMP column, stored as INT64.
|
|
564
|
+
#
|
|
565
|
+
# @param name [String, Symbol] field name
|
|
566
|
+
# @param unit [Symbol] +:millis+, +:micros+ or +:nanos+
|
|
567
|
+
# @param utc [Boolean] the isAdjustedToUTC flag: true for instants, false for local date-times
|
|
568
|
+
# @param opts [Hash{Symbol => Object}] options of #column
|
|
569
|
+
# @option opts [Boolean] :null (true) whether the field is nullable
|
|
570
|
+
# @option opts [Integer, nil] :field_id (nil) field id to store in the schema
|
|
571
|
+
# @return [Node] the added node
|
|
572
|
+
# @raise [ArgumentError] for an unknown unit or a duplicate name
|
|
412
573
|
def timestamp(name, unit: :micros, utc: true, **opts) = column(name, :timestamp, unit: unit, utc: utc, **opts)
|
|
574
|
+
# A DECIMAL column. Up to 9 digits are stored as INT32, up to 18 as INT64, more as a
|
|
575
|
+
# FIXED_LEN_BYTE_ARRAY of the minimal width, unless +physical:+ says otherwise.
|
|
576
|
+
#
|
|
577
|
+
# @param name [String, Symbol] field name
|
|
578
|
+
# @param precision [Integer] total number of digits
|
|
579
|
+
# @param scale [Integer] digits after the decimal point, between 0 and +precision+
|
|
580
|
+
# @param opts [Hash{Symbol => Object}] options of #column
|
|
581
|
+
# @option opts [Symbol] :physical storage: +:int32+, +:int64+, +:binary+ or +:fixed+
|
|
582
|
+
# @option opts [Boolean] :null (true) whether the field is nullable
|
|
583
|
+
# @option opts [Integer, nil] :field_id (nil) field id to store in the schema
|
|
584
|
+
# @return [Node] the added node
|
|
585
|
+
# @raise [ArgumentError] for an invalid precision or scale, or a duplicate name
|
|
413
586
|
def decimal(name, precision:, scale: 0, **opts) = column(name, :decimal, precision: precision, scale: scale, **opts)
|
|
587
|
+
# A FIXED_LEN_BYTE_ARRAY column without annotation.
|
|
588
|
+
#
|
|
589
|
+
# @param name [String, Symbol] field name
|
|
590
|
+
# @param length [Integer] byte width of every value
|
|
591
|
+
# @param opts [Hash{Symbol => Object}] options of #column
|
|
592
|
+
# @option opts [Boolean] :null (true) whether the field is nullable
|
|
593
|
+
# @option opts [Integer, nil] :field_id (nil) field id to store in the schema
|
|
594
|
+
# @return [Node] the added node
|
|
595
|
+
# @raise [ArgumentError] for a duplicate name
|
|
414
596
|
def fixed(name, length:, **opts) = column(name, :fixed, length: length, **opts)
|
|
415
597
|
|
|
598
|
+
# A group of named fields.
|
|
599
|
+
#
|
|
600
|
+
# @param name [String, Symbol] field name
|
|
601
|
+
# @param null [Boolean] whether the struct as a whole may be null
|
|
602
|
+
# @param field_id [Integer, nil] field id to store in the schema
|
|
603
|
+
# @yield block evaluated with +instance_eval+ on a new Builder, declaring the struct's fields
|
|
604
|
+
# @return [Node] the added group node
|
|
605
|
+
# @raise [ArgumentError] when the block declares no fields (or is missing), or for a duplicate name
|
|
416
606
|
def struct(name, null: true, field_id: nil, &block)
|
|
417
|
-
|
|
418
|
-
inner.instance_eval(&block)
|
|
419
|
-
raise ArgumentError, "struct #{name} has no fields" if inner.nodes.empty?
|
|
420
|
-
add Node.new(name: name, repetition: rep(null), children: inner.nodes, field_id: field_id)
|
|
607
|
+
add Node.new(name: name, repetition: rep(null), children: struct_fields(name, &block), field_id: field_id)
|
|
421
608
|
end
|
|
422
609
|
|
|
423
610
|
# list :tags, :string
|
|
424
611
|
# list :tags, :string, element_null: false
|
|
425
612
|
# list :points, :struct do double :x; double :y; end
|
|
426
613
|
# list :matrix do list :element, :double end (block declares the element)
|
|
614
|
+
#
|
|
615
|
+
# Written as the standard 3-level LIST: an optional (or required) group holding a repeated
|
|
616
|
+
# group "list" whose single child is "element".
|
|
617
|
+
#
|
|
618
|
+
# @param name [String, Symbol] field name
|
|
619
|
+
# @param type [Symbol, String, nil] element type (any #column type, or +:struct+ with a block);
|
|
620
|
+
# nil when the block declares the element
|
|
621
|
+
# @param null [Boolean] whether the list itself may be null
|
|
622
|
+
# @param element_null [Boolean] whether elements may be null; ignored when the block declares
|
|
623
|
+
# the element, which then keeps its own +null:+
|
|
624
|
+
# @param field_id [Integer, nil] field id to store in the schema
|
|
625
|
+
# @param type_opts [Hash{Symbol => Object}] options of the element type (+precision:+, +unit:+...)
|
|
626
|
+
# @option type_opts [Integer] :precision decimal precision
|
|
627
|
+
# @option type_opts [Integer] :scale decimal scale
|
|
628
|
+
# @option type_opts [Symbol] :unit time or timestamp unit
|
|
629
|
+
# @option type_opts [Boolean] :utc time or timestamp UTC adjustment
|
|
630
|
+
# @option type_opts [Integer] :length FIXED_LEN_BYTE_ARRAY width
|
|
631
|
+
# @yield block evaluated with +instance_eval+ on a new Builder: the struct's fields for a
|
|
632
|
+
# +:struct+ element, or exactly one field (renamed to "element") when +type+ is nil
|
|
633
|
+
# @return [Node] the added LIST group node
|
|
634
|
+
# @raise [ArgumentError] when neither a type nor a block is given, the block declares the wrong
|
|
635
|
+
# number of fields, or for a duplicate name
|
|
427
636
|
def list(name, type = nil, null: true, element_null: true, field_id: nil, **type_opts, &block)
|
|
428
637
|
element = element_node("element", type, element_null, type_opts, &block)
|
|
429
638
|
repeated = Node.new(name: "list", repetition: :repeated, children: [element])
|
|
@@ -434,6 +643,27 @@ module Herringbone
|
|
|
434
643
|
|
|
435
644
|
# map :scores, :string, :double
|
|
436
645
|
# map :things, :string, :struct do int32 :a end
|
|
646
|
+
#
|
|
647
|
+
# Written as the standard MAP: a group holding a repeated group "key_value" with a required
|
|
648
|
+
# "key" and a "value". Keys are never null.
|
|
649
|
+
#
|
|
650
|
+
# @param name [String, Symbol] field name
|
|
651
|
+
# @param key_type [Symbol, String] key type (a primitive #column type)
|
|
652
|
+
# @param value_type [Symbol, String, nil] value type (any #column type, or +:struct+ with a
|
|
653
|
+
# block); nil when the block declares the value
|
|
654
|
+
# @param null [Boolean] whether the map itself may be null
|
|
655
|
+
# @param value_null [Boolean] whether values may be null; ignored when the block declares the value
|
|
656
|
+
# @param field_id [Integer, nil] field id to store in the schema
|
|
657
|
+
# @param type_opts [Hash{Symbol => Object}] options of the value type (+precision:+, +unit:+...)
|
|
658
|
+
# @option type_opts [Integer] :precision decimal precision
|
|
659
|
+
# @option type_opts [Integer] :scale decimal scale
|
|
660
|
+
# @option type_opts [Symbol] :unit time or timestamp unit
|
|
661
|
+
# @option type_opts [Boolean] :utc time or timestamp UTC adjustment
|
|
662
|
+
# @option type_opts [Integer] :length FIXED_LEN_BYTE_ARRAY width
|
|
663
|
+
# @yield block evaluated with +instance_eval+ on a new Builder: the struct's fields for a
|
|
664
|
+
# +:struct+ value, or exactly one field (renamed to "value") when +value_type+ is nil
|
|
665
|
+
# @return [Node] the added MAP group node
|
|
666
|
+
# @raise [ArgumentError] for an invalid key or value declaration, or a duplicate name
|
|
437
667
|
def map(name, key_type, value_type = nil, null: true, value_null: true, field_id: nil, **type_opts, &block)
|
|
438
668
|
key = element_node("key", key_type, false, {})
|
|
439
669
|
value = element_node("value", value_type, value_null, type_opts, &block)
|
|
@@ -444,6 +674,23 @@ module Herringbone
|
|
|
444
674
|
end
|
|
445
675
|
|
|
446
676
|
# Generic column declaration: column :name, :int32, null: false
|
|
677
|
+
#
|
|
678
|
+
# @param name [String, Symbol] field name
|
|
679
|
+
# @param type [Symbol, String] DSL type: one of PRIMITIVES, or +:time+, +:timestamp+,
|
|
680
|
+
# +:decimal+, +:fixed+, +:enum+ with their options
|
|
681
|
+
# @param null [Boolean] whether the field is nullable (optional rather than required)
|
|
682
|
+
# @param field_id [Integer, nil] field id to store in the schema
|
|
683
|
+
# @param opts [Hash{Symbol => Object}] type options
|
|
684
|
+
# @option opts [Integer] :precision decimal precision (required for +:decimal+)
|
|
685
|
+
# @option opts [Integer] :scale (0) decimal scale
|
|
686
|
+
# @option opts [Symbol] :physical decimal storage: +:int32+, +:int64+, +:binary+ or +:fixed+
|
|
687
|
+
# @option opts [Symbol] :unit (:micros) time or timestamp unit
|
|
688
|
+
# @option opts [Boolean] :utc (true) time or timestamp UTC adjustment
|
|
689
|
+
# @option opts [Integer] :length FIXED_LEN_BYTE_ARRAY width (required for +:fixed+)
|
|
690
|
+
# @option opts [Array<String>, Hash] :values allowed values, see #enum
|
|
691
|
+
# @option opts [Boolean] :parquet_enum (false) ENUM instead of STRING annotation, see #enum
|
|
692
|
+
# @return [Node] the added leaf node
|
|
693
|
+
# @raise [ArgumentError] for an unknown type, invalid type options, or a duplicate name
|
|
447
694
|
def column(name, type, null: true, field_id: nil, **opts)
|
|
448
695
|
add leaf_node(name, type, rep(null), opts).tap { |n| n.field_id = field_id }
|
|
449
696
|
end
|
|
@@ -452,20 +699,45 @@ module Herringbone
|
|
|
452
699
|
# (note that pyarrow and pandas then read it as binary). values: restricts what can be
|
|
453
700
|
# written: an Array of labels, or a Hash like Rails' `Order.statuses` (label => stored value),
|
|
454
701
|
# in which case both labels and stored values are accepted and the label is written.
|
|
702
|
+
#
|
|
703
|
+
# @param name [String, Symbol] field name
|
|
704
|
+
# @param values [Array<String, Symbol>, Hash{String, Symbol => Object}, nil] allowed labels,
|
|
705
|
+
# or label => stored value; nil allows any string
|
|
706
|
+
# @param parquet_enum [Boolean] annotate as ENUM instead of STRING
|
|
707
|
+
# @param opts [Hash{Symbol => Object}] options of #column
|
|
708
|
+
# @option opts [Boolean] :null (true) whether the field is nullable
|
|
709
|
+
# @option opts [Integer, nil] :field_id (nil) field id to store in the schema
|
|
710
|
+
# @return [Node] the added leaf node
|
|
711
|
+
# @raise [ArgumentError] for a duplicate name
|
|
455
712
|
def enum(name, values: nil, parquet_enum: false, **opts)
|
|
456
713
|
column(name, :enum, values: values, parquet_enum: parquet_enum, **opts)
|
|
457
714
|
end
|
|
458
715
|
|
|
459
716
|
private
|
|
460
717
|
|
|
718
|
+
# @param node [Node] node to append to #nodes
|
|
719
|
+
# @return [Node] +node+
|
|
720
|
+
# @raise [ArgumentError] when a field of the same name was already declared
|
|
461
721
|
def add(node)
|
|
462
722
|
raise ArgumentError, "Duplicate field #{node.name}" if @nodes.any? { |n| n.name == node.name }
|
|
463
723
|
@nodes << node
|
|
464
724
|
node
|
|
465
725
|
end
|
|
466
726
|
|
|
727
|
+
# @param nullable [Boolean] whether the field may be null
|
|
728
|
+
# @return [Symbol] +:optional+ or +:required+
|
|
467
729
|
def rep(nullable) = nullable ? :optional : :required
|
|
468
730
|
|
|
731
|
+
# Builds the element of a list, or the key or value of a map.
|
|
732
|
+
#
|
|
733
|
+
# @param name [String] node name ("element", "key" or "value")
|
|
734
|
+
# @param type [Symbol, String, nil] DSL type, +:struct+, or nil to take the field the block declares
|
|
735
|
+
# @param nullable [Boolean] whether the node may be null (not applied to a block-declared field)
|
|
736
|
+
# @param type_opts [Hash{Symbol => Object}] type options passed to Types.physical_attributes
|
|
737
|
+
# @yield block evaluated with +instance_eval+ on a new Builder
|
|
738
|
+
# @return [Node] the element node, not added to #nodes
|
|
739
|
+
# @raise [ArgumentError] when the block is missing, declares the wrong number of fields, or
|
|
740
|
+
# declares no fields for a +:struct+
|
|
469
741
|
def element_node(name, type, nullable, type_opts, &block)
|
|
470
742
|
if type.nil?
|
|
471
743
|
raise ArgumentError, "Give an element type or a block declaring the element" unless block
|
|
@@ -476,15 +748,34 @@ module Herringbone
|
|
|
476
748
|
node.name = name
|
|
477
749
|
node
|
|
478
750
|
elsif type.to_sym == :struct
|
|
479
|
-
|
|
480
|
-
inner = Builder.new
|
|
481
|
-
inner.instance_eval(&block)
|
|
482
|
-
Node.new(name: name, repetition: rep(nullable), children: inner.nodes)
|
|
751
|
+
Node.new(name: name, repetition: rep(nullable), children: struct_fields(name, &block))
|
|
483
752
|
else
|
|
484
753
|
leaf_node(name, type, rep(nullable), type_opts)
|
|
485
754
|
end
|
|
486
755
|
end
|
|
487
756
|
|
|
757
|
+
# Evaluates a struct's block on a new Builder; Parquet groups need at least one child.
|
|
758
|
+
#
|
|
759
|
+
# @param name [String, Symbol] struct name, for the error message
|
|
760
|
+
# @yield block evaluated with +instance_eval+ on a new Builder, declaring the struct's fields
|
|
761
|
+
# @return [Array<Node>] the declared fields
|
|
762
|
+
# @raise [ArgumentError] when the block is missing or declares no fields
|
|
763
|
+
def struct_fields(name, &block)
|
|
764
|
+
raise ArgumentError, "struct #{name} needs a block declaring its fields" unless block
|
|
765
|
+
inner = Builder.new
|
|
766
|
+
inner.instance_eval(&block)
|
|
767
|
+
raise ArgumentError, "struct #{name} has no fields" if inner.nodes.empty?
|
|
768
|
+
inner.nodes
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
# @param name [String, Symbol] field name
|
|
772
|
+
# @param type [Symbol, String] DSL type
|
|
773
|
+
# @param repetition [Symbol] +:optional+ or +:required+
|
|
774
|
+
# @param opts [Hash{Symbol => Object}] type options; +:values+ is taken out as the enum values
|
|
775
|
+
# and the rest go to Types.physical_attributes
|
|
776
|
+
# @option opts [Array<String>, Hash] :values allowed values of a string/enum column
|
|
777
|
+
# @return [Node] leaf node, not added to #nodes
|
|
778
|
+
# @raise [ArgumentError] for an unknown type or invalid type options
|
|
488
779
|
def leaf_node(name, type, repetition, opts)
|
|
489
780
|
opts = opts.dup
|
|
490
781
|
values = opts.delete(:values)
|