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,551 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herringbone
|
|
4
|
+
# A Parquet schema. Holds two views of the same tree:
|
|
5
|
+
#
|
|
6
|
+
# * the physical tree of Schema::Node (what is stored in the footer as SchemaElements),
|
|
7
|
+
# whose leaves are the column chunks (Schema::Column), and
|
|
8
|
+
# * the logical tree of Schema::Field, which interprets LIST/MAP annotations and is
|
|
9
|
+
# what rows are assembled from (and shredded into) when reading and writing.
|
|
10
|
+
class Schema
|
|
11
|
+
REPETITIONS = {
|
|
12
|
+
Format::Repetition::REQUIRED => :required,
|
|
13
|
+
Format::Repetition::OPTIONAL => :optional,
|
|
14
|
+
Format::Repetition::REPEATED => :repeated
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# A node of the physical schema tree.
|
|
18
|
+
class Node
|
|
19
|
+
attr_accessor :name, :repetition, :type, :type_length, :converted_type, :logical_type,
|
|
20
|
+
:scale, :precision, :field_id, :children, :parent
|
|
21
|
+
# Allowed values of a string/enum column (writer-side validation only, not stored in the file)
|
|
22
|
+
attr_accessor :enum_values
|
|
23
|
+
|
|
24
|
+
def initialize(name:, repetition: :optional, type: nil, type_length: nil, converted_type: nil,
|
|
25
|
+
logical_type: nil, scale: nil, precision: nil, field_id: nil, children: nil, enum_values: nil)
|
|
26
|
+
@name = name.to_s
|
|
27
|
+
@enum_values = enum_values
|
|
28
|
+
@repetition = repetition
|
|
29
|
+
@type = type
|
|
30
|
+
@type_length = type_length
|
|
31
|
+
@converted_type = converted_type
|
|
32
|
+
@logical_type = logical_type
|
|
33
|
+
@scale = scale
|
|
34
|
+
@precision = precision
|
|
35
|
+
@field_id = field_id
|
|
36
|
+
@children = children
|
|
37
|
+
@children&.each { |c| c.parent = self }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def group? = !@children.nil?
|
|
41
|
+
def leaf? = @children.nil?
|
|
42
|
+
def repeated? = @repetition == :repeated
|
|
43
|
+
def optional? = @repetition == :optional
|
|
44
|
+
|
|
45
|
+
def logical_kind
|
|
46
|
+
@logical_type&.kind&.first
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def list_annotated?
|
|
50
|
+
logical_kind == :list || @converted_type == Format::ConvertedType::LIST
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def map_annotated?
|
|
54
|
+
logical_kind == :map || @converted_type == Format::ConvertedType::MAP ||
|
|
55
|
+
@converted_type == Format::ConvertedType::MAP_KEY_VALUE
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def path
|
|
59
|
+
parent&.parent ? parent.path + [name] : [name]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.from_element(el)
|
|
63
|
+
new(
|
|
64
|
+
name: el.name,
|
|
65
|
+
repetition: REPETITIONS.fetch(el.repetition_type || Format::Repetition::REQUIRED),
|
|
66
|
+
type: el.num_children ? nil : el.type,
|
|
67
|
+
type_length: el.type_length,
|
|
68
|
+
converted_type: el.converted_type,
|
|
69
|
+
logical_type: el.logical_type,
|
|
70
|
+
scale: el.scale,
|
|
71
|
+
precision: el.precision,
|
|
72
|
+
field_id: el.field_id
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_element(root: false)
|
|
77
|
+
Format::SchemaElement.new(
|
|
78
|
+
name: name,
|
|
79
|
+
repetition_type: root ? nil : REPETITIONS.key(repetition),
|
|
80
|
+
type: type,
|
|
81
|
+
type_length: type_length,
|
|
82
|
+
num_children: group? ? children.size : nil,
|
|
83
|
+
converted_type: converted_type,
|
|
84
|
+
logical_type: logical_type,
|
|
85
|
+
scale: scale,
|
|
86
|
+
precision: precision,
|
|
87
|
+
field_id: field_id
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A leaf column (one column chunk per row group)
|
|
93
|
+
class Column
|
|
94
|
+
attr_reader :index, :node, :path, :max_definition_level, :max_repetition_level
|
|
95
|
+
|
|
96
|
+
def initialize(index, node, max_def, max_rep)
|
|
97
|
+
@index = index
|
|
98
|
+
@node = node
|
|
99
|
+
@path = node.path
|
|
100
|
+
@max_definition_level = max_def
|
|
101
|
+
@max_repetition_level = max_rep
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def type = @node.type
|
|
105
|
+
def type_length = @node.type_length
|
|
106
|
+
def dotted_path = @path.join(".")
|
|
107
|
+
|
|
108
|
+
def converter
|
|
109
|
+
@converter ||= Types.reader_for(@node)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def encoder
|
|
113
|
+
@encoder ||= Types.writer_for(@node)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A node of the logical tree.
|
|
118
|
+
# kind: :leaf, :struct, :list or :map
|
|
119
|
+
# optional: whether this field itself may be null
|
|
120
|
+
# def_level: definition level at which this field counts as present
|
|
121
|
+
# For :list and :map:
|
|
122
|
+
# rep_level: repetition level of the repeated node
|
|
123
|
+
# item_def: definition level at which the repeated node has at least one entry
|
|
124
|
+
# element: element Field (lists); key/value Fields (maps, value may be nil)
|
|
125
|
+
class Field
|
|
126
|
+
attr_reader :kind, :name, :optional, :def_level, :rep_level, :item_def,
|
|
127
|
+
:children, :element, :key, :value, :column, :leaves, :node
|
|
128
|
+
|
|
129
|
+
def initialize(kind:, name:, optional:, def_level:, node:, rep_level: nil, item_def: nil,
|
|
130
|
+
children: nil, element: nil, key: nil, value: nil, column: nil)
|
|
131
|
+
@kind = kind
|
|
132
|
+
@name = name
|
|
133
|
+
@optional = optional
|
|
134
|
+
@def_level = def_level
|
|
135
|
+
@node = node
|
|
136
|
+
@rep_level = rep_level
|
|
137
|
+
@item_def = item_def
|
|
138
|
+
@children = children
|
|
139
|
+
@element = element
|
|
140
|
+
@key = key
|
|
141
|
+
@value = value
|
|
142
|
+
@column = column
|
|
143
|
+
@leaves = case kind
|
|
144
|
+
when :leaf then [column]
|
|
145
|
+
when :struct then children.flat_map(&:leaves)
|
|
146
|
+
when :list then element.leaves
|
|
147
|
+
when :map then key.leaves + (value ? value.leaves : [])
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def first_leaf = @leaves.first
|
|
152
|
+
def leaf? = @kind == :leaf
|
|
153
|
+
|
|
154
|
+
def children_by_name
|
|
155
|
+
@children_by_name ||= @children.to_h { |c| [c.name, c] }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
attr_reader :root, :columns, :fields
|
|
160
|
+
|
|
161
|
+
def self.from_elements(elements)
|
|
162
|
+
raise FormatError, "Empty schema" if elements.empty?
|
|
163
|
+
pos = 0
|
|
164
|
+
build = lambda do
|
|
165
|
+
el = elements[pos] or raise FormatError, "Schema is truncated"
|
|
166
|
+
pos += 1
|
|
167
|
+
node = Node.from_element(el)
|
|
168
|
+
if el.num_children
|
|
169
|
+
node.children = Array.new(el.num_children) { build.call }
|
|
170
|
+
node.children.each { |c| c.parent = node }
|
|
171
|
+
end
|
|
172
|
+
node
|
|
173
|
+
end
|
|
174
|
+
root = build.call
|
|
175
|
+
root.children ||= []
|
|
176
|
+
new(root)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Builds a schema with the DSL (see Schema::Builder), from a Hash, or both:
|
|
180
|
+
#
|
|
181
|
+
# Schema.define(id: {type: :int64, null: false}, name: :string, tags: [:string],
|
|
182
|
+
# address: {city: :string, zip: :string}, price: {type: :decimal, precision: 10, scale: 2})
|
|
183
|
+
#
|
|
184
|
+
# Hash values: a type Symbol; [element] for a list; a Hash with :type (plus DSL options such as
|
|
185
|
+
# null:, precision:, of: for lists, key:/value: for maps); or a Hash without :type for a struct.
|
|
186
|
+
def self.define(spec = nil, &block)
|
|
187
|
+
builder = Builder.new
|
|
188
|
+
builder.fields(spec) if spec
|
|
189
|
+
builder.instance_eval(&block) if block
|
|
190
|
+
raise ArgumentError, "A schema needs at least one field" if builder.nodes.empty?
|
|
191
|
+
new(Node.new(name: "schema", repetition: :required, children: builder.nodes))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Accepts a Schema or a Hash spec for Schema.define
|
|
195
|
+
def self.coerce(schema)
|
|
196
|
+
case schema
|
|
197
|
+
when Schema then schema
|
|
198
|
+
when Hash then define(schema)
|
|
199
|
+
else raise ArgumentError, "Expected a Herringbone::Schema or a Hash, got #{schema.class}"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Infers a schema from sample rows (Hashes, or objects responding to #attributes or #to_h).
|
|
204
|
+
# All fields are nullable. Integer -> int64, Integer mixed with Float -> double,
|
|
205
|
+
# String/Symbol -> string (binary if not valid UTF-8), true/false -> boolean,
|
|
206
|
+
# Time/DateTime -> timestamp(micros), Date -> date, BigDecimal -> decimal(38, max scale seen),
|
|
207
|
+
# Hash -> struct, Array -> list. Columns that are nil in every sampled row become strings.
|
|
208
|
+
# +types+ overrides inference for some columns, using the Hash spec of Schema.define:
|
|
209
|
+
# Schema.infer(rows, types: { payload: :json, status: { type: :enum, values: %w[a b] } })
|
|
210
|
+
def self.infer(rows, sample: 1000, types: {})
|
|
211
|
+
sample_rows = rows.first(sample).map { |r| Inference.row_hash(r) }
|
|
212
|
+
raise ArgumentError, "Cannot infer a schema from zero rows" if sample_rows.empty?
|
|
213
|
+
overrides = types.to_h { |k, v| [k.to_s, v] }
|
|
214
|
+
names = sample_rows.flat_map { |r| r.keys.map(&:to_s) }.uniq
|
|
215
|
+
nodes = names.map do |name|
|
|
216
|
+
if overrides.key?(name)
|
|
217
|
+
builder = Builder.new
|
|
218
|
+
builder.fields(name => overrides[name])
|
|
219
|
+
builder.nodes.first
|
|
220
|
+
else
|
|
221
|
+
values = sample_rows.map { |r| r.fetch(name) { r[name.to_sym] } }
|
|
222
|
+
Inference.node_for(name, values)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
new(Node.new(name: "schema", repetition: :required, children: nodes))
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
module Inference
|
|
229
|
+
module_function
|
|
230
|
+
|
|
231
|
+
def row_hash(row)
|
|
232
|
+
return row if row.is_a?(Hash)
|
|
233
|
+
return row.attributes if row.respond_to?(:attributes)
|
|
234
|
+
return row.to_h if row.respond_to?(:to_h)
|
|
235
|
+
raise ArgumentError, "Cannot infer a schema from a #{row.class}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def node_for(name, values)
|
|
239
|
+
present = values.compact
|
|
240
|
+
return Node.new(name: name, **Types.physical_attributes(:string)) if present.empty?
|
|
241
|
+
if present.all? { |v| v.is_a?(Hash) }
|
|
242
|
+
keys = present.flat_map { |h| h.keys.map(&:to_s) }.uniq
|
|
243
|
+
children = keys.map { |k| node_for(k, present.map { |h| h.fetch(k) { h[k.to_sym] } }) }
|
|
244
|
+
Node.new(name: name, children: children)
|
|
245
|
+
elsif present.all? { |v| v.is_a?(Array) }
|
|
246
|
+
element = node_for("element", present.flatten(1))
|
|
247
|
+
Node.new(name: name, children: [Node.new(name: "list", repetition: :repeated, children: [element])],
|
|
248
|
+
logical_type: Format::LogicalType.new(list: Format::ListType.new), converted_type: Format::ConvertedType::LIST)
|
|
249
|
+
else
|
|
250
|
+
type, opts = scalar_type(name, present)
|
|
251
|
+
Node.new(name: name, **Types.physical_attributes(type, **(opts || {})))
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def scalar_type(name, values)
|
|
256
|
+
all = ->(*classes) { values.all? { |v| classes.any? { |c| v.is_a?(c) } } }
|
|
257
|
+
if all.call(Integer)
|
|
258
|
+
[:int64]
|
|
259
|
+
elsif all.call(true.class, false.class)
|
|
260
|
+
[:boolean]
|
|
261
|
+
elsif all.call(Integer, BigDecimal)
|
|
262
|
+
scale = values.map { |v| v.is_a?(BigDecimal) ? v.to_s("F").split(".")[1].to_s.sub(/0+\z/, "").size : 0 }.max
|
|
263
|
+
[:decimal, { precision: 38, scale: scale }]
|
|
264
|
+
elsif all.call(Numeric)
|
|
265
|
+
[:double]
|
|
266
|
+
elsif all.call(String)
|
|
267
|
+
values.all? { |v| v.encoding != Encoding::BINARY && v.valid_encoding? } ? [:string] : [:binary]
|
|
268
|
+
elsif all.call(String, Symbol)
|
|
269
|
+
[:string]
|
|
270
|
+
elsif all.call(Time, DateTime)
|
|
271
|
+
[:timestamp, { unit: :micros }]
|
|
272
|
+
elsif all.call(Date)
|
|
273
|
+
[:date]
|
|
274
|
+
else
|
|
275
|
+
classes = values.map(&:class).uniq
|
|
276
|
+
raise ArgumentError, "Cannot infer a Parquet type for #{name} from #{classes.map(&:name).join(", ")}; " \
|
|
277
|
+
"pass types: { #{name}: ... }"
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def initialize(root)
|
|
283
|
+
@root = root
|
|
284
|
+
@columns = []
|
|
285
|
+
collect_columns(root, 0, 0)
|
|
286
|
+
@column_by_node = @columns.to_h { |c| [c.node, c] }
|
|
287
|
+
@fields = root.children.map { |child| build_field(child, 0, 0) }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def to_elements
|
|
291
|
+
out = []
|
|
292
|
+
walk = lambda do |node, is_root|
|
|
293
|
+
out << node.to_element(root: is_root)
|
|
294
|
+
node.children&.each { |c| walk.call(c, false) }
|
|
295
|
+
end
|
|
296
|
+
walk.call(@root, true)
|
|
297
|
+
out
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def field(name)
|
|
301
|
+
@fields.find { |f| f.name == name.to_s }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def column(path)
|
|
305
|
+
path = path.split(".") if path.is_a?(String)
|
|
306
|
+
@columns.find { |c| c.path == path }
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def inspect
|
|
310
|
+
lines = []
|
|
311
|
+
walk = lambda do |node, depth|
|
|
312
|
+
desc = if node.leaf?
|
|
313
|
+
[Format::Type::NAMES[node.type], node.type_length && "(#{node.type_length})"].compact.join
|
|
314
|
+
else
|
|
315
|
+
"group"
|
|
316
|
+
end
|
|
317
|
+
ann = node.logical_type ? node.logical_type.kind.first.to_s.upcase : Format::ConvertedType::NAMES[node.converted_type]
|
|
318
|
+
lines << "#{" " * depth}#{node.repetition} #{desc} #{node.name}#{ann ? " (#{ann})" : ""}"
|
|
319
|
+
node.children&.each { |c| walk.call(c, depth + 1) }
|
|
320
|
+
end
|
|
321
|
+
@root.children.each { |c| walk.call(c, 0) }
|
|
322
|
+
"#<Herringbone::Schema\n#{lines.join("\n")}>"
|
|
323
|
+
end
|
|
324
|
+
alias_method :to_s, :inspect
|
|
325
|
+
|
|
326
|
+
private
|
|
327
|
+
|
|
328
|
+
def collect_columns(node, max_def, max_rep)
|
|
329
|
+
node.children.each do |child|
|
|
330
|
+
d = child.repetition == :required ? max_def : max_def + 1
|
|
331
|
+
r = child.repeated? ? max_rep + 1 : max_rep
|
|
332
|
+
if child.leaf?
|
|
333
|
+
@columns << Column.new(@columns.size, child, d, r)
|
|
334
|
+
else
|
|
335
|
+
collect_columns(child, d, r)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def build_field(node, parent_def, parent_rep, as_element: false)
|
|
341
|
+
if node.repeated? && !as_element
|
|
342
|
+
# A repeated field outside of a LIST/MAP annotation is a list of required elements
|
|
343
|
+
d = parent_def + 1
|
|
344
|
+
r = parent_rep + 1
|
|
345
|
+
element = build_field(node, d, r, as_element: true)
|
|
346
|
+
return Field.new(kind: :list, name: node.name, optional: false, def_level: parent_def,
|
|
347
|
+
rep_level: r, item_def: d, element: element, node: node)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
optional = node.optional? && !as_element
|
|
351
|
+
d = optional ? parent_def + 1 : parent_def
|
|
352
|
+
|
|
353
|
+
if node.leaf?
|
|
354
|
+
Field.new(kind: :leaf, name: node.name, optional: optional, def_level: d,
|
|
355
|
+
column: @column_by_node.fetch(node), node: node)
|
|
356
|
+
elsif node.list_annotated? && node.children.size == 1 && node.children[0].repeated?
|
|
357
|
+
repeated = node.children[0]
|
|
358
|
+
rd = d + 1
|
|
359
|
+
rr = parent_rep + 1
|
|
360
|
+
element = if list_element_is_repeated_node?(node, repeated)
|
|
361
|
+
build_field(repeated, rd, rr, as_element: true)
|
|
362
|
+
else
|
|
363
|
+
build_field(repeated.children[0], rd, rr)
|
|
364
|
+
end
|
|
365
|
+
Field.new(kind: :list, name: node.name, optional: optional, def_level: d,
|
|
366
|
+
rep_level: rr, item_def: rd, element: element, node: node)
|
|
367
|
+
elsif node.map_annotated? && node.children.size == 1 && node.children[0].repeated? &&
|
|
368
|
+
node.children[0].group? && node.children[0].children.size.between?(1, 2)
|
|
369
|
+
kv = node.children[0]
|
|
370
|
+
rd = d + 1
|
|
371
|
+
rr = parent_rep + 1
|
|
372
|
+
key = build_field(kv.children[0], rd, rr)
|
|
373
|
+
# A map without values is read as a list of its keys, like Arrow does
|
|
374
|
+
return Field.new(kind: :list, name: node.name, optional: optional, def_level: d,
|
|
375
|
+
rep_level: rr, item_def: rd, element: key, node: node) unless kv.children[1]
|
|
376
|
+
value = build_field(kv.children[1], rd, rr)
|
|
377
|
+
Field.new(kind: :map, name: node.name, optional: optional, def_level: d,
|
|
378
|
+
rep_level: rr, item_def: rd, key: key, value: value, node: node)
|
|
379
|
+
else
|
|
380
|
+
children = node.children.map { |c| build_field(c, d, parent_rep) }
|
|
381
|
+
Field.new(kind: :struct, name: node.name, optional: optional, def_level: d,
|
|
382
|
+
children: children, node: node)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Backward-compatibility rules from the Parquet LogicalTypes spec
|
|
387
|
+
def list_element_is_repeated_node?(list_node, repeated)
|
|
388
|
+
return true if repeated.leaf?
|
|
389
|
+
return true if repeated.children.size > 1
|
|
390
|
+
return true if repeated.name == "array" || repeated.name == "#{list_node.name}_tuple"
|
|
391
|
+
false
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# DSL for defining schemas:
|
|
395
|
+
#
|
|
396
|
+
# Herringbone::Schema.define do
|
|
397
|
+
# int64 :id, null: false
|
|
398
|
+
# string :name
|
|
399
|
+
# list :tags, :string
|
|
400
|
+
# map :scores, :string, :double
|
|
401
|
+
# struct :address do
|
|
402
|
+
# string :city
|
|
403
|
+
# end
|
|
404
|
+
# decimal :price, precision: 12, scale: 2
|
|
405
|
+
# timestamp :created_at, unit: :micros
|
|
406
|
+
# end
|
|
407
|
+
#
|
|
408
|
+
# Fields are nullable unless null: false is given.
|
|
409
|
+
class Builder
|
|
410
|
+
attr_reader :nodes
|
|
411
|
+
|
|
412
|
+
def initialize
|
|
413
|
+
@nodes = []
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
PRIMITIVES = %i[
|
|
417
|
+
boolean int8 int16 int32 int64 uint8 uint16 uint32 uint64 float double float16
|
|
418
|
+
string binary json bson uuid date int96
|
|
419
|
+
].freeze
|
|
420
|
+
|
|
421
|
+
PRIMITIVES.each do |t|
|
|
422
|
+
define_method(t) { |name, **opts| column(name, t, **opts) }
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def time(name, unit: :micros, utc: true, **opts) = column(name, :time, unit: unit, utc: utc, **opts)
|
|
426
|
+
def timestamp(name, unit: :micros, utc: true, **opts) = column(name, :timestamp, unit: unit, utc: utc, **opts)
|
|
427
|
+
def decimal(name, precision:, scale: 0, **opts) = column(name, :decimal, precision: precision, scale: scale, **opts)
|
|
428
|
+
def fixed(name, length:, **opts) = column(name, :fixed, length: length, **opts)
|
|
429
|
+
|
|
430
|
+
def struct(name, null: true, field_id: nil, &block)
|
|
431
|
+
inner = Builder.new
|
|
432
|
+
inner.instance_eval(&block)
|
|
433
|
+
raise ArgumentError, "struct #{name} has no fields" if inner.nodes.empty?
|
|
434
|
+
add Node.new(name: name, repetition: rep(null), children: inner.nodes, field_id: field_id)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# list :tags, :string
|
|
438
|
+
# list :tags, :string, element_null: false
|
|
439
|
+
# list :points, :struct do double :x; double :y; end
|
|
440
|
+
# list :matrix do list :element, :double end (block declares the element)
|
|
441
|
+
def list(name, type = nil, null: true, element_null: true, field_id: nil, **type_opts, &block)
|
|
442
|
+
element = element_node("element", type, element_null, type_opts, &block)
|
|
443
|
+
repeated = Node.new(name: "list", repetition: :repeated, children: [element])
|
|
444
|
+
add Node.new(name: name, repetition: rep(null), children: [repeated],
|
|
445
|
+
logical_type: Format::LogicalType.new(list: Format::ListType.new),
|
|
446
|
+
converted_type: Format::ConvertedType::LIST, field_id: field_id)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# map :scores, :string, :double
|
|
450
|
+
# map :things, :string, :struct do int32 :a end
|
|
451
|
+
def map(name, key_type, value_type = nil, null: true, value_null: true, field_id: nil, **type_opts, &block)
|
|
452
|
+
key = element_node("key", key_type, false, {})
|
|
453
|
+
value = element_node("value", value_type, value_null, type_opts, &block)
|
|
454
|
+
kv = Node.new(name: "key_value", repetition: :repeated, children: [key, value])
|
|
455
|
+
add Node.new(name: name, repetition: rep(null), children: [kv],
|
|
456
|
+
logical_type: Format::LogicalType.new(map: Format::MapType.new),
|
|
457
|
+
converted_type: Format::ConvertedType::MAP, field_id: field_id)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Generic column declaration: column :name, :int32, null: false
|
|
461
|
+
def column(name, type, null: true, field_id: nil, **opts)
|
|
462
|
+
add leaf_node(name, type, rep(null), opts).tap { |n| n.field_id = field_id }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# A string column. With parquet_enum: true it carries the ENUM annotation instead of STRING
|
|
466
|
+
# (note that pyarrow and pandas then read it as binary). values: restricts what can be
|
|
467
|
+
# written: an Array of labels, or a Hash like Rails' `Order.statuses` (label => stored value),
|
|
468
|
+
# in which case both labels and stored values are accepted and the label is written.
|
|
469
|
+
def enum(name, values: nil, parquet_enum: false, **opts)
|
|
470
|
+
column(name, :enum, values: values, parquet_enum: parquet_enum, **opts)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Declares fields from a Hash spec, see Schema.define
|
|
474
|
+
def fields(spec)
|
|
475
|
+
spec.each { |name, desc| declare(name, desc) }
|
|
476
|
+
self
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Declares one field from a Hash spec value
|
|
480
|
+
def declare(name, desc, null: true)
|
|
481
|
+
case desc
|
|
482
|
+
when Symbol, String
|
|
483
|
+
column(name, desc.to_sym, null: null)
|
|
484
|
+
when Array
|
|
485
|
+
raise ArgumentError, "List spec for #{name} must have exactly one element type" unless desc.size == 1
|
|
486
|
+
element = desc.first
|
|
487
|
+
list(name, null: null) { declare(:element, element) }
|
|
488
|
+
when Hash
|
|
489
|
+
opts = desc.to_h { |k, v| [k.to_sym, v] }
|
|
490
|
+
type = opts.delete(:type)
|
|
491
|
+
null = opts.delete(:null) { null }
|
|
492
|
+
# A Hash without :type is a struct; its keys (other than null:) are the fields
|
|
493
|
+
return struct(name, null: null) { fields(opts) } if type.nil?
|
|
494
|
+
case type.to_sym
|
|
495
|
+
when :list
|
|
496
|
+
of = opts.delete(:of) or raise ArgumentError, "list #{name} needs of:"
|
|
497
|
+
element_null = opts.delete(:element_null) { true }
|
|
498
|
+
list(name, null: null, **opts) { declare(:element, of, null: element_null) }
|
|
499
|
+
when :map
|
|
500
|
+
key = opts.delete(:key) { :string }
|
|
501
|
+
value = opts.delete(:value) or raise ArgumentError, "map #{name} needs value:"
|
|
502
|
+
value_null = opts.delete(:value_null) { true }
|
|
503
|
+
map(name, key, null: null, **opts) { declare(:value, value, null: value_null) }
|
|
504
|
+
when :struct
|
|
505
|
+
struct(name, null: null) { fields(opts.fetch(:fields)) }
|
|
506
|
+
else
|
|
507
|
+
column(name, type.to_sym, null: null, **opts)
|
|
508
|
+
end
|
|
509
|
+
else
|
|
510
|
+
raise ArgumentError, "Cannot declare #{name} from #{desc.inspect}"
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
private
|
|
515
|
+
|
|
516
|
+
def add(node)
|
|
517
|
+
raise ArgumentError, "Duplicate field #{node.name}" if @nodes.any? { |n| n.name == node.name }
|
|
518
|
+
@nodes << node
|
|
519
|
+
node
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def rep(nullable) = nullable ? :optional : :required
|
|
523
|
+
|
|
524
|
+
def element_node(name, type, nullable, type_opts, &block)
|
|
525
|
+
if type.nil?
|
|
526
|
+
raise ArgumentError, "Give an element type or a block declaring the element" unless block
|
|
527
|
+
inner = Builder.new
|
|
528
|
+
inner.instance_eval(&block)
|
|
529
|
+
raise ArgumentError, "The element block must declare exactly one field" unless inner.nodes.size == 1
|
|
530
|
+
node = inner.nodes.first
|
|
531
|
+
node.name = name
|
|
532
|
+
node
|
|
533
|
+
elsif type.to_sym == :struct
|
|
534
|
+
raise ArgumentError, "A struct element needs a block" unless block
|
|
535
|
+
inner = Builder.new
|
|
536
|
+
inner.instance_eval(&block)
|
|
537
|
+
Node.new(name: name, repetition: rep(nullable), children: inner.nodes)
|
|
538
|
+
else
|
|
539
|
+
leaf_node(name, type, rep(nullable), type_opts)
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def leaf_node(name, type, repetition, opts)
|
|
544
|
+
opts = opts.dup
|
|
545
|
+
values = opts.delete(:values)
|
|
546
|
+
attrs = Types.physical_attributes(type.to_sym, **opts)
|
|
547
|
+
Node.new(name: name, repetition: repetition, enum_values: values, **attrs)
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|