Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ header:
- ".github/PULL_REQUEST_TEMPLATE.md"
- "crates/paimon/tests/**/*.json"
- "crates/paimon/testdata/**"
- "bindings/go/tests/testdata/**"
- "third-party-licenses/jieba-rs-0.10.3.LICENSE"
- "third-party-licenses/openssl-1.1.1.LICENSE"
- "**/go.sum"
Expand Down
37 changes: 37 additions & 0 deletions bindings/go/blob_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ import (
"context"
"fmt"
"runtime"
"strings"
"sync"
"unsafe"

"github.com/apache/arrow-go/v18/arrow"
"github.com/apache/arrow-go/v18/arrow/array"
"github.com/jupiterrider/ffi"
)

Expand Down Expand Up @@ -83,6 +86,40 @@ func (r *BlobReader) ReadBlobs(descriptors [][]byte) ([][]byte, error) {
return ffiBlobReaderReadBlobs.symbol(r.ctx)(r.inner, descriptors)
}

// StringBlobMapDescriptors returns one MAP<STRING, BLOB> row.
func StringBlobMapDescriptors(column arrow.Array, row int) (map[string][]byte, error) {
m, ok := column.(*array.Map)
if !ok {
return nil, fmt.Errorf("paimon: BLOB map column is %T, want *array.Map", column)
}
if row < 0 || row >= m.Len() {
return nil, fmt.Errorf("paimon: BLOB map row %d is out of range", row)
}
if m.IsNull(row) {
return nil, nil
}
keys, ok := m.Keys().(*array.String)
if !ok {
return nil, fmt.Errorf("paimon: BLOB map keys are %T, want *array.String", m.Keys())
}
descriptors, ok := m.Items().(*array.Binary)
if !ok {
return nil, fmt.Errorf("paimon: BLOB map values are %T, want *array.Binary", m.Items())
}
start, end := m.ValueOffsets(row)
result := make(map[string][]byte, end-start)
for index := start; index < end; index++ {
i := int(index)
key := strings.Clone(keys.Value(i))
if descriptors.IsNull(i) {
result[key] = nil
continue
}
result[key] = append([]byte(nil), descriptors.Value(i)...)
}
return result, nil
}

// Close releases the reader and is idempotent.
func (r *BlobReader) Close() {
r.mu.Lock()
Expand Down
63 changes: 61 additions & 2 deletions bindings/go/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package paimon

import (
"context"
"runtime"
"sync"
"unsafe"

Expand Down Expand Up @@ -54,8 +55,20 @@ func (t *Table) NewReadBuilder() (*ReadBuilder, error) {
if t.inner == nil {
return nil, ErrClosed
}
createFn := ffiTableNewReadBuilder.symbol(t.ctx)
inner, err := createFn(t.inner)
inner, err := ffiTableNewReadBuilder.symbol(t.ctx)(t.inner)
if err != nil {
return nil, err
}
t.lib.acquire()
return &ReadBuilder{ctx: t.ctx, lib: t.lib, inner: inner}, nil
}

// NewReadBuilderWithOptions creates a ReadBuilder with per-read options.
func (t *Table) NewReadBuilderWithOptions(options map[string]string) (*ReadBuilder, error) {
if t.inner == nil {
return nil, ErrClosed
}
inner, err := ffiTableNewReadBuilderWithOptions.symbol(t.ctx)(t.inner, options)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -93,3 +106,49 @@ var ffiTableNewReadBuilder = newFFI(ffiOpts{
return result.readBuilder, nil
}
})

var ffiTableNewReadBuilderWithOptions = newFFI(ffiOpts{
sym: "paimon_table_new_read_builder_with_options",
rType: &typeResultReadBuilder,
aTypes: []*ffi.Type{
&ffi.TypePointer,
&ffi.TypePointer,
&ffi.TypePointer,
},
}, func(ctx context.Context, ffiCall ffiCall) func(*paimonTable, map[string]string) (*paimonReadBuilder, error) {
return func(table *paimonTable, options map[string]string) (*paimonReadBuilder, error) {
type paimonOption struct {
key *byte
value *byte
}
opts := make([]paimonOption, 0, len(options))
for key, value := range options {
keyPtr, err := bytePtrFromString(key)
if err != nil {
return nil, err
}
valuePtr, err := bytePtrFromString(value)
if err != nil {
return nil, err
}
opts = append(opts, paimonOption{key: keyPtr, value: valuePtr})
}
var optsPtr unsafe.Pointer
if len(opts) > 0 {
optsPtr = unsafe.Pointer(&opts[0])
}
optsLen := uintptr(len(opts))
var result resultReadBuilder
ffiCall(
unsafe.Pointer(&result),
unsafe.Pointer(&table),
unsafe.Pointer(&optsPtr),
unsafe.Pointer(&optsLen),
)
runtime.KeepAlive(opts)
if result.error != nil {
return nil, parseError(ctx, result.error)
}
return result.readBuilder, nil
}
})
90 changes: 90 additions & 0 deletions bindings/go/tests/blob_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"strings"
"testing"

"github.com/apache/arrow-go/v18/arrow/array"
paimon "github.com/apache/paimon-rust/bindings/go"
)

Expand Down Expand Up @@ -101,6 +102,95 @@ func TestBlobReaderReadBlobAndBatch(t *testing.T) {
}
}

