1// Licensed to the Apache Software Foundation (ASF) under one 2// or more contributor license agreements. See the NOTICE file 3// distributed with this work for additional information 4// regarding copyright ownership. The ASF licenses this file 5// to you under the Apache License, Version 2.0 (the 6// "License"); you may not use this file except in compliance 7// with the License. You may obtain a copy of the License at 8// 9// http://www.apache.org/licenses/LICENSE-2.0 10// 11// Unless required by applicable law or agreed to in writing, software 12// distributed under the License is distributed on an "AS IS" BASIS, 13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14// See the License for the specific language governing permissions and 15// limitations under the License. 16 17package schema 18 19import ( 20 "github.com/apache/arrow/go/v6/parquet" 21 format "github.com/apache/arrow/go/v6/parquet/internal/gen-go/parquet" 22 "github.com/apache/thrift/lib/go/thrift" 23 "golang.org/x/xerrors" 24) 25 26// NodeType describes whether the Node is a Primitive or Group node 27type NodeType int 28 29// the available constants for NodeType 30const ( 31 Primitive NodeType = iota 32 Group 33) 34 35// Node is the interface for both Group and Primitive Nodes. 36// A logical schema type has a name, repetition level, and optionally 37// a logical type (converted type is the deprecated version of the logical 38// type concept, which is maintained for forward compatibility) 39type Node interface { 40 Name() string 41 Type() NodeType 42 RepetitionType() parquet.Repetition 43 ConvertedType() ConvertedType 44 LogicalType() LogicalType 45 FieldID() int32 46 Parent() Node 47 SetParent(Node) 48 Path() string 49 Equals(Node) bool 50 Visit(v Visitor) 51 toThrift() *format.SchemaElement 52} 53 54// Visitor is an interface for creating functionality to walk the schema tree. 55// 56// A visitor can be passed to the Visit function of a Node in order to walk 57// the tree. VisitPre is called the first time a node is encountered. If 58// it is a group node, the return is checked and if it is false, the children 59// will be skipped. 60// 61// VisitPost is called after visiting any children 62type Visitor interface { 63 VisitPre(Node) bool 64 VisitPost(Node) 65} 66 67// ColumnPathFromNode walks the parents of the given node to construct it's 68// column path 69func ColumnPathFromNode(n Node) parquet.ColumnPath { 70 if n == nil { 71 return nil 72 } 73 74 c := make([]string, 0) 75 76 // build the path in reverse order as we traverse nodes to the top 77 cursor := n 78 for cursor.Parent() != nil { 79 c = append(c, cursor.Name()) 80 cursor = cursor.Parent() 81 } 82 83 // reverse the order of the list in place so that our result 84 // is in the proper, correct order. 85 for i := len(c)/2 - 1; i >= 0; i-- { 86 opp := len(c) - 1 - i 87 c[i], c[opp] = c[opp], c[i] 88 } 89 90 return c 91} 92 93// node is the base embedded struct for both group and primitive nodes 94type node struct { 95 typ NodeType 96 parent Node 97 98 name string 99 repetition parquet.Repetition 100 fieldID int32 101 logicalType LogicalType 102 convertedType ConvertedType 103 colPath parquet.ColumnPath 104} 105 106func (n *node) toThrift() *format.SchemaElement { return nil } 107func (n *node) Name() string { return n.name } 108func (n *node) Type() NodeType { return n.typ } 109func (n *node) RepetitionType() parquet.Repetition { return n.repetition } 110func (n *node) ConvertedType() ConvertedType { return n.convertedType } 111func (n *node) LogicalType() LogicalType { return n.logicalType } 112func (n *node) FieldID() int32 { return n.fieldID } 113func (n *node) Parent() Node { return n.parent } 114func (n *node) SetParent(p Node) { n.parent = p } 115func (n *node) Path() string { 116 return n.columnPath().String() 117} 118func (n *node) columnPath() parquet.ColumnPath { 119 if n.colPath == nil { 120 n.colPath = ColumnPathFromNode(n) 121 } 122 return n.colPath 123} 124 125func (n *node) Equals(rhs Node) bool { 126 return n.typ == rhs.Type() && 127 n.Name() == rhs.Name() && 128 n.RepetitionType() == rhs.RepetitionType() && 129 n.ConvertedType() == rhs.ConvertedType() && 130 n.FieldID() == rhs.FieldID() && 131 n.LogicalType().Equals(rhs.LogicalType()) 132} 133 134func (n *node) Visit(v Visitor) {} 135 136// A PrimitiveNode is a type that is one of the primitive Parquet storage types. In addition to 137// the other type metadata (name, repetition level, logical type), also has the 138// physical storage type and their type-specific metadata (byte width, decimal 139// parameters) 140type PrimitiveNode struct { 141 node 142 143 ColumnOrder parquet.ColumnOrder 144 physicalType parquet.Type 145 typeLen int 146 decimalMetaData DecimalMetadata 147} 148 149// NewPrimitiveNodeLogical constructs a Primtive node using the provided logical type for a given 150// physical type and typelength. 151func NewPrimitiveNodeLogical(name string, repetition parquet.Repetition, logicalType LogicalType, physicalType parquet.Type, typeLen int, id int32) (*PrimitiveNode, error) { 152 n := &PrimitiveNode{ 153 node: node{typ: Primitive, name: name, repetition: repetition, logicalType: logicalType, fieldID: id}, 154 physicalType: physicalType, 155 typeLen: typeLen, 156 } 157 158 if logicalType != nil { 159 if !logicalType.IsNested() { 160 if logicalType.IsApplicable(physicalType, int32(typeLen)) { 161 n.convertedType, n.decimalMetaData = n.logicalType.ToConvertedType() 162 } else { 163 return nil, xerrors.Errorf("%s cannot be applied to primitive type %s", logicalType, physicalType) 164 } 165 } else { 166 return nil, xerrors.Errorf("nested logical type %s can not be applied to a non-group node", logicalType) 167 } 168 } else { 169 n.logicalType = NoLogicalType{} 170 n.convertedType, n.decimalMetaData = n.logicalType.ToConvertedType() 171 } 172 173 if !(n.logicalType != nil && !n.logicalType.IsNested() && n.logicalType.IsCompatible(n.convertedType, n.decimalMetaData)) { 174 return nil, xerrors.Errorf("invalid logical type %s", n.logicalType) 175 } 176 177 if n.physicalType == parquet.Types.FixedLenByteArray && n.typeLen <= 0 { 178 return nil, xerrors.New("invalid fixed length byte array length") 179 } 180 return n, nil 181} 182 183// NewPrimitiveNodeConverted constructs a primitive node from the given physical type and converted type, 184// determining the logical type from the converted type. 185func NewPrimitiveNodeConverted(name string, repetition parquet.Repetition, typ parquet.Type, converted ConvertedType, typeLen, precision, scale int, id int32) (*PrimitiveNode, error) { 186 n := &PrimitiveNode{ 187 node: node{typ: Primitive, name: name, repetition: repetition, convertedType: converted, fieldID: id}, 188 physicalType: typ, 189 typeLen: -1, 190 } 191 192 switch converted { 193 case ConvertedTypes.None: 194 case ConvertedTypes.UTF8, ConvertedTypes.JSON, ConvertedTypes.BSON: 195 if typ != parquet.Types.ByteArray { 196 return nil, xerrors.Errorf("parquet: %s can only annotate BYTE_LEN fields", typ) 197 } 198 case ConvertedTypes.Decimal: 199 switch typ { 200 case parquet.Types.Int32, parquet.Types.Int64, parquet.Types.ByteArray, parquet.Types.FixedLenByteArray: 201 default: 202 return nil, xerrors.New("parquet: DECIMAL can only annotate INT32, INT64, BYTE_ARRAY and FIXED") 203 } 204 205 switch { 206 case precision <= 0: 207 return nil, xerrors.Errorf("parquet: invalid decimal precision: %d, must be between 1 and 38 inclusive", precision) 208 case scale < 0: 209 return nil, xerrors.Errorf("parquet: invalid decimal scale: %d, must be a number between 0 and precision inclusive", scale) 210 case scale > precision: 211 return nil, xerrors.Errorf("parquet: invalid decimal scale %d, cannot be greater than precision: %d", scale, precision) 212 } 213 n.decimalMetaData.IsSet = true 214 n.decimalMetaData.Precision = int32(precision) 215 n.decimalMetaData.Scale = int32(scale) 216 case ConvertedTypes.Date, 217 ConvertedTypes.TimeMillis, 218 ConvertedTypes.Int8, 219 ConvertedTypes.Int16, 220 ConvertedTypes.Int32, 221 ConvertedTypes.Uint8, 222 ConvertedTypes.Uint16, 223 ConvertedTypes.Uint32: 224 if typ != parquet.Types.Int32 { 225 return nil, xerrors.Errorf("parquet: %s can only annotate INT32", converted) 226 } 227 case ConvertedTypes.TimeMicros, 228 ConvertedTypes.TimestampMicros, 229 ConvertedTypes.TimestampMillis, 230 ConvertedTypes.Int64, 231 ConvertedTypes.Uint64: 232 if typ != parquet.Types.Int64 { 233 return nil, xerrors.Errorf("parquet: %s can only annotate INT64", converted) 234 } 235 case ConvertedTypes.Interval: 236 if typ != parquet.Types.FixedLenByteArray || typeLen != 12 { 237 return nil, xerrors.New("parquet: INTERVAL can only annotate FIXED_LEN_BYTE_ARRAY(12)") 238 } 239 case ConvertedTypes.Enum: 240 if typ != parquet.Types.ByteArray { 241 return nil, xerrors.New("parquet: ENUM can only annotate BYTE_ARRAY fields") 242 } 243 case ConvertedTypes.NA: 244 default: 245 return nil, xerrors.Errorf("parquet: %s cannot be applied to a primitive type", converted.String()) 246 } 247 248 n.logicalType = n.convertedType.ToLogicalType(n.decimalMetaData) 249 if !(n.logicalType != nil && !n.logicalType.IsNested() && n.logicalType.IsCompatible(n.convertedType, n.decimalMetaData)) { 250 return nil, xerrors.Errorf("invalid logical type %s", n.logicalType) 251 } 252 253 if n.physicalType == parquet.Types.FixedLenByteArray { 254 if typeLen <= 0 { 255 return nil, xerrors.New("invalid fixed len byte array length") 256 } 257 n.typeLen = typeLen 258 } 259 260 return n, nil 261} 262 263func PrimitiveNodeFromThrift(elem *format.SchemaElement) (*PrimitiveNode, error) { 264 fieldID := int32(-1) 265 if elem.IsSetFieldID() { 266 fieldID = elem.GetFieldID() 267 } 268 269 if elem.IsSetLogicalType() { 270 return NewPrimitiveNodeLogical(elem.GetName(), parquet.Repetition(elem.GetRepetitionType()), 271 getLogicalType(elem.GetLogicalType()), parquet.Type(elem.GetType()), int(elem.GetTypeLength()), 272 fieldID) 273 } else if elem.IsSetConvertedType() { 274 return NewPrimitiveNodeConverted(elem.GetName(), parquet.Repetition(elem.GetRepetitionType()), 275 parquet.Type(elem.GetType()), ConvertedType(elem.GetConvertedType()), 276 int(elem.GetTypeLength()), int(elem.GetPrecision()), int(elem.GetScale()), fieldID) 277 } 278 return NewPrimitiveNodeLogical(elem.GetName(), parquet.Repetition(elem.GetRepetitionType()), NoLogicalType{}, parquet.Type(elem.GetType()), int(elem.GetTypeLength()), fieldID) 279} 280 281// NewPrimitiveNode constructs a primitive node with the ConvertedType of None and no logical type. 282// 283// Use NewPrimitiveNodeLogical and NewPrimitiveNodeConverted to specify the logical or converted type. 284func NewPrimitiveNode(name string, repetition parquet.Repetition, typ parquet.Type, fieldID, typeLength int32) (*PrimitiveNode, error) { 285 return NewPrimitiveNodeLogical(name, repetition, nil, typ, int(typeLength), fieldID) 286} 287 288// Equals returns true if both nodes are primitive nodes with the same physical 289// and converted/logical types. 290func (p *PrimitiveNode) Equals(rhs Node) bool { 291 if !p.node.Equals(rhs) { 292 return false 293 } 294 295 other := rhs.(*PrimitiveNode) 296 if p == other { 297 return true 298 } 299 300 if p.PhysicalType() != other.PhysicalType() { 301 return false 302 } 303 304 equal := true 305 if p.ConvertedType() == ConvertedTypes.Decimal { 306 equal = equal && 307 (p.decimalMetaData.Precision == other.decimalMetaData.Precision && 308 p.decimalMetaData.Scale == other.decimalMetaData.Scale) 309 } 310 if p.PhysicalType() == parquet.Types.FixedLenByteArray { 311 equal = equal && p.TypeLength() == other.TypeLength() 312 } 313 return equal 314} 315 316// PhysicalType returns the proper Physical parquet.Type primitive that is used 317// to store the values in this column. 318func (p *PrimitiveNode) PhysicalType() parquet.Type { return p.physicalType } 319 320// SetTypeLength will change the type length of the node, has no effect if the 321// physical type is not FixedLength Byte Array 322func (p *PrimitiveNode) SetTypeLength(length int) { 323 if p.PhysicalType() == parquet.Types.FixedLenByteArray { 324 p.typeLen = length 325 } 326} 327 328// TypeLength will be -1 if not a FixedLenByteArray column, otherwise will be the 329// length of the FixedLen Byte Array 330func (p *PrimitiveNode) TypeLength() int { return p.typeLen } 331 332// DecimalMetadata returns the current metadata for the node. If not a decimal 333// typed column, the return should have IsSet == false. 334func (p *PrimitiveNode) DecimalMetadata() DecimalMetadata { return p.decimalMetaData } 335 336// Visit is for implementing a Visitor pattern handler to walk a schema's tree. One 337// example is the Schema Printer which walks the tree to print out the schema in order. 338func (p *PrimitiveNode) Visit(v Visitor) { 339 v.VisitPre(p) 340 v.VisitPost(p) 341} 342 343func (p *PrimitiveNode) toThrift() *format.SchemaElement { 344 elem := &format.SchemaElement{ 345 Name: p.Name(), 346 RepetitionType: format.FieldRepetitionTypePtr(format.FieldRepetitionType(p.RepetitionType())), 347 Type: format.TypePtr(format.Type(p.PhysicalType())), 348 } 349 if p.ConvertedType() != ConvertedTypes.None { 350 elem.ConvertedType = format.ConvertedTypePtr(format.ConvertedType(p.ConvertedType())) 351 } 352 if p.FieldID() >= 0 { 353 elem.FieldID = thrift.Int32Ptr(p.FieldID()) 354 } 355 if p.logicalType != nil && p.logicalType.IsSerialized() && !p.logicalType.Equals(IntervalLogicalType{}) { 356 elem.LogicalType = p.logicalType.toThrift() 357 } 358 if p.physicalType == parquet.Types.FixedLenByteArray { 359 elem.TypeLength = thrift.Int32Ptr(int32(p.typeLen)) 360 } 361 if p.decimalMetaData.IsSet { 362 elem.Precision = &p.decimalMetaData.Precision 363 elem.Scale = &p.decimalMetaData.Scale 364 } 365 return elem 366} 367 368// FieldList is an alias for a slice of Nodes 369type FieldList []Node 370 371// Len is equivalent to len(fieldlist) 372func (f FieldList) Len() int { return len(f) } 373 374// GroupNode is for mananging nested nodes like List, Map, etc. 375type GroupNode struct { 376 node 377 fields FieldList 378 nameToIdx strIntMultimap 379} 380 381// NewGroupNodeConverted constructs a group node with the provided fields and converted type, 382// determining the logical type from that converted type. 383func NewGroupNodeConverted(name string, repetition parquet.Repetition, fields FieldList, converted ConvertedType, id int32) (n *GroupNode, err error) { 384 n = &GroupNode{ 385 node: node{typ: Group, name: name, repetition: repetition, convertedType: converted, fieldID: id}, 386 fields: fields, 387 } 388 n.logicalType = n.convertedType.ToLogicalType(DecimalMetadata{}) 389 if !(n.logicalType != nil && (n.logicalType.IsNested() || n.logicalType.IsNone()) && n.logicalType.IsCompatible(n.convertedType, DecimalMetadata{})) { 390 err = xerrors.Errorf("invalid logical type %s", n.logicalType.String()) 391 return 392 } 393 394 n.nameToIdx = make(strIntMultimap) 395 for idx, f := range n.fields { 396 f.SetParent(n) 397 n.nameToIdx.Add(f.Name(), idx) 398 } 399 return 400} 401 402// NewGroupNodeLogical constructs a group node with the provided fields and logical type, 403// determining the converted type from the provided logical type. 404func NewGroupNodeLogical(name string, repetition parquet.Repetition, fields FieldList, logical LogicalType, id int32) (n *GroupNode, err error) { 405 n = &GroupNode{ 406 node: node{typ: Group, name: name, repetition: repetition, logicalType: logical, fieldID: id}, 407 fields: fields, 408 } 409 410 if logical != nil { 411 if logical.IsNested() { 412 n.convertedType, _ = logical.ToConvertedType() 413 } else { 414 err = xerrors.Errorf("logical type %s cannot be applied to group node", logical) 415 return 416 } 417 } else { 418 n.logicalType = NoLogicalType{} 419 n.convertedType, _ = n.logicalType.ToConvertedType() 420 } 421 422 if !(n.logicalType != nil && (n.logicalType.IsNested() || n.logicalType.IsNone()) && n.logicalType.IsCompatible(n.convertedType, DecimalMetadata{})) { 423 err = xerrors.Errorf("invalid logical type %s", n.logicalType) 424 return 425 } 426 427 n.nameToIdx = make(strIntMultimap) 428 for idx, f := range n.fields { 429 f.SetParent(n) 430 n.nameToIdx.Add(f.Name(), idx) 431 } 432 return 433} 434 435// NewGroupNode constructs a new group node with the provided fields, 436// but with converted type None and No Logical Type 437func NewGroupNode(name string, repetition parquet.Repetition, fields FieldList, fieldID int32) (*GroupNode, error) { 438 return NewGroupNodeConverted(name, repetition, fields, ConvertedTypes.None, fieldID) 439} 440 441// Must is a convenience function for the NewNode functions that return a Node 442// and an error, panic'ing if err != nil or returning the node 443func Must(n Node, err error) Node { 444 if err != nil { 445 panic(err) 446 } 447 return n 448} 449 450// MustGroup is like Must, except it casts the node to a *GroupNode, which will panic 451// if it is a primitive node. 452func MustGroup(n Node, err error) *GroupNode { 453 if err != nil { 454 panic(err) 455 } 456 return n.(*GroupNode) 457} 458 459// MustPrimitive is like Must except it casts the node to *PrimitiveNode which will panic 460// if it is a group node. 461func MustPrimitive(n Node, err error) *PrimitiveNode { 462 if err != nil { 463 panic(err) 464 } 465 return n.(*PrimitiveNode) 466} 467 468func GroupNodeFromThrift(elem *format.SchemaElement, fields FieldList) (*GroupNode, error) { 469 id := int32(-1) 470 if elem.IsSetFieldID() { 471 id = elem.GetFieldID() 472 } 473 474 if elem.IsSetLogicalType() { 475 return NewGroupNodeLogical(elem.GetName(), parquet.Repetition(elem.GetRepetitionType()), fields, getLogicalType(elem.GetLogicalType()), id) 476 } 477 478 converted := ConvertedTypes.None 479 if elem.IsSetConvertedType() { 480 converted = ConvertedType(elem.GetConvertedType()) 481 } 482 return NewGroupNodeConverted(elem.GetName(), parquet.Repetition(elem.GetRepetitionType()), fields, converted, id) 483} 484 485func (g *GroupNode) toThrift() *format.SchemaElement { 486 elem := &format.SchemaElement{ 487 Name: g.name, 488 NumChildren: thrift.Int32Ptr(int32(len(g.fields))), 489 RepetitionType: format.FieldRepetitionTypePtr(format.FieldRepetitionType(g.RepetitionType())), 490 } 491 if g.convertedType != ConvertedTypes.None { 492 elem.ConvertedType = format.ConvertedTypePtr(format.ConvertedType(g.convertedType)) 493 } 494 if g.fieldID >= 0 { 495 elem.FieldID = &g.fieldID 496 } 497 if g.logicalType != nil && g.logicalType.IsSerialized() { 498 elem.LogicalType = g.logicalType.toThrift() 499 } 500 return elem 501} 502 503// Equals will compare this node to the provided node and only return true if 504// this node and all of it's children are the same as the passed in node and its 505// children. 506func (g *GroupNode) Equals(rhs Node) bool { 507 if !g.node.Equals(rhs) { 508 return false 509 } 510 511 other := rhs.(*GroupNode) 512 if g == other { 513 return true 514 } 515 if len(g.fields) != len(other.fields) { 516 return false 517 } 518 519 for idx, field := range g.fields { 520 if !field.Equals(other.fields[idx]) { 521 return false 522 } 523 } 524 return true 525} 526 527// NumFields returns the number of direct child fields for this group node 528func (g *GroupNode) NumFields() int { 529 return len(g.fields) 530} 531 532// Field returns the node in the field list which is of the provided (0-based) index 533func (g *GroupNode) Field(i int) Node { 534 return g.fields[i] 535} 536 537// FieldIndexByName provides the index for the field of the given name. Returns 538// -1 if not found. 539// 540// If there are more than one field of this name, it returns the index for the first one. 541func (g *GroupNode) FieldIndexByName(name string) int { 542 if idx, ok := g.nameToIdx[name]; ok { 543 return idx[0] 544 } 545 return -1 546} 547 548// FieldIndexByField looks up the index child of this node. Returns -1 549// if n isn't a child of this group 550func (g *GroupNode) FieldIndexByField(n Node) int { 551 if search, ok := g.nameToIdx[n.Name()]; ok { 552 for _, idx := range search { 553 if n == g.fields[idx] { 554 return idx 555 } 556 } 557 } 558 return -1 559} 560 561// Visit is for implementing a Visitor pattern handler to walk a schema's tree. One 562// example is the Schema Printer which walks the tree to print out the schema in order. 563func (g *GroupNode) Visit(v Visitor) { 564 if v.VisitPre(g) { 565 for _, field := range g.fields { 566 field.Visit(v) 567 } 568 } 569 v.VisitPost(g) 570} 571 572// HasRepeatedFields returns true if any of the children of this node have 573// Repeated as its repetition type. 574// 575// This is recursive and will check the children of any group nodes that are children. 576func (g *GroupNode) HasRepeatedFields() bool { 577 for _, field := range g.fields { 578 if field.RepetitionType() == parquet.Repetitions.Repeated { 579 return true 580 } 581 if field.Type() == Group { 582 return field.(*GroupNode).HasRepeatedFields() 583 } 584 } 585 return false 586} 587 588// NewInt32Node is a convenience factory for constructing an Int32 Primitive Node 589func NewInt32Node(name string, rep parquet.Repetition, fieldID int32) *PrimitiveNode { 590 return MustPrimitive(NewPrimitiveNode(name, rep, parquet.Types.Int32, fieldID, -1)) 591} 592 593// NewInt64Node is a convenience factory for constructing an Int64 Primitive Node 594func NewInt64Node(name string, rep parquet.Repetition, fieldID int32) *PrimitiveNode { 595 return MustPrimitive(NewPrimitiveNode(name, rep, parquet.Types.Int64, fieldID, -1)) 596} 597 598// NewInt96Node is a convenience factory for constructing an Int96 Primitive Node 599func NewInt96Node(name string, rep parquet.Repetition, fieldID int32) *PrimitiveNode { 600 return MustPrimitive(NewPrimitiveNode(name, rep, parquet.Types.Int96, fieldID, -1)) 601} 602 603// NewFloat32Node is a convenience factory for constructing an Float Primitive Node 604func NewFloat32Node(name string, rep parquet.Repetition, fieldID int32) *PrimitiveNode { 605 return MustPrimitive(NewPrimitiveNode(name, rep, parquet.Types.Float, fieldID, -1)) 606} 607 608// NewFloat64Node is a convenience factory for constructing an Double Primitive Node 609func NewFloat64Node(name string, rep parquet.Repetition, fieldID int32) *PrimitiveNode { 610 return MustPrimitive(NewPrimitiveNode(name, rep, parquet.Types.Double, fieldID, -1)) 611} 612 613// NewBooleanNode is a convenience factory for constructing an Boolean Primitive Node 614func NewBooleanNode(name string, rep parquet.Repetition, fieldID int32) *PrimitiveNode { 615 return MustPrimitive(NewPrimitiveNode(name, rep, parquet.Types.Boolean, fieldID, -1)) 616} 617 618// NewByteArrayNode is a convenience factory for constructing an Byte Array Primitive Node 619func NewByteArrayNode(name string, rep parquet.Repetition, fieldID int32) *PrimitiveNode { 620 return MustPrimitive(NewPrimitiveNode(name, rep, parquet.Types.ByteArray, fieldID, -1)) 621} 622 623// NewFixedLenByteArrayNode is a convenience factory for constructing an Fixed Length 624// Byte Array Primitive Node of the given length 625func NewFixedLenByteArrayNode(name string, rep parquet.Repetition, length int32, fieldID int32) *PrimitiveNode { 626 return MustPrimitive(NewPrimitiveNode(name, rep, parquet.Types.FixedLenByteArray, fieldID, length)) 627} 628