From 5ab60eaea3afaf1ff58e9f70bed481d6e726dd69 Mon Sep 17 00:00:00 2001 From: loicalleyne Date: Mon, 27 Nov 2023 11:02:44 -0500 Subject: [PATCH] GH-36760: [Go] Add Avro OCF reader (#37115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Rationale for this change ### What changes are included in this PR? Avro reader ### Are these changes tested? Local integration tests, no unit tests yet. ### Are there any user-facing changes? New Avro reader API * Closes: #36760 Lead-authored-by: Loïc Alleyne Co-authored-by: Sutou Kouhei Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Raúl Cumplido Co-authored-by: sgilmore10 <74676073+sgilmore10@users.noreply.github.com> Co-authored-by: Dewey Dunnington Co-authored-by: Alenka Frim Co-authored-by: mwish Co-authored-by: James Duong Co-authored-by: Dewey Dunnington Co-authored-by: Antoine Pitrou Co-authored-by: Felipe Oliveira Carvalho Co-authored-by: Dane Pitkin <48041712+danepitkin@users.noreply.github.com> Co-authored-by: Kevin Gurney Co-authored-by: Matt Topol Co-authored-by: Curt Hagenlocher Co-authored-by: Jacob Wujciak-Jens Co-authored-by: Hyunseok Seo Co-authored-by: Joris Van den Bossche Co-authored-by: James Duong Co-authored-by: Nic Crane Co-authored-by: Ivan Chesnov Co-authored-by: Diego Fernández Giraldo Co-authored-by: Bryce Mecum Co-authored-by: Jonathan Keane Co-authored-by: Peter Andreas Entschev Co-authored-by: abandy Co-authored-by: Lei Hou Co-authored-by: Yue Co-authored-by: davidhcoe <13318837+davidhcoe@users.noreply.github.com> Co-authored-by: Tsutomu Katsube Co-authored-by: Divyansh200102 <146909065+Divyansh200102@users.noreply.github.com> Co-authored-by: Thomas Newton Co-authored-by: Jeremy Aguilon Co-authored-by: prmoore77 Co-authored-by: Jiaxing Liang <58449896+sfc-gh-jliang@users.noreply.github.com> Co-authored-by: orgadish <48453207+orgadish@users.noreply.github.com> Co-authored-by: Maximilian Muecke Co-authored-by: Gavin Murrison <2135106+voidstar69@users.noreply.github.com> Co-authored-by: William Ayd Co-authored-by: Laurent Goujon Co-authored-by: Jin Shang Co-authored-by: Alexander Grueneberg Co-authored-by: Paul Spangler <7519484+spanglerco@users.noreply.github.com> Co-authored-by: Gang Wu Co-authored-by: Michael Lui Co-authored-by: patrick <100629128+p-a-a-a-trick@users.noreply.github.com> Co-authored-by: Dan Homola Co-authored-by: Ben Harkins <60872452+benibus@users.noreply.github.com> Co-authored-by: Nick Hughes Co-authored-by: Fernando Mayer Co-authored-by: Rok Mihevc Co-authored-by: Francis <455954986@qq.com> Co-authored-by: Donald Tolley Co-authored-by: Judah Rand <17158624+judahrand@users.noreply.github.com> Co-authored-by: Eero Lihavainen Co-authored-by: Benjamin Schmidt Co-authored-by: Phillip LeBlanc Co-authored-by: Pierre Moulon Co-authored-by: Benjamin Kietzman Co-authored-by: Fokko Driesprong Co-authored-by: Bryan Cutler Co-authored-by: Diogo Teles Sant'Anna Co-authored-by: Fatemah Panahi Co-authored-by: Junming Chen Co-authored-by: Chris Larsen Co-authored-by: Dan Stone Co-authored-by: Tim Schaub Co-authored-by: Will Jones Co-authored-by: Kevin Gurney Co-authored-by: Sarah Gilmore Co-authored-by: jeremy Co-authored-by: Dan Homola Co-authored-by: Sutou Kouhei Co-authored-by: anjakefala Co-authored-by: Antoine Pitrou Co-authored-by: Jeremy Aguilon Co-authored-by: Weston Pace Co-authored-by: scoder Co-authored-by: voidstar69 Co-authored-by: Dewey Dunnington Co-authored-by: Ivan Chesnov Co-authored-by: mwish <1506118561@qq.com> Co-authored-by: David Li Signed-off-by: Matt Topol --- go/arrow/avro/avro2parquet/main.go | 119 +++ go/arrow/avro/loader.go | 85 ++ go/arrow/avro/reader.go | 337 +++++++ go/arrow/avro/reader_test.go | 364 ++++++++ go/arrow/avro/reader_types.go | 875 ++++++++++++++++++ go/arrow/avro/schema.go | 429 +++++++++ go/arrow/avro/schema_test.go | 362 ++++++++ go/arrow/avro/testdata/arrayrecordmap.avro | Bin 0 -> 582 bytes .../avro/testdata/githubsamplecommits.avro | Bin 0 -> 95131 bytes go/go.mod | 9 + go/go.sum | 21 + 11 files changed, 2601 insertions(+) create mode 100644 go/arrow/avro/avro2parquet/main.go create mode 100644 go/arrow/avro/loader.go create mode 100644 go/arrow/avro/reader.go create mode 100644 go/arrow/avro/reader_test.go create mode 100644 go/arrow/avro/reader_types.go create mode 100644 go/arrow/avro/schema.go create mode 100644 go/arrow/avro/schema_test.go create mode 100644 go/arrow/avro/testdata/arrayrecordmap.avro create mode 100644 go/arrow/avro/testdata/githubsamplecommits.avro diff --git a/go/arrow/avro/avro2parquet/main.go b/go/arrow/avro/avro2parquet/main.go new file mode 100644 index 0000000000000..45377b46a444c --- /dev/null +++ b/go/arrow/avro/avro2parquet/main.go @@ -0,0 +1,119 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "log" + "os" + "runtime/pprof" + "time" + + "github.com/apache/arrow/go/v15/arrow/avro" + "github.com/apache/arrow/go/v15/parquet" + "github.com/apache/arrow/go/v15/parquet/compress" + pq "github.com/apache/arrow/go/v15/parquet/pqarrow" +) + +var ( + cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") + filepath = flag.String("file", "", "avro ocf to convert") +) + +func main() { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal("could not create CPU profile: ", err) + } + defer f.Close() // error handling omitted for example + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal("could not start CPU profile: ", err) + } + defer pprof.StopCPUProfile() + } + if *filepath == "" { + fmt.Println("no file specified") + } + chunk := 1024 * 8 + ts := time.Now() + log.Println("starting:") + info, err := os.Stat(*filepath) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + filesize := info.Size() + data, err := os.ReadFile(*filepath) + if err != nil { + fmt.Println(err) + os.Exit(2) + } + fmt.Printf("file : %v\nsize: %v MB\n", filepath, float64(filesize)/1024/1024) + + r := bytes.NewReader(data) + ior := bufio.NewReaderSize(r, 4096*8) + av2arReader, err := avro.NewOCFReader(ior, avro.WithChunk(chunk)) + if err != nil { + fmt.Println(err) + os.Exit(3) + } + fp, err := os.OpenFile(*filepath+".parquet", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + fmt.Println(err) + os.Exit(4) + } + defer fp.Close() + pwProperties := parquet.NewWriterProperties(parquet.WithDictionaryDefault(true), + parquet.WithVersion(parquet.V2_LATEST), + parquet.WithCompression(compress.Codecs.Snappy), + parquet.WithBatchSize(1024*32), + parquet.WithDataPageSize(1024*1024), + parquet.WithMaxRowGroupLength(64*1024*1024), + ) + awProperties := pq.NewArrowWriterProperties(pq.WithStoreSchema()) + pr, err := pq.NewFileWriter(av2arReader.Schema(), fp, pwProperties, awProperties) + if err != nil { + fmt.Println(err) + os.Exit(5) + } + defer pr.Close() + fmt.Printf("parquet version: %v\n", pwProperties.Version()) + for av2arReader.Next() { + if av2arReader.Err() != nil { + fmt.Println(err) + os.Exit(6) + } + recs := av2arReader.Record() + err = pr.WriteBuffered(recs) + if err != nil { + fmt.Println(err) + os.Exit(7) + } + recs.Release() + } + if av2arReader.Err() != nil { + fmt.Println(av2arReader.Err()) + } + + pr.Close() + log.Printf("time to convert: %v\n", time.Since(ts)) +} diff --git a/go/arrow/avro/loader.go b/go/arrow/avro/loader.go new file mode 100644 index 0000000000000..26d8678e8e2be --- /dev/null +++ b/go/arrow/avro/loader.go @@ -0,0 +1,85 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package avro + +import ( + "errors" + "fmt" + "io" +) + +func (r *OCFReader) decodeOCFToChan() { + defer close(r.avroChan) + for r.r.HasNext() { + select { + case <-r.readerCtx.Done(): + r.err = fmt.Errorf("avro decoding cancelled, %d records read", r.avroDatumCount) + return + default: + var datum any + err := r.r.Decode(&datum) + if err != nil { + if errors.Is(err, io.EOF) { + r.err = nil + return + } + r.err = err + return + } + r.avroChan <- datum + r.avroDatumCount++ + } + } +} + +func (r *OCFReader) recordFactory() { + defer close(r.recChan) + r.primed = true + recChunk := 0 + switch { + case r.chunk < 1: + for data := range r.avroChan { + err := r.ldr.loadDatum(data) + if err != nil { + r.err = err + return + } + } + r.recChan <- r.bld.NewRecord() + r.bldDone <- struct{}{} + case r.chunk >= 1: + for data := range r.avroChan { + if recChunk == 0 { + r.bld.Reserve(r.chunk) + } + err := r.ldr.loadDatum(data) + if err != nil { + r.err = err + return + } + recChunk++ + if recChunk >= r.chunk { + r.recChan <- r.bld.NewRecord() + recChunk = 0 + } + } + if recChunk != 0 { + r.recChan <- r.bld.NewRecord() + } + r.bldDone <- struct{}{} + } +} diff --git a/go/arrow/avro/reader.go b/go/arrow/avro/reader.go new file mode 100644 index 0000000000000..e72a5632bdd6e --- /dev/null +++ b/go/arrow/avro/reader.go @@ -0,0 +1,337 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package avro + +import ( + "context" + "errors" + "fmt" + "io" + "sync/atomic" + + "github.com/apache/arrow/go/v15/arrow" + "github.com/apache/arrow/go/v15/arrow/array" + "github.com/apache/arrow/go/v15/arrow/internal/debug" + "github.com/apache/arrow/go/v15/arrow/memory" + "github.com/hamba/avro/v2/ocf" + "github.com/tidwall/sjson" + + avro "github.com/hamba/avro/v2" +) + +var ErrMismatchFields = errors.New("arrow/avro: number of records mismatch") + +// Option configures an Avro reader/writer. +type ( + Option func(config) + config *OCFReader +) + +type schemaEdit struct { + method string + path string + value any +} + +// Reader wraps goavro/OCFReader and creates array.Records from a schema. +type OCFReader struct { + r *ocf.Decoder + avroSchema string + avroSchemaEdits []schemaEdit + schema *arrow.Schema + + refs int64 + bld *array.RecordBuilder + bldMap *fieldPos + ldr *dataLoader + cur arrow.Record + err error + + primed bool + readerCtx context.Context + readCancel func() + maxOCF int + maxRec int + + avroChan chan any + avroDatumCount int64 + avroChanSize int + recChan chan arrow.Record + + bldDone chan struct{} + + recChanSize int + chunk int + mem memory.Allocator +} + +// NewReader returns a reader that reads from an Avro OCF file and creates +// arrow.Records from the converted avro data. +func NewOCFReader(r io.Reader, opts ...Option) (*OCFReader, error) { + ocfr, err := ocf.NewDecoder(r) + if err != nil { + return nil, fmt.Errorf("%w: could not create avro ocfreader", arrow.ErrInvalid) + } + + rr := &OCFReader{ + r: ocfr, + refs: 1, + chunk: 1, + avroChanSize: 500, + recChanSize: 10, + } + for _, opt := range opts { + opt(rr) + } + + rr.avroChan = make(chan any, rr.avroChanSize) + rr.recChan = make(chan arrow.Record, rr.recChanSize) + rr.bldDone = make(chan struct{}) + schema, err := avro.Parse(string(ocfr.Metadata()["avro.schema"])) + if err != nil { + return nil, fmt.Errorf("%w: could not parse avro header", arrow.ErrInvalid) + } + rr.avroSchema = schema.String() + if len(rr.avroSchemaEdits) > 0 { + // execute schema edits + for _, e := range rr.avroSchemaEdits { + err := rr.editAvroSchema(e) + if err != nil { + return nil, fmt.Errorf("%w: could not edit avro schema", arrow.ErrInvalid) + } + } + // validate edited schema + schema, err = avro.Parse(rr.avroSchema) + if err != nil { + return nil, fmt.Errorf("%w: could not parse modified avro schema", arrow.ErrInvalid) + } + } + rr.schema, err = ArrowSchemaFromAvro(schema) + if err != nil { + return nil, fmt.Errorf("%w: could not convert avro schema", arrow.ErrInvalid) + } + if rr.mem == nil { + rr.mem = memory.DefaultAllocator + } + rr.readerCtx, rr.readCancel = context.WithCancel(context.Background()) + go rr.decodeOCFToChan() + + rr.bld = array.NewRecordBuilder(rr.mem, rr.schema) + rr.bldMap = newFieldPos() + rr.ldr = newDataLoader() + for idx, fb := range rr.bld.Fields() { + mapFieldBuilders(fb, rr.schema.Field(idx), rr.bldMap) + } + rr.ldr.drawTree(rr.bldMap) + go rr.recordFactory() + return rr, nil +} + +// Reuse allows the OCFReader to be reused to read another Avro file provided the +// new Avro file has an identical schema. +func (rr *OCFReader) Reuse(r io.Reader, opts ...Option) error { + rr.Close() + rr.err = nil + ocfr, err := ocf.NewDecoder(r) + if err != nil { + return fmt.Errorf("%w: could not create avro ocfreader", arrow.ErrInvalid) + } + schema, err := avro.Parse(string(ocfr.Metadata()["avro.schema"])) + if err != nil { + return fmt.Errorf("%w: could not parse avro header", arrow.ErrInvalid) + } + if rr.avroSchema != schema.String() { + return fmt.Errorf("%w: avro schema mismatch", arrow.ErrInvalid) + } + + rr.r = ocfr + for _, opt := range opts { + opt(rr) + } + + rr.maxOCF = 0 + rr.maxRec = 0 + rr.avroDatumCount = 0 + rr.primed = false + + rr.avroChan = make(chan any, rr.avroChanSize) + rr.recChan = make(chan arrow.Record, rr.recChanSize) + rr.bldDone = make(chan struct{}) + + rr.readerCtx, rr.readCancel = context.WithCancel(context.Background()) + go rr.decodeOCFToChan() + go rr.recordFactory() + return nil +} + +// Err returns the last error encountered during the iteration over the +// underlying Avro file. +func (r *OCFReader) Err() error { return r.err } + +// AvroSchema returns the Avro schema of the Avro OCF +func (r *OCFReader) AvroSchema() string { return r.avroSchema } + +// Schema returns the converted Arrow schema of the Avro OCF +func (r *OCFReader) Schema() *arrow.Schema { return r.schema } + +// Record returns the current record that has been extracted from the +// underlying Avro OCF file. +// It is valid until the next call to Next. +func (r *OCFReader) Record() arrow.Record { return r.cur } + +// Metrics returns the maximum queue depth of the Avro record read cache and of the +// converted Arrow record cache. +func (r *OCFReader) Metrics() string { + return fmt.Sprintf("Max. OCF queue depth: %d/%d Max. record queue depth: %d/%d", r.maxOCF, r.avroChanSize, r.maxRec, r.recChanSize) +} + +// OCFRecordsReadCount returns the number of Avro datum that were read from the Avro file. +func (r *OCFReader) OCFRecordsReadCount() int64 { return r.avroDatumCount } + +// Close closes the OCFReader's Avro record read cache and converted Arrow record cache. OCFReader must +// be closed if the Avro OCF's records have not been read to completion. +func (r *OCFReader) Close() { + r.readCancel() + r.err = r.readerCtx.Err() +} + +func (r *OCFReader) editAvroSchema(e schemaEdit) error { + var err error + switch e.method { + case "set": + r.avroSchema, err = sjson.Set(r.avroSchema, e.path, e.value) + if err != nil { + return fmt.Errorf("%w: schema edit 'set %s = %v' failure - %v", arrow.ErrInvalid, e.path, e.value, err) + } + case "delete": + r.avroSchema, err = sjson.Delete(r.avroSchema, e.path) + if err != nil { + return fmt.Errorf("%w: schema edit 'delete' failure - %v", arrow.ErrInvalid, err) + } + default: + return fmt.Errorf("%w: schema edit method must be 'set' or 'delete'", arrow.ErrInvalid) + } + return nil +} + +// Next returns whether a Record can be received from the converted record queue. +// The user should check Err() after call to Next that return false to check +// if an error took place. +func (r *OCFReader) Next() bool { + if r.cur != nil { + r.cur.Release() + r.cur = nil + } + if r.maxOCF < len(r.avroChan) { + r.maxOCF = len(r.avroChan) + } + if r.maxRec < len(r.recChan) { + r.maxRec = len(r.recChan) + } + select { + case r.cur = <-r.recChan: + case <-r.bldDone: + if len(r.recChan) > 0 { + r.cur = <-r.recChan + } + } + if r.err != nil { + return false + } + + return r.cur != nil +} + +// WithAllocator specifies the Arrow memory allocator used while building records. +func WithAllocator(mem memory.Allocator) Option { + return func(cfg config) { + cfg.mem = mem + } +} + +// WithReadCacheSize specifies the size of the OCF record decode queue, default value +// is 500. +func WithReadCacheSize(n int) Option { + return func(cfg config) { + if n < 1 { + cfg.avroChanSize = 500 + } else { + cfg.avroChanSize = n + } + } +} + +// WithRecordCacheSize specifies the size of the converted Arrow record queue, default +// value is 1. +func WithRecordCacheSize(n int) Option { + return func(cfg config) { + if n < 1 { + cfg.recChanSize = 1 + } else { + cfg.recChanSize = n + } + } +} + +// WithSchemaEdit specifies modifications to the Avro schema. Supported methods are 'set' and +// 'delete'. Set sets the value for the specified path. Delete deletes the value for the specified path. +// A path is in dot syntax, such as "fields.1" or "fields.0.type". The modified Avro schema is +// validated before conversion to Arrow schema - NewOCFReader will return an error if the modified schema +// cannot be parsed. +func WithSchemaEdit(method, path string, value any) Option { + return func(cfg config) { + var e schemaEdit + e.method = method + e.path = path + e.value = value + cfg.avroSchemaEdits = append(cfg.avroSchemaEdits, e) + } +} + +// WithChunk specifies the chunk size used while reading Avro OCF files. +// +// If n is zero or 1, no chunking will take place and the reader will create +// one record per row. +// If n is greater than 1, chunks of n rows will be read. +// If n is negative, the reader will load the whole Avro OCF file into memory and +// create one big record with all the rows. +func WithChunk(n int) Option { + return func(cfg config) { + cfg.chunk = n + } +} + +// Retain increases the reference count by 1. +// Retain may be called simultaneously from multiple goroutines. +func (r *OCFReader) Retain() { + atomic.AddInt64(&r.refs, 1) +} + +// Release decreases the reference count by 1. +// When the reference count goes to zero, the memory is freed. +// Release may be called simultaneously from multiple goroutines. +func (r *OCFReader) Release() { + debug.Assert(atomic.LoadInt64(&r.refs) > 0, "too many releases") + + if atomic.AddInt64(&r.refs, -1) == 0 { + if r.cur != nil { + r.cur.Release() + } + } +} + +var _ array.RecordReader = (*OCFReader)(nil) diff --git a/go/arrow/avro/reader_test.go b/go/arrow/avro/reader_test.go new file mode 100644 index 0000000000000..e94d4f48fb933 --- /dev/null +++ b/go/arrow/avro/reader_test.go @@ -0,0 +1,364 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package avro + +import ( + "fmt" + "testing" + + "github.com/apache/arrow/go/v15/arrow" + hamba "github.com/hamba/avro/v2" +) + +func TestEditSchemaStringEqual(t *testing.T) { + tests := []struct { + avroSchema string + arrowSchema []arrow.Field + }{ + { + avroSchema: `{ + "fields": [ + { + "name": "inheritNull", + "type": { + "name": "Simple", + "symbols": [ + "a", + "b" + ], + "type": "enum" + } + }, + { + "name": "explicitNamespace", + "type": { + "name": "test", + "namespace": "org.hamba.avro", + "size": 12, + "type": "fixed" + } + }, + { + "name": "fullName", + "type": { + "type": "record", + "name": "fullName_data", + "namespace": "ignored", + "doc": "A name attribute with a fullname, so the namespace attribute is ignored. The fullname is 'a.full.Name', and the namespace is 'a.full'.", + "fields": [{ + "name": "inheritNamespace", + "type": { + "type": "enum", + "name": "Understanding", + "doc": "A simple name (attribute) and no namespace attribute: inherit the namespace of the enclosing type 'a.full.Name'. The fullname is 'a.full.Understanding'.", + "symbols": ["d", "e"] + } + }, { + "name": "md5", + "type": { + "name": "md5_data", + "type": "fixed", + "size": 16, + "namespace": "ignored" + } + } + ] + } + }, + { + "name": "id", + "type": "int" + }, + { + "name": "bigId", + "type": "long" + }, + { + "name": "temperature", + "type": [ + "null", + "float" + ] + }, + { + "name": "fraction", + "type": [ + "null", + "double" + ] + }, + { + "name": "is_emergency", + "type": "boolean" + }, + { + "name": "remote_ip", + "type": [ + "null", + "bytes" + ] + }, + { + "name": "person", + "type": { + "fields": [ + { + "name": "lastname", + "type": "string" + }, + { + "name": "address", + "type": { + "fields": [ + { + "name": "streetaddress", + "type": "string" + }, + { + "name": "city", + "type": "string" + } + ], + "name": "AddressUSRecord", + "type": "record" + } + }, + { + "name": "mapfield", + "type": { + "default": { + }, + "type": "map", + "values": "long" + } + }, + { + "name": "arrayField", + "type": { + "default": [ + ], + "items": "string", + "type": "array" + } + } + ], + "name": "person_data", + "type": "record" + } + }, + { + "name": "decimalField", + "type": { + "logicalType": "decimal", + "precision": 4, + "scale": 2, + "type": "bytes" + } + }, + { + "logicalType": "uuid", + "name": "uuidField", + "type": "string" + }, + { + "name": "timemillis", + "type": { + "type": "int", + "logicalType": "time-millis" + } + }, + { + "name": "timemicros", + "type": { + "type": "long", + "logicalType": "time-micros" + } + }, + { + "name": "timestampmillis", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + }, + { + "name": "timestampmicros", + "type": { + "type": "long", + "logicalType": "timestamp-micros" + } + }, + { + "name": "duration", + "type": { + "name": "duration", + "namespace": "whyowhy", + "logicalType": "duration", + "size": 12, + "type": "fixed" + } + }, + { + "name": "date", + "type": { + "logicalType": "date", + "type": "int" + } + } + ], + "name": "Example", + "type": "record" + }`, + arrowSchema: []arrow.Field{ + { + Name: "explicitNamespace", + Type: &arrow.FixedSizeBinaryType{ByteWidth: 12}, + }, + { + Name: "fullName", + Type: arrow.StructOf( + arrow.Field{ + Name: "inheritNamespace", + Type: &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint8, ValueType: arrow.BinaryTypes.String, Ordered: false}, + }, + arrow.Field{ + Name: "md5", + Type: &arrow.FixedSizeBinaryType{ByteWidth: 16}, + }, + ), + }, + { + Name: "id", + Type: arrow.PrimitiveTypes.Int32, + }, + { + Name: "bigId", + Type: arrow.PrimitiveTypes.Int64, + }, + { + Name: "temperature", + Type: arrow.PrimitiveTypes.Float32, + Nullable: true, + }, + { + Name: "fraction", + Type: arrow.PrimitiveTypes.Float64, + Nullable: true, + }, + { + Name: "is_emergency", + Type: arrow.FixedWidthTypes.Boolean, + }, + { + Name: "remote_ip", + Type: arrow.BinaryTypes.Binary, + Nullable: true, + }, + { + Name: "person", + Type: arrow.StructOf( + arrow.Field{ + Name: "lastname", + Type: arrow.BinaryTypes.String, + }, + arrow.Field{ + Name: "address", + Type: arrow.StructOf( + arrow.Field{ + Name: "streetaddress", + Type: arrow.BinaryTypes.String, + }, + arrow.Field{ + Name: "city", + Type: arrow.BinaryTypes.String, + }, + ), + }, + arrow.Field{ + Name: "mapfield", + Type: arrow.MapOf(arrow.BinaryTypes.String, arrow.PrimitiveTypes.Int64), + Nullable: true, + }, + arrow.Field{ + Name: "arrayField", + Type: arrow.ListOfNonNullable(arrow.BinaryTypes.String), + }, + ), + }, + { + Name: "decimalField", + Type: &arrow.Decimal128Type{Precision: 4, Scale: 2}, + }, + { + Name: "uuidField", + Type: arrow.BinaryTypes.String, + }, + { + Name: "timemillis", + Type: arrow.FixedWidthTypes.Time32ms, + }, + { + Name: "timemicros", + Type: arrow.FixedWidthTypes.Time64us, + }, + { + Name: "timestampmillis", + Type: arrow.FixedWidthTypes.Timestamp_ms, + }, + { + Name: "timestampmicros", + Type: arrow.FixedWidthTypes.Timestamp_us, + }, + { + Name: "duration", + Type: arrow.FixedWidthTypes.MonthDayNanoInterval, + }, + { + Name: "date", + Type: arrow.FixedWidthTypes.Date32, + }, + }, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + want := arrow.NewSchema(test.arrowSchema, nil) + + schema, err := hamba.ParseBytes([]byte(test.avroSchema)) + if err != nil { + t.Fatalf("%v", err) + } + r := new(OCFReader) + r.avroSchema = schema.String() + r.editAvroSchema(schemaEdit{method: "delete", path: "fields.0"}) + schema, err = hamba.Parse(r.avroSchema) + if err != nil { + t.Fatalf("%v: could not parse modified avro schema", arrow.ErrInvalid) + } + got, err := ArrowSchemaFromAvro(schema) + if err != nil { + t.Fatalf("%v", err) + } + if !(fmt.Sprintf("%+v", want.String()) == fmt.Sprintf("%+v", got.String())) { + t.Fatalf("got=%v,\n want=%v", got.String(), want.String()) + } else { + t.Logf("schema.String() comparison passed") + } + }) + } +} diff --git a/go/arrow/avro/reader_types.go b/go/arrow/avro/reader_types.go new file mode 100644 index 0000000000000..5658c6e587db2 --- /dev/null +++ b/go/arrow/avro/reader_types.go @@ -0,0 +1,875 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package avro + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math/big" + + "github.com/apache/arrow/go/v15/arrow" + "github.com/apache/arrow/go/v15/arrow/array" + "github.com/apache/arrow/go/v15/arrow/decimal128" + "github.com/apache/arrow/go/v15/arrow/decimal256" + "github.com/apache/arrow/go/v15/arrow/memory" + "github.com/apache/arrow/go/v15/internal/types" +) + +type dataLoader struct { + idx, depth int32 + list *fieldPos + item *fieldPos + mapField *fieldPos + mapKey *fieldPos + mapValue *fieldPos + fields []*fieldPos + children []*dataLoader +} + +var ( + ErrNullStructData = errors.New("null struct data") +) + +func newDataLoader() *dataLoader { return &dataLoader{idx: 0, depth: 0} } + +// drawTree takes the tree of field builders produced by mapFieldBuilders() +// and produces another tree structure and aggregates fields whose values can +// be retrieved from a `map[string]any` into a slice of builders, and creates a hierarchy to +// deal with nested types (lists and maps). +func (d *dataLoader) drawTree(field *fieldPos) { + for _, f := range field.children() { + if f.isList || f.isMap { + if f.isList { + c := d.newListChild(f) + if !f.childrens[0].isList { + c.item = f.childrens[0] + c.drawTree(f.childrens[0]) + } else { + c.drawTree(f.childrens[0].childrens[0]) + } + } + if f.isMap { + c := d.newMapChild(f) + if !arrow.IsNested(f.childrens[1].builder.Type().ID()) { + c.mapKey = f.childrens[0] + c.mapValue = f.childrens[1] + } else { + c.mapKey = f.childrens[0] + m := c.newChild() + m.mapValue = f.childrens[1] + m.drawTree(f.childrens[1]) + } + } + } else { + d.fields = append(d.fields, f) + if len(f.children()) > 0 { + d.drawTree(f) + } + } + } +} + +// loadDatum loads decoded Avro data to the schema fields' builder functions. +// Since array.StructBuilder.AppendNull() will recursively append null to all of the +// struct's fields, in the case of nil being passed to a struct's builderFunc it will +// return a ErrNullStructData error to signal that all its sub-fields can be skipped. +func (d *dataLoader) loadDatum(data any) error { + if d.list == nil && d.mapField == nil { + if d.mapValue != nil { + d.mapValue.appendFunc(data) + } + var NullParent *fieldPos + for _, f := range d.fields { + if f.parent == NullParent { + continue + } + if d.mapValue == nil { + err := f.appendFunc(f.getValue(data)) + if err != nil { + if err == ErrNullStructData { + NullParent = f + continue + } + return err + } + } else { + switch dt := data.(type) { + case nil: + err := f.appendFunc(dt) + if err != nil { + if err == ErrNullStructData { + NullParent = f + continue + } + return err + } + case []any: + if len(d.children) < 1 { + for _, e := range dt { + err := f.appendFunc(e) + if err != nil { + if err == ErrNullStructData { + NullParent = f + continue + } + return err + } + } + } else { + for _, e := range dt { + d.children[0].loadDatum(e) + } + } + case map[string]any: + err := f.appendFunc(f.getValue(dt)) + if err != nil { + if err == ErrNullStructData { + NullParent = f + continue + } + return err + } + } + + } + } + for _, c := range d.children { + if c.list != nil { + c.loadDatum(c.list.getValue(data)) + } + if c.mapField != nil { + switch dt := data.(type) { + case nil: + c.loadDatum(dt) + case map[string]any: + c.loadDatum(c.mapField.getValue(dt)) + default: + c.loadDatum(c.mapField.getValue(data)) + } + } + } + } else { + if d.list != nil { + switch dt := data.(type) { + case nil: + d.list.appendFunc(dt) + case []any: + d.list.appendFunc(dt) + for _, e := range dt { + if d.item != nil { + d.item.appendFunc(e) + } + var NullParent *fieldPos + for _, f := range d.fields { + if f.parent == NullParent { + continue + } + err := f.appendFunc(f.getValue(e)) + if err != nil { + if err == ErrNullStructData { + NullParent = f + continue + } + return err + } + } + for _, c := range d.children { + if c.list != nil { + c.loadDatum(c.list.getValue(e)) + } + if c.mapField != nil { + c.loadDatum(c.mapField.getValue(e)) + } + } + } + case map[string]any: + d.list.appendFunc(dt["array"]) + for _, e := range dt["array"].([]any) { + if d.item != nil { + d.item.appendFunc(e) + } + var NullParent *fieldPos + for _, f := range d.fields { + if f.parent == NullParent { + continue + } + err := f.appendFunc(f.getValue(e)) + if err != nil { + if err == ErrNullStructData { + NullParent = f + continue + } + return err + } + } + for _, c := range d.children { + c.loadDatum(c.list.getValue(e)) + } + } + default: + d.list.appendFunc(data) + d.item.appendFunc(dt) + } + } + if d.mapField != nil { + switch dt := data.(type) { + case nil: + d.mapField.appendFunc(dt) + case map[string]any: + + d.mapField.appendFunc(dt) + for k, v := range dt { + d.mapKey.appendFunc(k) + if d.mapValue != nil { + d.mapValue.appendFunc(v) + } else { + d.children[0].loadDatum(v) + } + } + } + } + } + return nil +} + +func (d *dataLoader) newChild() *dataLoader { + var child *dataLoader = &dataLoader{ + depth: d.depth + 1, + } + d.children = append(d.children, child) + return child +} + +func (d *dataLoader) newListChild(list *fieldPos) *dataLoader { + var child *dataLoader = &dataLoader{ + list: list, + item: list.childrens[0], + depth: d.depth + 1, + } + d.children = append(d.children, child) + return child +} + +func (d *dataLoader) newMapChild(mapField *fieldPos) *dataLoader { + var child *dataLoader = &dataLoader{ + mapField: mapField, + depth: d.depth + 1, + } + d.children = append(d.children, child) + return child +} + +type fieldPos struct { + parent *fieldPos + fieldName string + builder array.Builder + path []string + isList bool + isItem bool + isStruct bool + isMap bool + typeName string + appendFunc func(val interface{}) error + metadatas arrow.Metadata + childrens []*fieldPos + index, depth int32 +} + +func newFieldPos() *fieldPos { return &fieldPos{index: -1} } + +func (f *fieldPos) children() []*fieldPos { return f.childrens } + +func (f *fieldPos) newChild(childName string, childBuilder array.Builder, meta arrow.Metadata) *fieldPos { + var child fieldPos = fieldPos{ + parent: f, + fieldName: childName, + builder: childBuilder, + metadatas: meta, + index: int32(len(f.childrens)), + depth: f.depth + 1, + } + if f.isList { + child.isItem = true + } + child.path = child.buildNamePath() + f.childrens = append(f.childrens, &child) + return &child +} + +func (f *fieldPos) buildNamePath() []string { + var path []string + var listPath []string + cur := f + for i := f.depth - 1; i >= 0; i-- { + if cur.typeName == "" { + path = append([]string{cur.fieldName}, path...) + } else { + path = append([]string{cur.fieldName, cur.typeName}, path...) + } + if !cur.parent.isMap { + cur = cur.parent + } + } + if f.parent.parent != nil && f.parent.parent.isList { + for i := len(path) - 1; i >= 0; i-- { + if path[i] != "item" { + listPath = append([]string{path[i]}, listPath...) + } else { + return listPath + } + } + } + if f.parent != nil && f.parent.fieldName == "value" { + for i := len(path) - 1; i >= 0; i-- { + if path[i] != "value" { + listPath = append([]string{path[i]}, listPath...) + } else { + return listPath + } + } + } + return path +} + +// NamePath returns a slice of keys making up the path to the field +func (f *fieldPos) namePath() []string { return f.path } + +// GetValue retrieves the value from the map[string]any +// by following the field's key path +func (f *fieldPos) getValue(m any) any { + if _, ok := m.(map[string]any); !ok { + return m + } + for _, key := range f.namePath() { + valueMap, ok := m.(map[string]any) + if !ok { + if key == "item" { + return m + } + return nil + } + m, ok = valueMap[key] + if !ok { + return nil + } + } + return m +} + +// Avro data is loaded to Arrow arrays using the following type mapping: +// +// Avro Go Arrow +// null nil Null +// boolean bool Boolean +// bytes []byte Binary +// float float32 Float32 +// double float64 Float64 +// long int64 Int64 +// int int32 Int32 +// string string String +// array []interface{} List +// enum string Dictionary +// fixed []byte FixedSizeBinary +// map and record map[string]any Struct +// +// mapFieldBuilders builds a tree of field builders matching the Arrow schema +func mapFieldBuilders(b array.Builder, field arrow.Field, parent *fieldPos) { + f := parent.newChild(field.Name, b, field.Metadata) + switch bt := b.(type) { + case *array.BinaryBuilder: + f.appendFunc = func(data interface{}) error { + appendBinaryData(bt, data) + return nil + } + case *array.BinaryDictionaryBuilder: + // has metadata for Avro enum symbols + f.appendFunc = func(data interface{}) error { + appendBinaryDictData(bt, data) + return nil + } + // add Avro enum symbols to builder + sb := array.NewStringBuilder(memory.DefaultAllocator) + for _, v := range field.Metadata.Values() { + sb.Append(v) + } + sa := sb.NewStringArray() + bt.InsertStringDictValues(sa) + case *array.BooleanBuilder: + f.appendFunc = func(data interface{}) error { + appendBoolData(bt, data) + return nil + } + case *array.Date32Builder: + f.appendFunc = func(data interface{}) error { + appendDate32Data(bt, data) + return nil + } + case *array.Decimal128Builder: + f.appendFunc = func(data interface{}) error { + err := appendDecimal128Data(bt, data) + if err != nil { + return err + } + return nil + } + case *array.Decimal256Builder: + f.appendFunc = func(data interface{}) error { + err := appendDecimal256Data(bt, data) + if err != nil { + return err + } + return nil + } + case *types.UUIDBuilder: + f.appendFunc = func(data interface{}) error { + switch dt := data.(type) { + case nil: + bt.AppendNull() + case string: + err := bt.AppendValueFromString(dt) + if err != nil { + return err + } + case []byte: + err := bt.AppendValueFromString(string(dt)) + if err != nil { + return err + } + } + return nil + } + case *array.FixedSizeBinaryBuilder: + f.appendFunc = func(data interface{}) error { + appendFixedSizeBinaryData(bt, data) + return nil + } + case *array.Float32Builder: + f.appendFunc = func(data interface{}) error { + appendFloat32Data(bt, data) + return nil + } + case *array.Float64Builder: + f.appendFunc = func(data interface{}) error { + appendFloat64Data(bt, data) + return nil + } + case *array.Int32Builder: + f.appendFunc = func(data interface{}) error { + appendInt32Data(bt, data) + return nil + } + case *array.Int64Builder: + f.appendFunc = func(data interface{}) error { + appendInt64Data(bt, data) + return nil + } + case *array.LargeListBuilder: + vb := bt.ValueBuilder() + f.isList = true + mapFieldBuilders(vb, field.Type.(*arrow.LargeListType).ElemField(), f) + f.appendFunc = func(data interface{}) error { + switch dt := data.(type) { + case nil: + bt.AppendNull() + case []interface{}: + if len(dt) == 0 { + bt.AppendEmptyValue() + } else { + bt.Append(true) + } + default: + bt.Append(true) + } + return nil + } + case *array.ListBuilder: + vb := bt.ValueBuilder() + f.isList = true + mapFieldBuilders(vb, field.Type.(*arrow.ListType).ElemField(), f) + f.appendFunc = func(data interface{}) error { + switch dt := data.(type) { + case nil: + bt.AppendNull() + case []interface{}: + if len(dt) == 0 { + bt.AppendEmptyValue() + } else { + bt.Append(true) + } + default: + bt.Append(true) + } + return nil + } + case *array.MapBuilder: + // has metadata for objects in values + f.isMap = true + kb := bt.KeyBuilder() + ib := bt.ItemBuilder() + mapFieldBuilders(kb, field.Type.(*arrow.MapType).KeyField(), f) + mapFieldBuilders(ib, field.Type.(*arrow.MapType).ItemField(), f) + f.appendFunc = func(data interface{}) error { + switch data.(type) { + case nil: + bt.AppendNull() + default: + bt.Append(true) + } + return nil + } + case *array.MonthDayNanoIntervalBuilder: + f.appendFunc = func(data interface{}) error { + appendDurationData(bt, data) + return nil + } + case *array.StringBuilder: + f.appendFunc = func(data interface{}) error { + appendStringData(bt, data) + return nil + } + case *array.StructBuilder: + // has metadata for Avro Union named types + f.typeName, _ = field.Metadata.GetValue("typeName") + f.isStruct = true + // create children + for i, p := range field.Type.(*arrow.StructType).Fields() { + mapFieldBuilders(bt.FieldBuilder(i), p, f) + } + f.appendFunc = func(data interface{}) error { + switch data.(type) { + case nil: + bt.AppendNull() + return ErrNullStructData + default: + bt.Append(true) + } + return nil + } + case *array.Time32Builder: + f.appendFunc = func(data interface{}) error { + appendTime32Data(bt, data) + return nil + } + case *array.Time64Builder: + f.appendFunc = func(data interface{}) error { + appendTime64Data(bt, data) + return nil + } + case *array.TimestampBuilder: + f.appendFunc = func(data interface{}) error { + appendTimestampData(bt, data) + return nil + } + } +} + +func appendBinaryData(b *array.BinaryBuilder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case map[string]any: + switch ct := dt["bytes"].(type) { + case nil: + b.AppendNull() + default: + b.Append(ct.([]byte)) + } + default: + b.Append(fmt.Append([]byte{}, data)) + } +} + +func appendBinaryDictData(b *array.BinaryDictionaryBuilder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case string: + b.AppendString(dt) + case map[string]any: + switch v := dt["string"].(type) { + case nil: + b.AppendNull() + case string: + b.AppendString(v) + } + } +} + +func appendBoolData(b *array.BooleanBuilder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case bool: + b.Append(dt) + case map[string]any: + switch v := dt["boolean"].(type) { + case nil: + b.AppendNull() + case bool: + b.Append(v) + } + } +} + +func appendDate32Data(b *array.Date32Builder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case int32: + b.Append(arrow.Date32(dt)) + case map[string]any: + switch v := dt["int"].(type) { + case nil: + b.AppendNull() + case int32: + b.Append(arrow.Date32(v)) + } + } +} + +func appendDecimal128Data(b *array.Decimal128Builder, data interface{}) error { + switch dt := data.(type) { + case nil: + b.AppendNull() + case []byte: + buf := bytes.NewBuffer(dt) + if len(dt) <= 38 { + var intData int64 + err := binary.Read(buf, binary.BigEndian, &intData) + if err != nil { + return err + } + b.Append(decimal128.FromI64(intData)) + } else { + var bigIntData big.Int + b.Append(decimal128.FromBigInt(bigIntData.SetBytes(buf.Bytes()))) + } + case map[string]any: + buf := bytes.NewBuffer(dt["bytes"].([]byte)) + if len(dt["bytes"].([]byte)) <= 38 { + var intData int64 + err := binary.Read(buf, binary.BigEndian, &intData) + if err != nil { + return err + } + b.Append(decimal128.FromI64(intData)) + } else { + var bigIntData big.Int + b.Append(decimal128.FromBigInt(bigIntData.SetBytes(buf.Bytes()))) + } + } + return nil +} + +func appendDecimal256Data(b *array.Decimal256Builder, data interface{}) error { + switch dt := data.(type) { + case nil: + b.AppendNull() + case []byte: + var bigIntData big.Int + buf := bytes.NewBuffer(dt) + b.Append(decimal256.FromBigInt(bigIntData.SetBytes(buf.Bytes()))) + case map[string]any: + var bigIntData big.Int + buf := bytes.NewBuffer(dt["bytes"].([]byte)) + b.Append(decimal256.FromBigInt(bigIntData.SetBytes(buf.Bytes()))) + } + return nil +} + +// Avro duration logical type annotates Avro fixed type of size 12, which stores three little-endian +// unsigned integers that represent durations at different granularities of time. The first stores +// a number in months, the second stores a number in days, and the third stores a number in milliseconds. +func appendDurationData(b *array.MonthDayNanoIntervalBuilder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case []byte: + dur := new(arrow.MonthDayNanoInterval) + dur.Months = int32(binary.LittleEndian.Uint16(dt[:3])) + dur.Days = int32(binary.LittleEndian.Uint16(dt[4:7])) + dur.Nanoseconds = int64(binary.LittleEndian.Uint32(dt[8:]) * 1000000) + b.Append(*dur) + case map[string]any: + switch dtb := dt["bytes"].(type) { + case nil: + b.AppendNull() + case []byte: + dur := new(arrow.MonthDayNanoInterval) + dur.Months = int32(binary.LittleEndian.Uint16(dtb[:3])) + dur.Days = int32(binary.LittleEndian.Uint16(dtb[4:7])) + dur.Nanoseconds = int64(binary.LittleEndian.Uint32(dtb[8:]) * 1000000) + b.Append(*dur) + } + } +} + +func appendFixedSizeBinaryData(b *array.FixedSizeBinaryBuilder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case []byte: + b.Append(dt) + case map[string]any: + switch v := dt["bytes"].(type) { + case nil: + b.AppendNull() + case []byte: + b.Append(v) + } + } +} + +func appendFloat32Data(b *array.Float32Builder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case float32: + b.Append(dt) + case map[string]any: + switch v := dt["float"].(type) { + case nil: + b.AppendNull() + case float32: + b.Append(v) + } + } +} + +func appendFloat64Data(b *array.Float64Builder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case float64: + b.Append(dt) + case map[string]any: + switch v := dt["double"].(type) { + case nil: + b.AppendNull() + case float64: + b.Append(v) + } + } +} + +func appendInt32Data(b *array.Int32Builder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case int: + b.Append(int32(dt)) + case int32: + b.Append(dt) + case map[string]any: + switch v := dt["int"].(type) { + case nil: + b.AppendNull() + case int: + b.Append(int32(v)) + case int32: + b.Append(v) + } + } +} + +func appendInt64Data(b *array.Int64Builder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case int: + b.Append(int64(dt)) + case int64: + b.Append(dt) + case map[string]any: + switch v := dt["long"].(type) { + case nil: + b.AppendNull() + case int: + b.Append(int64(v)) + case int64: + b.Append(v) + } + } +} + +func appendStringData(b *array.StringBuilder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case string: + b.Append(dt) + case map[string]any: + switch v := dt["string"].(type) { + case nil: + b.AppendNull() + case string: + b.Append(v) + } + default: + b.Append(fmt.Sprint(data)) + } +} + +func appendTime32Data(b *array.Time32Builder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case int32: + b.Append(arrow.Time32(dt)) + case map[string]any: + switch v := dt["int"].(type) { + case nil: + b.AppendNull() + case int32: + b.Append(arrow.Time32(v)) + } + } +} + +func appendTime64Data(b *array.Time64Builder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case int64: + b.Append(arrow.Time64(dt)) + case map[string]any: + switch v := dt["long"].(type) { + case nil: + b.AppendNull() + case int64: + b.Append(arrow.Time64(v)) + } + } +} + +func appendTimestampData(b *array.TimestampBuilder, data interface{}) { + switch dt := data.(type) { + case nil: + b.AppendNull() + case int64: + b.Append(arrow.Timestamp(dt)) + case map[string]any: + switch v := dt["long"].(type) { + case nil: + b.AppendNull() + case int64: + b.Append(arrow.Timestamp(v)) + } + } +} diff --git a/go/arrow/avro/schema.go b/go/arrow/avro/schema.go new file mode 100644 index 0000000000000..32e37096c68f2 --- /dev/null +++ b/go/arrow/avro/schema.go @@ -0,0 +1,429 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package avro reads Avro OCF files and presents the extracted data as records +package avro + +import ( + "fmt" + "math" + "strconv" + + "github.com/apache/arrow/go/v15/arrow" + "github.com/apache/arrow/go/v15/arrow/decimal128" + "github.com/apache/arrow/go/v15/internal/types" + avro "github.com/hamba/avro/v2" +) + +type schemaNode struct { + name string + parent *schemaNode + schema avro.Schema + union bool + nullable bool + childrens []*schemaNode + arrowField arrow.Field + schemaCache *avro.SchemaCache + index, depth int32 +} + +func newSchemaNode() *schemaNode { + var schemaCache avro.SchemaCache + return &schemaNode{name: "", index: -1, schemaCache: &schemaCache} +} + +func (node *schemaNode) schemaPath() string { + var path string + n := node + for n.parent != nil { + path = "." + n.name + path + n = n.parent + } + return path +} + +func (node *schemaNode) newChild(n string, s avro.Schema) *schemaNode { + child := &schemaNode{ + name: n, + parent: node, + schema: s, + schemaCache: node.schemaCache, + index: int32(len(node.childrens)), + depth: node.depth + 1, + } + node.childrens = append(node.childrens, child) + return child +} +func (node *schemaNode) children() []*schemaNode { return node.childrens } + +// func (node *schemaNode) nodeName() string { return node.name } + +// ArrowSchemaFromAvro returns a new Arrow schema from an Avro schema +func ArrowSchemaFromAvro(schema avro.Schema) (s *arrow.Schema, err error) { + defer func() { + if r := recover(); r != nil { + s = nil + switch x := r.(type) { + case string: + err = fmt.Errorf("invalid avro schema: %s", x) + case error: + err = fmt.Errorf("invalid avro schema: %w", x) + default: + err = fmt.Errorf("invalid avro schema: unknown error") + } + } + }() + n := newSchemaNode() + n.schema = schema + c := n.newChild(n.schema.(avro.NamedSchema).Name(), n.schema) + arrowSchemafromAvro(c) + var fields []arrow.Field + for _, g := range c.children() { + fields = append(fields, g.arrowField) + } + s = arrow.NewSchema(fields, nil) + return s, nil +} + +func arrowSchemafromAvro(n *schemaNode) { + if ns, ok := n.schema.(avro.NamedSchema); ok { + n.schemaCache.Add(ns.Name(), ns) + } + switch st := n.schema.Type(); st { + case "record": + iterateFields(n) + case "enum": + n.schemaCache.Add(n.schema.(avro.NamedSchema).Name(), n.schema.(*avro.EnumSchema)) + symbols := make(map[string]string) + for index, symbol := range n.schema.(avro.PropertySchema).(*avro.EnumSchema).Symbols() { + k := strconv.FormatInt(int64(index), 10) + symbols[k] = symbol + } + var dt arrow.DictionaryType = arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint64, ValueType: arrow.BinaryTypes.String, Ordered: false} + sl := int64(len(symbols)) + switch { + case sl <= math.MaxUint8: + dt.IndexType = arrow.PrimitiveTypes.Uint8 + case sl > math.MaxUint8 && sl <= math.MaxUint16: + dt.IndexType = arrow.PrimitiveTypes.Uint16 + case sl > math.MaxUint16 && sl <= math.MaxUint32: + dt.IndexType = arrow.PrimitiveTypes.Uint32 + } + n.arrowField = buildArrowField(n, &dt, arrow.MetadataFrom(symbols)) + case "array": + // logical items type + c := n.newChild(n.name, n.schema.(*avro.ArraySchema).Items()) + if isLogicalSchemaType(n.schema.(*avro.ArraySchema).Items()) { + avroLogicalToArrowField(c) + } else { + arrowSchemafromAvro(c) + } + switch c.arrowField.Nullable { + case true: + n.arrowField = arrow.Field{Name: n.name, Type: arrow.ListOfField(c.arrowField), Metadata: c.arrowField.Metadata} + case false: + n.arrowField = arrow.Field{Name: n.name, Type: arrow.ListOfNonNullable(c.arrowField.Type), Metadata: c.arrowField.Metadata} + } + case "map": + n.schemaCache.Add(n.schema.(*avro.MapSchema).Values().(avro.NamedSchema).Name(), n.schema.(*avro.MapSchema).Values()) + c := n.newChild(n.name, n.schema.(*avro.MapSchema).Values()) + arrowSchemafromAvro(c) + n.arrowField = buildArrowField(n, arrow.MapOf(arrow.BinaryTypes.String, c.arrowField.Type), c.arrowField.Metadata) + case "union": + if n.schema.(*avro.UnionSchema).Nullable() { + if len(n.schema.(*avro.UnionSchema).Types()) > 1 { + n.schema = n.schema.(*avro.UnionSchema).Types()[1] + n.union = true + n.nullable = true + arrowSchemafromAvro(n) + } + } + // Avro "fixed" field type = Arrow FixedSize Primitive BinaryType + case "fixed": + n.schemaCache.Add(n.schema.(avro.NamedSchema).Name(), n.schema.(*avro.FixedSchema)) + if isLogicalSchemaType(n.schema) { + avroLogicalToArrowField(n) + } else { + n.arrowField = buildArrowField(n, &arrow.FixedSizeBinaryType{ByteWidth: n.schema.(*avro.FixedSchema).Size()}, arrow.Metadata{}) + } + case "string", "bytes", "int", "long": + if isLogicalSchemaType(n.schema) { + avroLogicalToArrowField(n) + } else { + n.arrowField = buildArrowField(n, avroPrimitiveToArrowType(string(st)), arrow.Metadata{}) + } + case "float", "double", "boolean": + n.arrowField = arrow.Field{Name: n.name, Type: avroPrimitiveToArrowType(string(st)), Nullable: n.nullable} + case "": + refSchema := n.schemaCache.Get(string(n.schema.(*avro.RefSchema).Schema().Name())) + if refSchema == nil { + panic(fmt.Errorf("could not find schema for '%v' in schema cache - %v", n.schemaPath(), n.schema.(*avro.RefSchema).Schema().Name())) + } + n.schema = refSchema + arrowSchemafromAvro(n) + case "null": + n.schemaCache.Add(n.schema.(*avro.MapSchema).Values().(avro.NamedSchema).Name(), &avro.NullSchema{}) + n.nullable = true + n.arrowField = buildArrowField(n, arrow.Null, arrow.Metadata{}) + } +} + +// iterate record Fields() +func iterateFields(n *schemaNode) { + for _, f := range n.schema.(*avro.RecordSchema).Fields() { + switch ft := f.Type().(type) { + // Avro "array" field type + case *avro.ArraySchema: + n.schemaCache.Add(f.Name(), ft.Items()) + // logical items type + c := n.newChild(f.Name(), ft.Items()) + if isLogicalSchemaType(ft.Items()) { + avroLogicalToArrowField(c) + } else { + arrowSchemafromAvro(c) + } + switch c.arrowField.Nullable { + case true: + c.arrowField = arrow.Field{Name: c.name, Type: arrow.ListOfField(c.arrowField), Metadata: c.arrowField.Metadata} + case false: + c.arrowField = arrow.Field{Name: c.name, Type: arrow.ListOfNonNullable(c.arrowField.Type), Metadata: c.arrowField.Metadata} + } + // Avro "enum" field type = Arrow dictionary type + case *avro.EnumSchema: + n.schemaCache.Add(f.Type().(*avro.EnumSchema).Name(), f.Type()) + c := n.newChild(f.Name(), f.Type()) + symbols := make(map[string]string) + for index, symbol := range ft.Symbols() { + k := strconv.FormatInt(int64(index), 10) + symbols[k] = symbol + } + var dt arrow.DictionaryType = arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint64, ValueType: arrow.BinaryTypes.String, Ordered: false} + sl := len(symbols) + switch { + case sl <= math.MaxUint8: + dt.IndexType = arrow.PrimitiveTypes.Uint8 + case sl > math.MaxUint8 && sl <= math.MaxUint16: + dt.IndexType = arrow.PrimitiveTypes.Uint16 + case sl > math.MaxUint16 && sl <= math.MaxInt: + dt.IndexType = arrow.PrimitiveTypes.Uint32 + } + c.arrowField = buildArrowField(c, &dt, arrow.MetadataFrom(symbols)) + // Avro "fixed" field type = Arrow FixedSize Primitive BinaryType + case *avro.FixedSchema: + n.schemaCache.Add(f.Name(), f.Type()) + c := n.newChild(f.Name(), f.Type()) + if isLogicalSchemaType(f.Type()) { + avroLogicalToArrowField(c) + } else { + arrowSchemafromAvro(c) + } + case *avro.RecordSchema: + n.schemaCache.Add(f.Name(), f.Type()) + c := n.newChild(f.Name(), f.Type()) + iterateFields(c) + // Avro "map" field type - KVP with value of one type - keys are strings + case *avro.MapSchema: + n.schemaCache.Add(f.Name(), ft.Values()) + c := n.newChild(f.Name(), ft.Values()) + arrowSchemafromAvro(c) + c.arrowField = buildArrowField(c, arrow.MapOf(arrow.BinaryTypes.String, c.arrowField.Type), c.arrowField.Metadata) + case *avro.UnionSchema: + if ft.Nullable() { + if len(ft.Types()) > 1 { + n.schemaCache.Add(f.Name(), ft.Types()[1]) + c := n.newChild(f.Name(), ft.Types()[1]) + c.union = true + c.nullable = true + arrowSchemafromAvro(c) + } + } + default: + n.schemaCache.Add(f.Name(), f.Type()) + if isLogicalSchemaType(f.Type()) { + c := n.newChild(f.Name(), f.Type()) + avroLogicalToArrowField(c) + } else { + c := n.newChild(f.Name(), f.Type()) + arrowSchemafromAvro(c) + } + + } + } + var fields []arrow.Field + for _, child := range n.children() { + fields = append(fields, child.arrowField) + } + + namedSchema, ok := isNamedSchema(n.schema) + + var md arrow.Metadata + if ok && namedSchema != n.name+"_data" && n.union { + md = arrow.NewMetadata([]string{"typeName"}, []string{namedSchema}) + } + n.arrowField = buildArrowField(n, arrow.StructOf(fields...), md) +} + +func isLogicalSchemaType(s avro.Schema) bool { + lts, ok := s.(avro.LogicalTypeSchema) + if !ok { + return false + } + if lts.Logical() != nil { + return true + } + return false +} + +func isNamedSchema(s avro.Schema) (string, bool) { + if ns, ok := s.(avro.NamedSchema); ok { + return ns.FullName(), ok + } + return "", false +} + +func buildArrowField(n *schemaNode, t arrow.DataType, m arrow.Metadata) arrow.Field { + return arrow.Field{ + Name: n.name, + Type: t, + Metadata: m, + Nullable: n.nullable, + } +} + +// Avro primitive type. +// +// NOTE: Arrow Binary type is used as a catchall to avoid potential data loss. +func avroPrimitiveToArrowType(avroFieldType string) arrow.DataType { + switch avroFieldType { + // int: 32-bit signed integer + case "int": + return arrow.PrimitiveTypes.Int32 + // long: 64-bit signed integer + case "long": + return arrow.PrimitiveTypes.Int64 + // float: single precision (32-bit) IEEE 754 floating-point number + case "float": + return arrow.PrimitiveTypes.Float32 + // double: double precision (64-bit) IEEE 754 floating-point number + case "double": + return arrow.PrimitiveTypes.Float64 + // bytes: sequence of 8-bit unsigned bytes + case "bytes": + return arrow.BinaryTypes.Binary + // boolean: a binary value + case "boolean": + return arrow.FixedWidthTypes.Boolean + // string: unicode character sequence + case "string": + return arrow.BinaryTypes.String + } + return nil +} + +func avroLogicalToArrowField(n *schemaNode) { + var dt arrow.DataType + // Avro logical types + switch lt := n.schema.(avro.LogicalTypeSchema).Logical(); lt.Type() { + // The decimal logical type represents an arbitrary-precision signed decimal number of the form unscaled × 10-scale. + // A decimal logical type annotates Avro bytes or fixed types. The byte array must contain the two’s-complement + // representation of the unscaled integer value in big-endian byte order. The scale is fixed, and is specified + // using an attribute. + // + // The following attributes are supported: + // scale, a JSON integer representing the scale (optional). If not specified the scale is 0. + // precision, a JSON integer representing the (maximum) precision of decimals stored in this type (required). + case "decimal": + id := arrow.DECIMAL128 + if lt.(*avro.DecimalLogicalSchema).Precision() > decimal128.MaxPrecision { + id = arrow.DECIMAL256 + } + dt, _ = arrow.NewDecimalType(id, int32(lt.(*avro.DecimalLogicalSchema).Precision()), int32(lt.(*avro.DecimalLogicalSchema).Scale())) + + // The uuid logical type represents a random generated universally unique identifier (UUID). + // A uuid logical type annotates an Avro string. The string has to conform with RFC-4122 + case "uuid": + dt = types.NewUUIDType() + + // The date logical type represents a date within the calendar, with no reference to a particular + // time zone or time of day. + // A date logical type annotates an Avro int, where the int stores the number of days from the unix epoch, + // 1 January 1970 (ISO calendar). + case "date": + dt = arrow.FixedWidthTypes.Date32 + + // The time-millis logical type represents a time of day, with no reference to a particular calendar, + // time zone or date, with a precision of one millisecond. + // A time-millis logical type annotates an Avro int, where the int stores the number of milliseconds + // after midnight, 00:00:00.000. + case "time-millis": + dt = arrow.FixedWidthTypes.Time32ms + + // The time-micros logical type represents a time of day, with no reference to a particular calendar, + // time zone or date, with a precision of one microsecond. + // A time-micros logical type annotates an Avro long, where the long stores the number of microseconds + // after midnight, 00:00:00.000000. + case "time-micros": + dt = arrow.FixedWidthTypes.Time64us + + // The timestamp-millis logical type represents an instant on the global timeline, independent of a + // particular time zone or calendar, with a precision of one millisecond. Please note that time zone + // information gets lost in this process. Upon reading a value back, we can only reconstruct the instant, + // but not the original representation. In practice, such timestamps are typically displayed to users in + // their local time zones, therefore they may be displayed differently depending on the execution environment. + // A timestamp-millis logical type annotates an Avro long, where the long stores the number of milliseconds + // from the unix epoch, 1 January 1970 00:00:00.000 UTC. + case "timestamp-millis": + dt = arrow.FixedWidthTypes.Timestamp_ms + + // The timestamp-micros logical type represents an instant on the global timeline, independent of a + // particular time zone or calendar, with a precision of one microsecond. Please note that time zone + // information gets lost in this process. Upon reading a value back, we can only reconstruct the instant, + // but not the original representation. In practice, such timestamps are typically displayed to users + // in their local time zones, therefore they may be displayed differently depending on the execution environment. + // A timestamp-micros logical type annotates an Avro long, where the long stores the number of microseconds + // from the unix epoch, 1 January 1970 00:00:00.000000 UTC. + case "timestamp-micros": + dt = arrow.FixedWidthTypes.Timestamp_us + + // The local-timestamp-millis logical type represents a timestamp in a local timezone, regardless of + // what specific time zone is considered local, with a precision of one millisecond. + // A local-timestamp-millis logical type annotates an Avro long, where the long stores the number of + // milliseconds, from 1 January 1970 00:00:00.000. + // Note: not implemented in hamba/avro + // case "local-timestamp-millis": + // dt = &arrow.TimestampType{Unit: arrow.Millisecond} + + // The local-timestamp-micros logical type represents a timestamp in a local timezone, regardless of + // what specific time zone is considered local, with a precision of one microsecond. + // A local-timestamp-micros logical type annotates an Avro long, where the long stores the number of + // microseconds, from 1 January 1970 00:00:00.000000. + // case "local-timestamp-micros": + // Note: not implemented in hamba/avro + // dt = &arrow.TimestampType{Unit: arrow.Microsecond} + + // The duration logical type represents an amount of time defined by a number of months, days and milliseconds. + // This is not equivalent to a number of milliseconds, because, depending on the moment in time from which the + // duration is measured, the number of days in the month and number of milliseconds in a day may differ. Other + // standard periods such as years, quarters, hours and minutes can be expressed through these basic periods. + + // A duration logical type annotates Avro fixed type of size 12, which stores three little-endian unsigned integers + // that represent durations at different granularities of time. The first stores a number in months, the second + // stores a number in days, and the third stores a number in milliseconds. + case "duration": + dt = arrow.FixedWidthTypes.MonthDayNanoInterval + } + n.arrowField = buildArrowField(n, dt, arrow.Metadata{}) +} diff --git a/go/arrow/avro/schema_test.go b/go/arrow/avro/schema_test.go new file mode 100644 index 0000000000000..08a3fe1ed7440 --- /dev/null +++ b/go/arrow/avro/schema_test.go @@ -0,0 +1,362 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package avro + +import ( + "fmt" + "testing" + + "github.com/apache/arrow/go/v15/arrow" + hamba "github.com/hamba/avro/v2" +) + +func TestSchemaStringEqual(t *testing.T) { + tests := []struct { + avroSchema string + arrowSchema []arrow.Field + }{ + { + avroSchema: `{ + "fields": [ + { + "name": "inheritNull", + "type": { + "name": "Simple", + "symbols": [ + "a", + "b" + ], + "type": "enum" + } + }, + { + "name": "explicitNamespace", + "type": { + "name": "test", + "namespace": "org.hamba.avro", + "size": 12, + "type": "fixed" + } + }, + { + "name": "fullName", + "type": { + "type": "record", + "name": "fullName_data", + "namespace": "ignored", + "doc": "A name attribute with a fullname, so the namespace attribute is ignored. The fullname is 'a.full.Name', and the namespace is 'a.full'.", + "fields": [{ + "name": "inheritNamespace", + "type": { + "type": "enum", + "name": "Understanding", + "doc": "A simple name (attribute) and no namespace attribute: inherit the namespace of the enclosing type 'a.full.Name'. The fullname is 'a.full.Understanding'.", + "symbols": ["d", "e"] + } + }, { + "name": "md5", + "type": { + "name": "md5_data", + "type": "fixed", + "size": 16, + "namespace": "ignored" + } + } + ] + } + }, + { + "name": "id", + "type": "int" + }, + { + "name": "bigId", + "type": "long" + }, + { + "name": "temperature", + "type": [ + "null", + "float" + ] + }, + { + "name": "fraction", + "type": [ + "null", + "double" + ] + }, + { + "name": "is_emergency", + "type": "boolean" + }, + { + "name": "remote_ip", + "type": [ + "null", + "bytes" + ] + }, + { + "name": "person", + "type": { + "fields": [ + { + "name": "lastname", + "type": "string" + }, + { + "name": "address", + "type": { + "fields": [ + { + "name": "streetaddress", + "type": "string" + }, + { + "name": "city", + "type": "string" + } + ], + "name": "AddressUSRecord", + "type": "record" + } + }, + { + "name": "mapfield", + "type": { + "default": { + }, + "type": "map", + "values": "long" + } + }, + { + "name": "arrayField", + "type": { + "default": [ + ], + "items": "string", + "type": "array" + } + } + ], + "name": "person_data", + "type": "record" + } + }, + { + "name": "decimalField", + "type": { + "logicalType": "decimal", + "precision": 4, + "scale": 2, + "type": "bytes" + } + }, + { + "logicalType": "uuid", + "name": "uuidField", + "type": "string" + }, + { + "name": "timemillis", + "type": { + "type": "int", + "logicalType": "time-millis" + } + }, + { + "name": "timemicros", + "type": { + "type": "long", + "logicalType": "time-micros" + } + }, + { + "name": "timestampmillis", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + }, + { + "name": "timestampmicros", + "type": { + "type": "long", + "logicalType": "timestamp-micros" + } + }, + { + "name": "duration", + "type": { + "name": "duration", + "namespace": "whyowhy", + "logicalType": "duration", + "size": 12, + "type": "fixed" + } + }, + { + "name": "date", + "type": { + "logicalType": "date", + "type": "int" + } + } + ], + "name": "Example", + "type": "record" + }`, + arrowSchema: []arrow.Field{ + { + Name: "inheritNull", + Type: &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint8, ValueType: arrow.BinaryTypes.String, Ordered: false}, + Metadata: arrow.MetadataFrom(map[string]string{"0": "a", "1": "b"}), + }, + { + Name: "explicitNamespace", + Type: &arrow.FixedSizeBinaryType{ByteWidth: 12}, + }, + { + Name: "fullName", + Type: arrow.StructOf( + arrow.Field{ + Name: "inheritNamespace", + Type: &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint8, ValueType: arrow.BinaryTypes.String, Ordered: false}, + }, + arrow.Field{ + Name: "md5", + Type: &arrow.FixedSizeBinaryType{ByteWidth: 16}, + }, + ), + }, + { + Name: "id", + Type: arrow.PrimitiveTypes.Int32, + }, + { + Name: "bigId", + Type: arrow.PrimitiveTypes.Int64, + }, + { + Name: "temperature", + Type: arrow.PrimitiveTypes.Float32, + Nullable: true, + }, + { + Name: "fraction", + Type: arrow.PrimitiveTypes.Float64, + Nullable: true, + }, + { + Name: "is_emergency", + Type: arrow.FixedWidthTypes.Boolean, + }, + { + Name: "remote_ip", + Type: arrow.BinaryTypes.Binary, + Nullable: true, + }, + { + Name: "person", + Type: arrow.StructOf( + arrow.Field{ + Name: "lastname", + Type: arrow.BinaryTypes.String, + Nullable: true, + }, + arrow.Field{ + Name: "address", + Type: arrow.StructOf( + arrow.Field{ + Name: "streetaddress", + Type: arrow.BinaryTypes.String, + }, + arrow.Field{ + Name: "city", + Type: arrow.BinaryTypes.String, + }, + ), + }, + arrow.Field{ + Name: "mapfield", + Type: arrow.MapOf(arrow.BinaryTypes.String, arrow.PrimitiveTypes.Int64), + Nullable: true, + }, + arrow.Field{ + Name: "arrayField", + Type: arrow.ListOfNonNullable(arrow.BinaryTypes.String), + }, + ), + }, + { + Name: "decimalField", + Type: &arrow.Decimal128Type{Precision: 4, Scale: 2}, + }, + { + Name: "uuidField", + Type: arrow.BinaryTypes.String, + }, + { + Name: "timemillis", + Type: arrow.FixedWidthTypes.Time32ms, + }, + { + Name: "timemicros", + Type: arrow.FixedWidthTypes.Time64us, + }, + { + Name: "timestampmillis", + Type: arrow.FixedWidthTypes.Timestamp_ms, + }, + { + Name: "timestampmicros", + Type: arrow.FixedWidthTypes.Timestamp_us, + }, + { + Name: "duration", + Type: arrow.FixedWidthTypes.MonthDayNanoInterval, + }, + { + Name: "date", + Type: arrow.FixedWidthTypes.Date32, + }, + }, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + want := arrow.NewSchema(test.arrowSchema, nil) + schema, err := hamba.ParseBytes([]byte(test.avroSchema)) + if err != nil { + t.Fatalf("%v", err) + } + got, err := ArrowSchemaFromAvro(schema) + if err != nil { + t.Fatalf("%v", err) + } + if !(fmt.Sprintf("%+v", want.String()) == fmt.Sprintf("%+v", got.String())) { + t.Fatalf("got=%v,\n want=%v", got.String(), want.String()) + } else { + t.Logf("schema.String() comparison passed") + } + }) + } +} diff --git a/go/arrow/avro/testdata/arrayrecordmap.avro b/go/arrow/avro/testdata/arrayrecordmap.avro new file mode 100644 index 0000000000000000000000000000000000000000..84a8b59b427b5597866fb1df8dd2e805df722386 GIT binary patch literal 582 zcmeZI%3@?b#V(duR+O(-oSc!Go7l%*t(2FTo2q1`lwXvtmywv8l&A-h&&(~zNmbHO zDyb|0Nf)Ii=NF{_`DvM{IVr_TR?%>siA6<;m2j2SFdZ=A%#zexpnAA+G>gE>;HDE{ z8{8(P%)An%S{=B-NpKllZcr*NDay=CSE@zQm77?AsShGpmY7qTN}SV?O~Dsdd8N5Y zsYP%L(Y*kU3s{&F_C;)MZ7l?gfFmtAKP5GpBd;_khao>y>(M5Q>$5qRCvShebea!S w9%Dubvlv%WVsd^SM@fEKDFdU63{v0&eQ@jlA`WH=uB_C&yh;ekz<^;80Ap0g=>Px# literal 0 HcmV?d00001 diff --git a/go/arrow/avro/testdata/githubsamplecommits.avro b/go/arrow/avro/testdata/githubsamplecommits.avro new file mode 100644 index 0000000000000000000000000000000000000000..f16d17d29e991f540ad18946375e5ed19c70f8c0 GIT binary patch literal 95131 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 mNCKlABU%WlN28+`sNV4W0S%!j