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