blob: 41571112d44ae06d6fcdbaed36dc52cc995a594d [file] [view]
<!--
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.
-->
# Go Integration
The Go integration is a binding built on top of Apache Paimon Rust, allowing you to access Paimon tables from Go programs. It uses the [Arrow C Data Interface](https://arrow.apache.org/docs/format/CDataInterface.html) for zero-copy data transfer.
## Prerequisites
- Go 1.22.4 or later
- Supported platforms: Linux (amd64, arm64), macOS (amd64, arm64)
## Installation
```bash
go get github.com/apache/paimon-rust/bindings/go
```
The pre-built native library is embedded in the package and automatically loaded at runtime — no manual build step is needed.
## Creating a Catalog
Use `NewCatalog` with a map of options to create a catalog. The catalog type is determined by the `metastore` option (default: `filesystem`).
```go
import paimon "github.com/apache/paimon-rust/bindings/go"
// Local filesystem
catalog, err := paimon.NewCatalog(map[string]string{
"warehouse": "/path/to/warehouse",
})
if err != nil {
log.Fatal(err)
}
defer catalog.Close()
```
### Alibaba Cloud OSS
```go
catalog, err := paimon.NewCatalog(map[string]string{
"warehouse": "oss://bucket/warehouse",
"fs.oss.accessKeyId": "your-access-key-id",
"fs.oss.accessKeySecret": "your-access-key-secret",
"fs.oss.endpoint": "oss-cn-hangzhou.aliyuncs.com",
})
```
### REST Catalog
```go
catalog, err := paimon.NewCatalog(map[string]string{
"metastore": "rest",
"uri": "http://localhost:8080",
"warehouse": "my_warehouse",
})
```
## Reading a Table
Paimon Go uses a **scan-then-read** pattern: first scan the table to produce splits, then read data from those splits as Arrow RecordBatches.
```go
import (
"errors"
"fmt"
"io"
"github.com/apache/arrow-go/v18/arrow/array"
paimon "github.com/apache/paimon-rust/bindings/go"
)
// Get a table from the catalog
table, err := catalog.GetTable(paimon.NewIdentifier("default", "my_table"))
if err != nil {
log.Fatal(err)
}
defer table.Close()
// Create a read builder
rb, err := table.NewReadBuilder()
if err != nil {
log.Fatal(err)
}
defer rb.Close()
// Step 1: Scan — produces a Plan containing DataSplits
scan, err := rb.NewScan()
if err != nil {
log.Fatal(err)
}
defer scan.Close()
plan, err := scan.Plan()
if err != nil {
log.Fatal(err)
}
defer plan.Close()
splits := plan.Splits()
// Step 2: Read — consumes splits and returns Arrow RecordBatches
read, err := rb.NewRead()
if err != nil {
log.Fatal(err)
}
defer read.Close()
reader, err := read.NewRecordBatchReader(splits)
if err != nil {
log.Fatal(err)
}
defer reader.Close()
for {
record, err := reader.NextRecord()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Println(record)
record.Release()
}
```
## Column Projection
Use `WithProjection` to select specific columns. Only the requested columns are read, reducing I/O.
```go
rb, err := table.NewReadBuilder()
if err != nil {
log.Fatal(err)
}
defer rb.Close()
// Only read the "id" and "name" columns
if err := rb.WithProjection([]string{"id", "name"}); err != nil {
log.Fatal(err)
}
// Continue with scan-then-read as above...
```
## Filter Push-Down
Filter push-down prunes data at two levels:
1. **Scan planning** — skips partitions, buckets, and data files based on file-level statistics (min/max).
2. **Read-side** — applies row-level filtering via Parquet native row filters for leaf predicates.
!!! warning
Filter push-down is a **best-effort** optimization. The returned results may still contain rows that do not satisfy the filter condition. Callers should always apply residual filtering on the returned records to ensure correctness.
### Building Predicates
Create predicates through the `PredicateBuilder` obtained from a table:
```go
pb := table.PredicateBuilder()
// Comparison predicates
pred, err := pb.Eq("id", 1) // id = 1
pred, err := pb.NotEq("name", "bob") // name != "bob"
pred, err := pb.Lt("id", 3) // id < 3
pred, err := pb.Le("id", 2) // id <= 2
pred, err := pb.Gt("id", 1) // id > 1
pred, err := pb.Ge("id", 2) // id >= 2
// Null checks
pred, err := pb.IsNull("name") // name IS NULL
pred, err := pb.IsNotNull("name") // name IS NOT NULL
// IN / NOT IN
pred, err := pb.In("id", 1, 2, 3) // id IN (1, 2, 3)
pred, err := pb.NotIn("name", "x", "y") // name NOT IN ("x", "y")
```
### Applying Filters
Pass a predicate to `WithFilter` on the `ReadBuilder`:
```go
rb, err := table.NewReadBuilder()
if err != nil {
log.Fatal(err)
}
defer rb.Close()
pb := table.PredicateBuilder()
pred, err := pb.Eq("id", 1)
if err != nil {
log.Fatal(err)
}
// Ownership of pred is transferred — do NOT close it after this call
if err := rb.WithFilter(pred); err != nil {
log.Fatal(err)
}
// Continue with scan-then-read...
```
### Compound Predicates
Combine predicates with `And`, `Or`, and `Not`. The `predicate` sub-package provides variadic helpers:
```go
import (
paimon "github.com/apache/paimon-rust/bindings/go"
"github.com/apache/paimon-rust/bindings/go/predicate"
)
pb := table.PredicateBuilder()
p1, _ := pb.Ge("id", 1)
p2, _ := pb.Le("id", 3)
p3, _ := pb.Eq("name", "alice")
// id >= 1 AND id <= 3
combined, err := predicate.And(p1, p2)
// (id >= 1 AND id <= 3) OR name = "alice"
combined, err = predicate.Or(combined, p3)
// NOT (...)
negated, err := predicate.Not(combined)
```
!!! note "Predicate Ownership"
Predicates follow a **move** ownership model. After passing a predicate to `WithFilter`, `And`, `Or`, or `Not`, the predicate is consumed and must NOT be closed or reused by the caller.
### Supported Datum Types
Predicate values are automatically converted from Go types:
| Go Type | Paimon Type |
|-----------------------------|----------------------|
| `bool` | Bool |
| `int8` | TinyInt |
| `int16` | SmallInt |
| `int32` | Int |
| `int` / `int64` | Int or Long |
| `float32` | Float |
| `float64` | Double |
| `string` | String |
| `paimon.Date` | Date (epoch days) |
| `paimon.Time` | Time (millis) |
| `paimon.Timestamp` | Timestamp |
| `paimon.LocalZonedTimestamp` | LocalZonedTimestamp |
| `paimon.Decimal` | Decimal |
| `paimon.Bytes` | Binary |
For special types, use the dedicated constructors:
```go
// Date as epoch days since 1970-01-01
pred, _ := pb.Eq("dt", paimon.Date(19000))
// Decimal(123.45) as DECIMAL(10,2)
pred, _ := pb.Eq("amount", paimon.NewDecimal(12345, 10, 2))
// Timestamp
pred, _ := pb.Eq("ts", paimon.Timestamp{Millis: 1700000000000, Nanos: 0})
```
## Resource Management
All Paimon objects (`Catalog`, `Table`, `ReadBuilder`, `TableScan`, `Plan`, `TableRead`, `RecordBatchReader`) hold native resources and must be closed when no longer needed. Use `defer` to ensure cleanup:
```go
catalog, err := paimon.NewCatalog(opts)
if err != nil { log.Fatal(err) }
defer catalog.Close()
table, err := catalog.GetTable(id)
if err != nil { log.Fatal(err) }
defer table.Close()
// ... and so on for ReadBuilder, TableScan, Plan, TableRead, RecordBatchReader
```
All `Close()` methods are safe to call multiple times.
## Complete Example
```go
package main
import (
"errors"
"fmt"
"io"
"log"
"github.com/apache/arrow-go/v18/arrow/array"
paimon "github.com/apache/paimon-rust/bindings/go"
)
func main() {
// 1. Open catalog and table
catalog, err := paimon.NewCatalog(map[string]string{
"warehouse": "/tmp/paimon-warehouse",
})
if err != nil {
log.Fatal(err)
}
defer catalog.Close()
table, err := catalog.GetTable(paimon.NewIdentifier("default", "my_table"))
if err != nil {
log.Fatal(err)
}
defer table.Close()
// 2. Configure read: projection + filter
rb, err := table.NewReadBuilder()
if err != nil {
log.Fatal(err)
}
defer rb.Close()
if err := rb.WithProjection([]string{"id", "name"}); err != nil {
log.Fatal(err)
}
pb := table.PredicateBuilder()
pred, err := pb.Gt("id", 0)
if err != nil {
log.Fatal(err)
}
if err := rb.WithFilter(pred); err != nil {
log.Fatal(err)
}
// 3. Scan
scan, err := rb.NewScan()
if err != nil {
log.Fatal(err)
}
defer scan.Close()
plan, err := scan.Plan()
if err != nil {
log.Fatal(err)
}
defer plan.Close()
// 4. Read
read, err := rb.NewRead()
if err != nil {
log.Fatal(err)
}
defer read.Close()
reader, err := read.NewRecordBatchReader(plan.Splits())
if err != nil {
log.Fatal(err)
}
defer reader.Close()
for {
record, err := reader.NextRecord()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
log.Fatal(err)
}
idCol := record.Column(0).(*array.Int32)
nameCol := record.Column(1).(*array.String)
for i := 0; i < int(record.NumRows()); i++ {
fmt.Printf("id=%d name=%s\n", idCol.Value(i), nameCol.Value(i))
}
record.Release()
}
}
```