func TestStringBlobMapDescriptors(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this test cover the actual read path instead of starting from a hand-built Arrow map? As written, it only tests descriptor extraction and BlobReader. A writer-generated fixture read through NewReadBuilderWithOptions would also cover the Rust decoder, C Data conversion, and Go API.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cd19225. The Go test now reads a PyPaimon-generated table through NewReadBuilderWithOptions, Rust decoding, C Data import, Go map extraction, and ReadBlobs.

source := filepath.Join("testdata", "map_blob_table")
warehouse := t.TempDir()
if err := copyDirectory(source, filepath.Join(warehouse, "default.db", "map_blob_table")); err != nil {
t.Fatal(err)
}
table := openTableAt(t, warehouse, "map_blob_table")
builder, err := table.NewReadBuilderWithOptions(map[string]string{
"blob-as-descriptor": "true",
})
if err != nil {
t.Fatal(err)
}
defer builder.Close()
if err := builder.WithProjection([]string{"id", "assets"}); err != nil {
t.Fatal(err)
}
scan, err := builder.NewScan()
if err != nil {
t.Fatal(err)
}
defer scan.Close()
plan, err := scan.Plan()
if err != nil {
t.Fatal(err)
}
defer plan.Close()
read, err := builder.NewRead()
if err != nil {
t.Fatal(err)
}
defer read.Close()
batches, err := read.NewRecordBatchReader(plan.Splits())
if err != nil {
t.Fatal(err)
}
defer batches.Close()

rows := make(map[int32]map[string][]byte)
for {
record, err := batches.NextRecord()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatal(err)
}
ids := record.Column(0).(*array.Int32)
for row := 0; row < int(record.NumRows()); row++ {
rows[ids.Value(row)], err = paimon.StringBlobMapDescriptors(record.Column(1), row)
if err != nil {
record.Release()
t.Fatal(err)
}
}
record.Release()
}

descriptors := rows[1]
if len(rows) != 3 {
t.Fatalf("read %d rows, want 3", len(rows))
}
if len(descriptors["first"]) == 0 || len(descriptors["tail"]) == 0 {
t.Fatalf("descriptor map is invalid after Arrow release: %#v", descriptors)
}
if rows[2] != nil || len(rows[3]) != 0 {
t.Fatalf("unexpected null or empty maps: %#v", rows)
}
reader, err := table.NewBlobReader()
if err != nil {
t.Fatal(err)
}
defer reader.Close()
resolved, err := reader.ReadBlobs([][]byte{
descriptors["tail"],
descriptors["first"],
descriptors["empty"],
})
if err != nil {
t.Fatal(err)
}
if string(resolved[0]) != "ghij" || string(resolved[1]) != "abc" || len(resolved[2]) != 0 {
t.Fatalf("unexpected values: %q", resolved)
}
if descriptors["null"] != nil {
t.Fatalf("null BLOB returned %#v", descriptors["null"])
}
}

func TestBlobReaderFromTableOutlivesTable(t *testing.T) {
file := writeBlobFile(t, "table", "abcdefghij")

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
30 changes: 30 additions & 0 deletions bindings/go/tests/testdata/map_blob_table/schema/schema-0
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"version": 3,
"id": 0,
"fields": [
{
"id": 0,
"name": "id",
"type": "INT"
},
{
"id": 1,
"name": "assets",
"type": {
"type": "MAP",
"key": "STRING NOT NULL",
"value": "BLOB",
"nullable": true
}
}
],
"highestFieldId": 1,
"partitionKeys": [],
"primaryKeys": [],
"options": {
"row-tracking.enabled": "true",
"data-evolution.enabled": "true"
},
"comment": null,
"timeMillis": 1788359448372
}
1 change: 1 addition & 0 deletions bindings/go/tests/testdata/map_blob_table/snapshot/LATEST
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
15 changes: 15 additions & 0 deletions bindings/go/tests/testdata/map_blob_table/snapshot/snapshot-1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": 3,
"id": 1,
"schemaId": 0,
"baseManifestList": "manifest-list-d854a2ec-c453-4ece-bcbe-5ad5411a8273-0",
"deltaManifestList": "manifest-list-d854a2ec-c453-4ece-bcbe-5ad5411a8273-1",
"totalRecordCount": 6,
"deltaRecordCount": 6,
"commitUser": "ab9bd0e7-c0bc-4695-84cf-44f36df42a6a",
"commitIdentifier": 9223372036854775807,
"commitKind": "APPEND",
"timeMillis": 1788359448379,
"nextRowId": 3,
"uuid": "19a3dfb1-eae3-4d7a-8329-26b85143edcc"
}
Loading
Loading