herringbone 0.6.0 → 0.7.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 +40 -0
- data/MANUAL.md +161 -12
- data/README.md +19 -4
- data/bin/herringbone +56 -1
- data/lib/herringbone/active_record.rb +2 -0
- data/lib/herringbone/bloom_filter.rb +2 -0
- data/lib/herringbone/byte_values.rb +2 -0
- data/lib/herringbone/codecs/lz4.rb +9 -4
- data/lib/herringbone/codecs/lzo.rb +2 -0
- data/lib/herringbone/codecs/snappy.rb +48 -6
- data/lib/herringbone/combiner.rb +426 -0
- data/lib/herringbone/compression.rb +53 -9
- data/lib/herringbone/encodings/delta.rb +4 -0
- data/lib/herringbone/encodings/plain.rb +2 -0
- data/lib/herringbone/encodings/rle.rb +2 -0
- data/lib/herringbone/encryption.rb +2 -0
- data/lib/herringbone/inspector.rb +29 -5
- data/lib/herringbone/io_buffer_support.rb +2 -0
- data/lib/herringbone/reader/bloom_filters.rb +1 -0
- data/lib/herringbone/reader/chunk_copier.rb +177 -0
- data/lib/herringbone/reader/column_chunk_reader.rb +4 -2
- data/lib/herringbone/reader/column_cursor.rb +2 -0
- data/lib/herringbone/reader/numo.rb +4 -0
- data/lib/herringbone/reader/page_stream.rb +2 -0
- data/lib/herringbone/reader/scan.rb +2 -0
- data/lib/herringbone/reader.rb +19 -6
- data/lib/herringbone/redaction/rewriter.rb +51 -157
- data/lib/herringbone/redaction.rb +25 -15
- data/lib/herringbone/restricted_readable_io.rb +85 -0
- data/lib/herringbone/schema/merge.rb +355 -0
- data/lib/herringbone/schema.rb +80 -0
- data/lib/herringbone/thrift.rb +166 -68
- data/lib/herringbone/types.rb +2 -0
- data/lib/herringbone/version.rb +1 -1
- data/lib/herringbone/visualizer.rb +2 -0
- data/lib/herringbone/writer.rb +58 -22
- data/lib/herringbone/xxhash.rb +2 -0
- data/lib/herringbone.rb +96 -9
- metadata +6 -2
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herringbone
|
|
4
|
+
class Schema
|
|
5
|
+
# The union or intersection of two schemas, behind Schema#union and Schema#intersect. Fields
|
|
6
|
+
# are matched by name at every level of nesting, and the type of a field both schemas have is
|
|
7
|
+
# widened to one that holds the values of both without loss. Fields that do not fit are
|
|
8
|
+
# collected rather than raised one by one, so a single IncompatibleSchema names them all.
|
|
9
|
+
#
|
|
10
|
+
# Leaf types are compared as Arrays like +[:int, 32, true]+ or +[:timestamp, :micros, false]+,
|
|
11
|
+
# so the same type spelled with a logical type in one file and a converted type in another
|
|
12
|
+
# still matches.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
class Merge
|
|
16
|
+
# Shorthand for Format::Type
|
|
17
|
+
T = Format::Type
|
|
18
|
+
# Shorthand for Format::ConvertedType
|
|
19
|
+
C = Format::ConvertedType
|
|
20
|
+
|
|
21
|
+
# Converted integer annotations => [bit width, signed]
|
|
22
|
+
INT_CONVERTED = {
|
|
23
|
+
C::INT_8 => [8, true], C::INT_16 => [16, true], C::INT_32 => [32, true], C::INT_64 => [64, true],
|
|
24
|
+
C::UINT_8 => [8, false], C::UINT_16 => [16, false], C::UINT_32 => [32, false], C::UINT_64 => [64, false]
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Converted annotations of time and timestamp columns => [kind, unit, adjusted to UTC]
|
|
28
|
+
TIME_CONVERTED = {
|
|
29
|
+
C::TIME_MILLIS => [:time, :millis, true], C::TIME_MICROS => [:time, :micros, true],
|
|
30
|
+
C::TIMESTAMP_MILLIS => [:timestamp, :millis, true], C::TIMESTAMP_MICROS => [:timestamp, :micros, true]
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# Converted annotations of UTF-8 byte arrays => the text kind
|
|
34
|
+
TEXT_CONVERTED = {C::UTF8 => :string, C::ENUM => :enum, C::JSON => :json}.freeze
|
|
35
|
+
|
|
36
|
+
# Time units from coarsest to finest
|
|
37
|
+
TIME_UNITS = %i[millis micros nanos].freeze
|
|
38
|
+
|
|
39
|
+
# Float width => bits of its significand, which is how wide an Integer it holds exactly
|
|
40
|
+
FLOAT_SIGNIFICANDS = {16 => 11, 32 => 24, 64 => 53}.freeze
|
|
41
|
+
|
|
42
|
+
# @param mode [Symbol] +:union+ or +:intersect+
|
|
43
|
+
def initialize(mode)
|
|
44
|
+
@mode = mode
|
|
45
|
+
@conflicts = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param mine [Schema] the receiver, whose field order comes first
|
|
49
|
+
# @param theirs [Schema] the other schema
|
|
50
|
+
# @return [Schema] a new schema sharing no nodes with either
|
|
51
|
+
# @raise [IncompatibleSchema] listing every field that does not fit
|
|
52
|
+
def call(mine, theirs)
|
|
53
|
+
nodes = merge_members(mine.fields, theirs.fields, [])
|
|
54
|
+
unless @conflicts.empty?
|
|
55
|
+
raise IncompatibleSchema.new(@conflicts, operation: (@mode == :union) ? "unite" : "intersect")
|
|
56
|
+
end
|
|
57
|
+
Schema.new(Node.new(name: mine.root.name, repetition: :required, children: nodes))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param field [Field] any field
|
|
61
|
+
# @return [String] its type as error messages name it: the leaf type's label ("uint16",
|
|
62
|
+
# "timestamp(millis, UTC)", "decimal(10, 2)"...), or "struct", "list" or "map"
|
|
63
|
+
def describe(field) = kind_label(field)
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Matches the members of two groups (or the top-level fields) by name: those of +mine+ in
|
|
68
|
+
# order, then, for a union, those only +theirs+ has.
|
|
69
|
+
#
|
|
70
|
+
# @param mine [Array<Field>] fields of the receiver
|
|
71
|
+
# @param theirs [Array<Field>] fields of the other schema
|
|
72
|
+
# @param path [Array<String>] path of the enclosing group, empty at the top level
|
|
73
|
+
# @return [Array<Node>] merged nodes; incomplete when a conflict was recorded
|
|
74
|
+
def merge_members(mine, theirs, path)
|
|
75
|
+
by_name = theirs.to_h { |f| [f.name, f] }
|
|
76
|
+
names = mine.map(&:name)
|
|
77
|
+
if @mode == :intersect && (names & by_name.keys).empty?
|
|
78
|
+
conflict(path, "fields #{names.join(", ")}", "fields #{by_name.keys.join(", ")}", "no fields in common")
|
|
79
|
+
return []
|
|
80
|
+
end
|
|
81
|
+
nodes = mine.map do |field|
|
|
82
|
+
if (twin = by_name[field.name])
|
|
83
|
+
merge(field, twin, path + [field.name])
|
|
84
|
+
elsif @mode == :union
|
|
85
|
+
nullable(field, path + [field.name], :left)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
if @mode == :union
|
|
89
|
+
nodes += theirs.reject { |f| names.include?(f.name) }.map { |f| nullable(f, path + [f.name], :right) }
|
|
90
|
+
end
|
|
91
|
+
nodes.compact
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A field only one side of a union has: rows from the other side hold no value for it
|
|
95
|
+
#
|
|
96
|
+
# @param field [Field] the field only one schema has
|
|
97
|
+
# @param path [Array<String>] path of the field
|
|
98
|
+
# @param side [Symbol] +:left+ or +:right+, the schema that has it
|
|
99
|
+
# @return [Node, nil] an optional copy of the field's node; nil for a conflict
|
|
100
|
+
def nullable(field, path, side)
|
|
101
|
+
unless field.node.repeated?
|
|
102
|
+
return copy(field.node, repetition: :optional)
|
|
103
|
+
end
|
|
104
|
+
sides = ["repeated #{kind_label(field.element)}", "absent"]
|
|
105
|
+
conflict(path, *((side == :left) ? sides : sides.reverse), "a repeated field cannot be null, so both schemas need it")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @param a [Field] the field in the receiver
|
|
109
|
+
# @param b [Field] the field of the same name in the other schema
|
|
110
|
+
# @param path [Array<String>] path of the field
|
|
111
|
+
# @return [Node, nil] the merged node; nil when a conflict was recorded
|
|
112
|
+
def merge(a, b, path)
|
|
113
|
+
an = a.node
|
|
114
|
+
bn = b.node
|
|
115
|
+
a_id = field_id_of(a)
|
|
116
|
+
b_id = field_id_of(b)
|
|
117
|
+
if a_id && b_id && a_id != b_id
|
|
118
|
+
return conflict(path, "field_id #{a_id}", "field_id #{b_id}", "different field ids, so different columns")
|
|
119
|
+
end
|
|
120
|
+
if a.kind != b.kind
|
|
121
|
+
return conflict(path, kind_label(a), kind_label(b), "a #{a.kind} and a #{b.kind} do not mix")
|
|
122
|
+
end
|
|
123
|
+
both_bare = a.kind == :list && an.repeated? && bn.repeated?
|
|
124
|
+
repetition = if both_bare then :repeated
|
|
125
|
+
elsif a.optional || b.optional then :optional
|
|
126
|
+
else :required
|
|
127
|
+
end
|
|
128
|
+
field_id = a_id || b_id
|
|
129
|
+
if shape(an) == shape(bn)
|
|
130
|
+
enum_values = an.enum_values if an.enum_values == bn.enum_values
|
|
131
|
+
return copy(an, repetition: repetition, field_id: field_id, enum_values: enum_values)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
case a.kind
|
|
135
|
+
when :leaf
|
|
136
|
+
attrs = merge_leaf(an, bn, path) or return
|
|
137
|
+
enum_values = an.enum_values if an.enum_values == bn.enum_values
|
|
138
|
+
Node.new(name: an.name, repetition: repetition, field_id: field_id, enum_values: enum_values, **attrs)
|
|
139
|
+
when :struct
|
|
140
|
+
children = merge_members(a.children, b.children, path)
|
|
141
|
+
Node.new(name: an.name, repetition: repetition, field_id: field_id, children: children) unless children.empty?
|
|
142
|
+
when :list
|
|
143
|
+
element = merge(a.element, b.element, path + ["element"]) or return
|
|
144
|
+
if both_bare
|
|
145
|
+
element.name = an.name
|
|
146
|
+
element.repetition = :repeated
|
|
147
|
+
element.field_id = field_id
|
|
148
|
+
return element
|
|
149
|
+
end
|
|
150
|
+
element.name = "element"
|
|
151
|
+
Node.new(name: an.name, repetition: repetition, field_id: field_id,
|
|
152
|
+
children: [Node.new(name: "list", repetition: :repeated, children: [element])],
|
|
153
|
+
logical_type: Format::LogicalType.new(list: Format::ListType.new), converted_type: C::LIST)
|
|
154
|
+
when :map
|
|
155
|
+
key = merge(a.key, b.key, path + ["key"])
|
|
156
|
+
value = merge(a.value, b.value, path + ["value"])
|
|
157
|
+
return unless key && value
|
|
158
|
+
key.name = "key"
|
|
159
|
+
key.repetition = :required
|
|
160
|
+
value.name = "value"
|
|
161
|
+
Node.new(name: an.name, repetition: repetition, field_id: field_id,
|
|
162
|
+
children: [Node.new(name: "key_value", repetition: :repeated, children: [key, value])],
|
|
163
|
+
logical_type: Format::LogicalType.new(map: Format::MapType.new), converted_type: C::MAP)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @param an [Node] leaf in the receiver
|
|
168
|
+
# @param bn [Node] leaf of the same name in the other schema
|
|
169
|
+
# @param path [Array<String>] path of the field
|
|
170
|
+
# @return [Hash{Symbol => Object}, nil] physical attributes for Node.new; nil when a conflict
|
|
171
|
+
# was recorded
|
|
172
|
+
def merge_leaf(an, bn, path)
|
|
173
|
+
ta = leaf_type(an)
|
|
174
|
+
tb = leaf_type(bn)
|
|
175
|
+
return attributes_of(an) if ta == tb && ta.first == :other
|
|
176
|
+
type, reason = (ta == tb) ? [ta] : widen(ta, tb)
|
|
177
|
+
return conflict(path, label(ta), label(tb), reason) if reason
|
|
178
|
+
attributes(type)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @param field [Field] any field
|
|
182
|
+
# @return [Integer, nil] the field id of its node, nil for the element of a bare repeated
|
|
183
|
+
# field, whose node (and field id) is that of the list
|
|
184
|
+
def field_id_of(field)
|
|
185
|
+
field.node.field_id unless field.node.repeated? && field.kind != :list
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @param ta [Array] leaf type of the receiver's field (see #leaf_type)
|
|
189
|
+
# @param tb [Array] a different leaf type of the other schema's field
|
|
190
|
+
# @return [Array(Array, nil), Array(nil, String)] the widened type, or why there is none
|
|
191
|
+
def widen(ta, tb)
|
|
192
|
+
case [ta.first, tb.first]
|
|
193
|
+
when [:int, :int] then widen_ints(ta, tb)
|
|
194
|
+
when [:float, :float] then [[:float, [ta[1], tb[1]].max]]
|
|
195
|
+
when [:int, :float] then int_to_float(ta, tb)
|
|
196
|
+
when [:float, :int] then int_to_float(tb, ta)
|
|
197
|
+
when [:timestamp, :timestamp], [:time, :time]
|
|
198
|
+
return [nil, "one is adjusted to UTC and the other is not"] if ta[2] != tb[2]
|
|
199
|
+
[[ta[0], TIME_UNITS[[TIME_UNITS.index(ta[1]), TIME_UNITS.index(tb[1])].max], ta[2]]]
|
|
200
|
+
when [:text, :text] then [[:text, :string]]
|
|
201
|
+
when [:text, :binary], [:binary, :text] then [[:binary]]
|
|
202
|
+
when [:decimal, :decimal] then [nil, "decimals need the same precision and scale"]
|
|
203
|
+
else [nil, "no common type"]
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @param ta [Array] an integer type
|
|
208
|
+
# @param tb [Array] another integer type
|
|
209
|
+
# @return [Array(Array, nil), Array(nil, String)] the narrowest integer holding both, or why
|
|
210
|
+
# there is none
|
|
211
|
+
def widen_ints(ta, tb)
|
|
212
|
+
_, a_bits, a_signed = ta
|
|
213
|
+
_, b_bits, b_signed = tb
|
|
214
|
+
return [[:int, [a_bits, b_bits].max, a_signed]] if a_signed == b_signed
|
|
215
|
+
signed_bits, unsigned_bits = a_signed ? [a_bits, b_bits] : [b_bits, a_bits]
|
|
216
|
+
return [nil, "uint64 does not fit any signed integer"] if unsigned_bits == 64
|
|
217
|
+
[[:int, [signed_bits, unsigned_bits * 2].max, true]]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @param int [Array] an integer type
|
|
221
|
+
# @param float [Array] a float type
|
|
222
|
+
# @return [Array(Array, nil), Array(nil, String)] the narrowest float at least as wide as
|
|
223
|
+
# +float+ that holds every value of +int+ exactly, or why there is none
|
|
224
|
+
def int_to_float(int, float)
|
|
225
|
+
_, bits, signed = int
|
|
226
|
+
magnitude = signed ? bits - 1 : bits
|
|
227
|
+
width = FLOAT_SIGNIFICANDS.find { |w, significand| w >= float[1] && significand >= magnitude }&.first
|
|
228
|
+
return [nil, "#{label(int)} does not fit a double exactly"] unless width
|
|
229
|
+
[[:float, width]]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# @param node [Node] a leaf
|
|
233
|
+
# @return [Array] its type, independent of whether a logical or converted type spells it;
|
|
234
|
+
# +[:other, ...]+ for annotations nothing widens (INTERVAL, unknown logical types)
|
|
235
|
+
def leaf_type(node)
|
|
236
|
+
lt = node.logical_type
|
|
237
|
+
case node.logical_kind
|
|
238
|
+
when :integer then [:int, lt.integer.bit_width, lt.integer.is_signed]
|
|
239
|
+
when :decimal then [:decimal, lt.decimal.precision, lt.decimal.scale]
|
|
240
|
+
when :timestamp then [:timestamp, lt.timestamp.unit.to_sym, lt.timestamp.is_adjusted_to_utc]
|
|
241
|
+
when :time then [:time, lt.time.unit.to_sym, lt.time.is_adjusted_to_utc]
|
|
242
|
+
when :string, :enum, :json then [:text, node.logical_kind]
|
|
243
|
+
when :float16 then [:float, 16]
|
|
244
|
+
when :date, :uuid, :bson then [node.logical_kind]
|
|
245
|
+
when nil
|
|
246
|
+
# A logical type this version does not know decodes as an empty union
|
|
247
|
+
lt ? [:other, shape(node), "an unknown logical type"] : converted_type(node)
|
|
248
|
+
else [:other, shape(node), node.logical_kind.to_s]
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# @param node [Node] a leaf without a logical type
|
|
253
|
+
# @return [Array] its type, see #leaf_type
|
|
254
|
+
def converted_type(node)
|
|
255
|
+
ct = node.converted_type
|
|
256
|
+
if (int = INT_CONVERTED[ct]) then [:int, *int]
|
|
257
|
+
elsif (time = TIME_CONVERTED[ct]) then time
|
|
258
|
+
elsif (text = TEXT_CONVERTED[ct]) then [:text, text]
|
|
259
|
+
elsif ct == C::DECIMAL then [:decimal, node.precision, node.scale || 0]
|
|
260
|
+
elsif ct == C::DATE then [:date]
|
|
261
|
+
elsif ct == C::BSON then [:bson]
|
|
262
|
+
elsif ct then [:other, shape(node), C::NAMES[ct].to_s.downcase]
|
|
263
|
+
else
|
|
264
|
+
case node.type
|
|
265
|
+
when T::BOOLEAN then [:boolean]
|
|
266
|
+
when T::INT32 then [:int, 32, true]
|
|
267
|
+
when T::INT64 then [:int, 64, true]
|
|
268
|
+
when T::INT96 then [:int96]
|
|
269
|
+
when T::FLOAT then [:float, 32]
|
|
270
|
+
when T::DOUBLE then [:float, 64]
|
|
271
|
+
when T::BYTE_ARRAY then [:binary]
|
|
272
|
+
when T::FIXED_LEN_BYTE_ARRAY then [:fixed, node.type_length]
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# @param type [Array] a leaf type, see #leaf_type
|
|
278
|
+
# @return [Hash{Symbol => Object}] physical attributes for Node.new, as the Builder DSL
|
|
279
|
+
# declares that type
|
|
280
|
+
def attributes(type)
|
|
281
|
+
case type
|
|
282
|
+
in [:int, 32, true] then Types.physical_attributes(:int32)
|
|
283
|
+
in [:int, 64, true] then Types.physical_attributes(:int64)
|
|
284
|
+
in [:int, bits, signed] then Types.int_type(bits, signed)
|
|
285
|
+
in [:float, bits] then Types.physical_attributes({16 => :float16, 32 => :float, 64 => :double}.fetch(bits))
|
|
286
|
+
in [:timestamp | :time => kind, unit, utc] then Types.physical_attributes(kind, unit: unit, utc: utc)
|
|
287
|
+
in [:text, :enum] then Types.physical_attributes(:enum, parquet_enum: true)
|
|
288
|
+
in [:text, kind] then Types.physical_attributes(kind)
|
|
289
|
+
in [:decimal, precision, scale] then Types.physical_attributes(:decimal, precision: precision, scale: scale)
|
|
290
|
+
in [:fixed, length] then Types.physical_attributes(:fixed, length: length)
|
|
291
|
+
in [kind] then Types.physical_attributes(kind)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# @param node [Node] a leaf
|
|
296
|
+
# @return [Hash{Symbol => Object}] its physical attributes as they are
|
|
297
|
+
def attributes_of(node)
|
|
298
|
+
{type: node.type, type_length: node.type_length, converted_type: node.converted_type,
|
|
299
|
+
logical_type: node.logical_type, scale: node.scale, precision: node.precision}
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# @param type [Array] a leaf type, see #leaf_type
|
|
303
|
+
# @return [String] e.g. "uint16", "timestamp(millis, UTC)", "decimal(10, 2)"
|
|
304
|
+
def label(type)
|
|
305
|
+
case type
|
|
306
|
+
in [:int, bits, signed] then "#{"u" unless signed}int#{bits}"
|
|
307
|
+
in [:float, bits] then {16 => "float16", 32 => "float", 64 => "double"}.fetch(bits)
|
|
308
|
+
in [:timestamp | :time => kind, unit, utc] then "#{kind}(#{unit}, #{utc ? "UTC" : "local"})"
|
|
309
|
+
in [:text, kind] then kind.to_s
|
|
310
|
+
in [:decimal, precision, scale] then "decimal(#{precision}, #{scale})"
|
|
311
|
+
in [:fixed, length] then "fixed(#{length})"
|
|
312
|
+
in [:other, _, description] then description
|
|
313
|
+
in [kind] then kind.to_s
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# @param field [Field] any field
|
|
318
|
+
# @return [String] the leaf type's label, or "struct", "list" or "map"
|
|
319
|
+
def kind_label(field)
|
|
320
|
+
field.leaf? ? label(leaf_type(field.node)) : field.kind.to_s
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# What two nodes must share to be merged as they are: Node#signature without the name,
|
|
324
|
+
# repetition and field id of the node itself
|
|
325
|
+
#
|
|
326
|
+
# @param node [Node] any node
|
|
327
|
+
# @return [Array]
|
|
328
|
+
def shape(node)
|
|
329
|
+
node.signature.values_at(2, 3, 4, 6)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# @param node [Node] node to copy, with its children
|
|
333
|
+
# @param changes [Hash{Symbol => Object}] attributes to set on the copy (not on its children)
|
|
334
|
+
# @option changes [Symbol] :repetition repetition of the copy
|
|
335
|
+
# @option changes [Integer, nil] :field_id field id of the copy
|
|
336
|
+
# @option changes [Array<String>, Hash, nil] :enum_values enum values of the copy
|
|
337
|
+
# @return [Node] a copy sharing no nodes with the original
|
|
338
|
+
def copy(node, **changes)
|
|
339
|
+
attrs = {name: node.name, repetition: node.repetition, field_id: node.field_id,
|
|
340
|
+
enum_values: node.enum_values, children: node.children&.map { |c| copy(c) }}
|
|
341
|
+
Node.new(**attrs.merge(attributes_of(node)).merge(changes))
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# @param path [Array<String>] path of the field
|
|
345
|
+
# @param left [String] the field in the receiver
|
|
346
|
+
# @param right [String] the field in the other schema
|
|
347
|
+
# @param reason [String] why they do not fit
|
|
348
|
+
# @return [nil]
|
|
349
|
+
def conflict(path, left, right, reason)
|
|
350
|
+
@conflicts << IncompatibleSchema::Conflict.new(path.join("."), left, right, reason)
|
|
351
|
+
nil
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
data/lib/herringbone/schema.rb
CHANGED
|
@@ -120,6 +120,17 @@ module Herringbone
|
|
|
120
120
|
field_id: field_id
|
|
121
121
|
)
|
|
122
122
|
end
|
|
123
|
+
|
|
124
|
+
# What Schema#== compares: name, repetition, physical type and width, annotation and field id,
|
|
125
|
+
# and the same of the children. A logical type supersedes the converted type (and the
|
|
126
|
+
# decimal scale and precision, which it holds), since writers differ in whether they also
|
|
127
|
+
# store the legacy annotation. +enum_values+ is not stored in the file and does not count.
|
|
128
|
+
#
|
|
129
|
+
# @return [Array] nested Arrays of plain values, comparable with == and usable as a Hash key
|
|
130
|
+
def signature
|
|
131
|
+
annotation = logical_type ? logical_type.to_h : [converted_type, scale, precision]
|
|
132
|
+
[name, repetition, type, type_length, annotation, field_id, children&.map(&:signature)]
|
|
133
|
+
end
|
|
123
134
|
end
|
|
124
135
|
|
|
125
136
|
# A leaf column (one column chunk per row group)
|
|
@@ -296,6 +307,8 @@ module Herringbone
|
|
|
296
307
|
end
|
|
297
308
|
|
|
298
309
|
# Type inference behind Schema.infer
|
|
310
|
+
#
|
|
311
|
+
# @api private
|
|
299
312
|
module Inference
|
|
300
313
|
module_function
|
|
301
314
|
|
|
@@ -401,6 +414,62 @@ module Herringbone
|
|
|
401
414
|
@columns.find { |c| c.path == path }
|
|
402
415
|
end
|
|
403
416
|
|
|
417
|
+
# Structural equality: the same fields in the same order, with the same names, repetition,
|
|
418
|
+
# physical types and widths, annotations, field ids and nesting (see Node#signature). The name
|
|
419
|
+
# of the root (+schema+, +spark_schema+...) does not count.
|
|
420
|
+
#
|
|
421
|
+
# @param other [Object] object to compare with
|
|
422
|
+
# @return [Boolean]
|
|
423
|
+
def ==(other)
|
|
424
|
+
other.is_a?(Schema) && signature == other.signature
|
|
425
|
+
end
|
|
426
|
+
alias_method :eql?, :==
|
|
427
|
+
|
|
428
|
+
# @return [Integer] hash consistent with #==
|
|
429
|
+
def hash = signature.hash
|
|
430
|
+
|
|
431
|
+
# The fields of this schema, then those only +other+ has, matched by name at every level of
|
|
432
|
+
# nesting (struct members, list elements, map keys and values). A field only one side has
|
|
433
|
+
# becomes optional, as does one that is optional on either side. The types of fields both
|
|
434
|
+
# have are widened without loss:
|
|
435
|
+
#
|
|
436
|
+
# * integers to the wider one: int32 + int64 -> int64, uint8 + uint32 -> uint32, and unsigned
|
|
437
|
+
# with signed to a signed integer twice as wide: uint32 + int8 -> int64
|
|
438
|
+
# * floats to the wider one, and integers with floats to a float holding every integer
|
|
439
|
+
# exactly: int16 + float -> float, int32 + float -> double
|
|
440
|
+
# * timestamps and times to the finer unit: millis + micros -> micros
|
|
441
|
+
# * string, enum and json to string; any of them with binary to binary
|
|
442
|
+
#
|
|
443
|
+
# (a.schema + b.schema).fields.map(&:name) # => ["id", "name", "email"]
|
|
444
|
+
#
|
|
445
|
+
# @param other [Schema] the schema to unite with
|
|
446
|
+
# @return [Schema] a new schema, sharing no nodes with either
|
|
447
|
+
# @raise [IncompatibleSchema] listing every field without a common type (int64 + double,
|
|
448
|
+
# uint64 + signed, decimals of another precision or scale, UTC + local timestamps, a struct and
|
|
449
|
+
# a list...), with different field ids, or repeated and only in one schema
|
|
450
|
+
# @raise [ArgumentError] when +other+ is not a Schema
|
|
451
|
+
def union(other)
|
|
452
|
+
raise ArgumentError, "Expected a Herringbone::Schema, got #{other.class}" unless other.is_a?(Schema)
|
|
453
|
+
Merge.new(:union).call(self, other)
|
|
454
|
+
end
|
|
455
|
+
alias_method :+, :union
|
|
456
|
+
|
|
457
|
+
# The fields both schemas have, in the order of this one, matched by name at every level of
|
|
458
|
+
# nesting. Nullability and types are widened as by #union.
|
|
459
|
+
#
|
|
460
|
+
# (a.schema & b.schema).fields.map(&:name) # => ["id", "name"]
|
|
461
|
+
#
|
|
462
|
+
# @param other [Schema] the schema to intersect with
|
|
463
|
+
# @return [Schema] a new schema, sharing no nodes with either
|
|
464
|
+
# @raise [IncompatibleSchema] listing every field without a common type or with different field
|
|
465
|
+
# ids, and the schemas or structs that have no fields in common
|
|
466
|
+
# @raise [ArgumentError] when +other+ is not a Schema
|
|
467
|
+
def intersect(other)
|
|
468
|
+
raise ArgumentError, "Expected a Herringbone::Schema, got #{other.class}" unless other.is_a?(Schema)
|
|
469
|
+
Merge.new(:intersect).call(self, other)
|
|
470
|
+
end
|
|
471
|
+
alias_method :&, :intersect
|
|
472
|
+
|
|
404
473
|
# @return [String] one line per node, indented by depth: repetition, physical type, name and
|
|
405
474
|
# annotation
|
|
406
475
|
def inspect
|
|
@@ -425,6 +494,11 @@ module Herringbone
|
|
|
425
494
|
end
|
|
426
495
|
alias_method :to_s, :inspect
|
|
427
496
|
|
|
497
|
+
protected
|
|
498
|
+
|
|
499
|
+
# @return [Array] the signatures of the top-level nodes (Node#signature), for #== and #hash
|
|
500
|
+
def signature = @root.children.map(&:signature)
|
|
501
|
+
|
|
428
502
|
private
|
|
429
503
|
|
|
430
504
|
# Appends a Column for every leaf under +node+, depth-first. A non-required node adds one
|
|
@@ -846,3 +920,9 @@ module Herringbone
|
|
|
846
920
|
end
|
|
847
921
|
|
|
848
922
|
require_relative "active_record"
|
|
923
|
+
|
|
924
|
+
module Herringbone
|
|
925
|
+
class Schema
|
|
926
|
+
autoload :Merge, File.expand_path("schema/merge", __dir__)
|
|
927
|
+
end
|
|
928
|
+
end
|