| /* |
| * 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 paimon |
| |
| import ( |
| "context" |
| "fmt" |
| "math" |
| "runtime" |
| "sync" |
| "unsafe" |
| |
| "github.com/jupiterrider/ffi" |
| ) |
| |
| // Datum type tags (must match paimon-c datum_from_c). |
| const ( |
| datumTagBool int32 = 0 |
| datumTagTinyInt int32 = 1 |
| datumTagSmallInt int32 = 2 |
| datumTagInt int32 = 3 |
| datumTagLong int32 = 4 |
| datumTagFloat int32 = 5 |
| datumTagDouble int32 = 6 |
| datumTagString int32 = 7 |
| datumTagDate int32 = 8 |
| datumTagTime int32 = 9 |
| datumTagTimestamp int32 = 10 |
| datumTagLocalZonedTimestamp int32 = 11 |
| datumTagDecimal int32 = 12 |
| datumTagBytes int32 = 13 |
| ) |
| |
| // Datum is a typed literal value for predicate comparison. |
| // The internal representation is hidden to allow future changes |
| // (e.g. switching to opaque handles) without breaking callers. |
| type Datum struct { |
| inner paimonDatumC |
| } |
| |
| // BoolDatum creates a boolean datum. |
| func BoolDatum(v bool) Datum { |
| var iv int64 |
| if v { |
| iv = 1 |
| } |
| return Datum{inner: paimonDatumC{tag: datumTagBool, intVal: iv}} |
| } |
| |
| // TinyIntDatum creates a tinyint datum. |
| func TinyIntDatum(v int8) Datum { |
| return Datum{inner: paimonDatumC{tag: datumTagTinyInt, intVal: int64(v)}} |
| } |
| |
| // SmallIntDatum creates a smallint datum. |
| func SmallIntDatum(v int16) Datum { |
| return Datum{inner: paimonDatumC{tag: datumTagSmallInt, intVal: int64(v)}} |
| } |
| |
| // IntDatum creates an int datum. |
| func IntDatum(v int32) Datum { |
| return Datum{inner: paimonDatumC{tag: datumTagInt, intVal: int64(v)}} |
| } |
| |
| // LongDatum creates a long (bigint) datum. |
| func LongDatum(v int64) Datum { |
| return Datum{inner: paimonDatumC{tag: datumTagLong, intVal: v}} |
| } |
| |
| // FloatDatum creates a float datum. |
| func FloatDatum(v float32) Datum { |
| return Datum{inner: paimonDatumC{tag: datumTagFloat, dblVal: float64(v)}} |
| } |
| |
| // DoubleDatum creates a double datum. |
| func DoubleDatum(v float64) Datum { |
| return Datum{inner: paimonDatumC{tag: datumTagDouble, dblVal: v}} |
| } |
| |
| // StringDatum creates a string datum. |
| func StringDatum(v string) Datum { |
| b := []byte(v) |
| d := paimonDatumC{tag: datumTagString, strLen: uintptr(len(b))} |
| if len(b) > 0 { |
| d.strData = &b[0] |
| } |
| return Datum{inner: d} |
| } |
| |
| // Date represents a date value as epoch days since 1970-01-01. |
| // Usage: table.PredicateEqual("dt", paimon.Date(19000)) |
| type Date int32 |
| |
| // Time represents a time-of-day value as milliseconds since midnight. |
| // Usage: table.PredicateEqual("t", paimon.Time(3600000)) |
| type Time int32 |
| |
| // Timestamp represents a timestamp without timezone (millis + sub-millis nanos). |
| // Usage: table.PredicateEqual("ts", paimon.Timestamp{Millis: 1700000000000, Nanos: 0}) |
| type Timestamp struct { |
| Millis int64 |
| Nanos int32 |
| } |
| |
| // LocalZonedTimestamp represents a timestamp with local timezone semantics. |
| // Usage: table.PredicateEqual("lzts", paimon.LocalZonedTimestamp{Millis: 1700000000000, Nanos: 0}) |
| type LocalZonedTimestamp struct { |
| Millis int64 |
| Nanos int32 |
| } |
| |
| // Decimal represents a fixed-precision decimal value up to DECIMAL(38, s). |
| // |
| // The unscaled value is stored as a little-endian i128 split into two int64 |
| // halves: Lo (low 64 bits, unsigned interpretation) and Hi (high 64 bits, |
| // sign-extended). For values that fit in int64, use [NewDecimal]. |
| // |
| // Usage: |
| // |
| // paimon.NewDecimal(12345, 10, 2) // 123.45 as DECIMAL(10,2) |
| // paimon.Decimal{Lo: lo, Hi: hi, ...} // full i128 |
| type Decimal struct { |
| Lo int64 // low 64 bits of unscaled i128 (unsigned interpretation) |
| Hi int64 // high 64 bits of unscaled i128 (sign extension) |
| Precision uint32 |
| Scale uint32 |
| } |
| |
| // NewDecimal creates a Decimal from an int64 unscaled value. |
| // For unscaled values that exceed int64 range, construct Decimal directly |
| // with Lo/Hi fields. |
| func NewDecimal(unscaled int64, precision, scale uint32) Decimal { |
| hi := int64(0) |
| if unscaled < 0 { |
| hi = -1 |
| } |
| return Decimal{Lo: unscaled, Hi: hi, Precision: precision, Scale: scale} |
| } |
| |
| // Bytes represents a binary value. |
| // Usage: table.PredicateEqual("data", paimon.Bytes(someSlice)) |
| type Bytes []byte |
| |
| // toDatum converts a Go value to a Datum for predicate comparison. |
| // |
| // Supported Go types and their Paimon mappings: |
| // - bool → Bool |
| // - int8 → TinyInt |
| // - int16 → SmallInt |
| // - int32 → Int |
| // - int → Int (if fits int32) or Long |
| // - int64 → Long |
| // - float32 → Float |
| // - float64 → Double |
| // - string → String |
| // - Date → Date |
| // - Time → Time |
| // - Timestamp → Timestamp |
| // - LocalZonedTimestamp → LocalZonedTimestamp |
| // - Decimal → Decimal |
| // - Bytes → Bytes |
| // - Datum → passed through |
| func toDatum(v any) (Datum, error) { |
| switch val := v.(type) { |
| case bool: |
| return BoolDatum(val), nil |
| case int8: |
| return TinyIntDatum(val), nil |
| case int16: |
| return SmallIntDatum(val), nil |
| case int32: |
| return IntDatum(val), nil |
| case int: |
| if val >= math.MinInt32 && val <= math.MaxInt32 { |
| return IntDatum(int32(val)), nil |
| } |
| return LongDatum(int64(val)), nil |
| case int64: |
| return LongDatum(val), nil |
| case float32: |
| return FloatDatum(val), nil |
| case float64: |
| return DoubleDatum(val), nil |
| case string: |
| return StringDatum(val), nil |
| case Date: |
| return Datum{inner: paimonDatumC{tag: datumTagDate, intVal: int64(val)}}, nil |
| case Time: |
| return Datum{inner: paimonDatumC{tag: datumTagTime, intVal: int64(val)}}, nil |
| case Timestamp: |
| return Datum{inner: paimonDatumC{tag: datumTagTimestamp, intVal: val.Millis, intVal2: int64(val.Nanos)}}, nil |
| case LocalZonedTimestamp: |
| return Datum{inner: paimonDatumC{tag: datumTagLocalZonedTimestamp, intVal: val.Millis, intVal2: int64(val.Nanos)}}, nil |
| case Decimal: |
| return Datum{inner: paimonDatumC{ |
| tag: datumTagDecimal, intVal: val.Lo, intVal2: val.Hi, |
| uintVal: val.Precision, uintVal2: val.Scale, |
| }}, nil |
| case Bytes: |
| d := paimonDatumC{tag: datumTagBytes, strLen: uintptr(len(val))} |
| if len(val) > 0 { |
| d.strData = &val[0] |
| } |
| return Datum{inner: d}, nil |
| case Datum: |
| return val, nil |
| default: |
| return Datum{}, fmt.Errorf("unsupported datum type: %T", v) |
| } |
| } |
| |
| // Predicate is an opaque filter predicate for scan planning. |
| type Predicate struct { |
| ctx context.Context |
| lib *libRef |
| inner *paimonPredicate |
| closeOnce sync.Once |
| } |
| |
| // Close releases the predicate resources. Safe to call multiple times. |
| // Note: predicates passed to WithFilter or combinators (And/Or/Not) are consumed |
| // and should NOT be closed by the caller. |
| func (p *Predicate) Close() { |
| p.closeOnce.Do(func() { |
| if p.inner != nil { |
| ffiPredicateFree.symbol(p.ctx)(p.inner) |
| p.inner = nil |
| p.lib.release() |
| } |
| }) |
| } |
| |
| // errConsumedPredicate is returned when a consumed or nil predicate is reused. |
| var errConsumedPredicate = fmt.Errorf("paimon: predicate already consumed or nil") |
| |
| // PredicateBuilder creates filter predicates for a table. |
| // It holds a Go-level reference to the Table and does not own any C resources, |
| // so there is no Close() method. |
| type PredicateBuilder struct { |
| table *Table |
| } |
| |
| // Eq creates an equality predicate: column = value. |
| func (pb *PredicateBuilder) Eq(column string, value any) (*Predicate, error) { |
| datum, err := toDatum(value) |
| if err != nil { |
| return nil, err |
| } |
| return pb.buildLeafPredicate(ffiPredicateEqual, column, datum) |
| } |
| |
| // NotEq creates a not-equal predicate: column != value. |
| func (pb *PredicateBuilder) NotEq(column string, value any) (*Predicate, error) { |
| datum, err := toDatum(value) |
| if err != nil { |
| return nil, err |
| } |
| return pb.buildLeafPredicate(ffiPredicateNotEqual, column, datum) |
| } |
| |
| // Lt creates a less-than predicate: column < value. |
| func (pb *PredicateBuilder) Lt(column string, value any) (*Predicate, error) { |
| datum, err := toDatum(value) |
| if err != nil { |
| return nil, err |
| } |
| return pb.buildLeafPredicate(ffiPredicateLessThan, column, datum) |
| } |
| |
| // Le creates a less-or-equal predicate: column <= value. |
| func (pb *PredicateBuilder) Le(column string, value any) (*Predicate, error) { |
| datum, err := toDatum(value) |
| if err != nil { |
| return nil, err |
| } |
| return pb.buildLeafPredicate(ffiPredicateLessOrEqual, column, datum) |
| } |
| |
| // Gt creates a greater-than predicate: column > value. |
| func (pb *PredicateBuilder) Gt(column string, value any) (*Predicate, error) { |
| datum, err := toDatum(value) |
| if err != nil { |
| return nil, err |
| } |
| return pb.buildLeafPredicate(ffiPredicateGreaterThan, column, datum) |
| } |
| |
| // Ge creates a greater-or-equal predicate: column >= value. |
| func (pb *PredicateBuilder) Ge(column string, value any) (*Predicate, error) { |
| datum, err := toDatum(value) |
| if err != nil { |
| return nil, err |
| } |
| return pb.buildLeafPredicate(ffiPredicateGreaterOrEqual, column, datum) |
| } |
| |
| // IsNull creates an IS NULL predicate. |
| func (pb *PredicateBuilder) IsNull(column string) (*Predicate, error) { |
| return pb.buildNullPredicate(ffiPredicateIsNull, column) |
| } |
| |
| // IsNotNull creates an IS NOT NULL predicate. |
| func (pb *PredicateBuilder) IsNotNull(column string) (*Predicate, error) { |
| return pb.buildNullPredicate(ffiPredicateIsNotNull, column) |
| } |
| |
| // In creates an IN predicate: column IN (values...). |
| func (pb *PredicateBuilder) In(column string, values ...any) (*Predicate, error) { |
| return pb.buildInPredicate(ffiPredicateIsIn, column, values) |
| } |
| |
| // NotIn creates a NOT IN predicate: column NOT IN (values...). |
| func (pb *PredicateBuilder) NotIn(column string, values ...any) (*Predicate, error) { |
| return pb.buildInPredicate(ffiPredicateIsNotIn, column, values) |
| } |
| |
| // buildLeafPredicate is a helper for comparison predicates that take (table, column, datum). |
| func (pb *PredicateBuilder) buildLeafPredicate( |
| ffiVar *FFI[func(*paimonTable, *byte, paimonDatumC) (*paimonPredicate, error)], |
| column string, datum Datum, |
| ) (*Predicate, error) { |
| t := pb.table |
| if t.inner == nil { |
| return nil, ErrClosed |
| } |
| createFn := ffiVar.symbol(t.ctx) |
| cCol := append([]byte(column), 0) |
| inner, err := createFn(t.inner, &cCol[0], datum.inner) |
| runtime.KeepAlive(cCol) |
| runtime.KeepAlive(datum) |
| if err != nil { |
| return nil, err |
| } |
| t.lib.acquire() |
| return &Predicate{ctx: t.ctx, lib: t.lib, inner: inner}, nil |
| } |
| |
| // buildNullPredicate is a helper for IS NULL / IS NOT NULL predicates. |
| func (pb *PredicateBuilder) buildNullPredicate( |
| ffiVar *FFI[func(*paimonTable, *byte) (*paimonPredicate, error)], |
| column string, |
| ) (*Predicate, error) { |
| t := pb.table |
| if t.inner == nil { |
| return nil, ErrClosed |
| } |
| createFn := ffiVar.symbol(t.ctx) |
| cCol := append([]byte(column), 0) |
| inner, err := createFn(t.inner, &cCol[0]) |
| runtime.KeepAlive(cCol) |
| if err != nil { |
| return nil, err |
| } |
| t.lib.acquire() |
| return &Predicate{ctx: t.ctx, lib: t.lib, inner: inner}, nil |
| } |
| |
| // buildInPredicate is a helper for IS IN / IS NOT IN predicates. |
| func (pb *PredicateBuilder) buildInPredicate( |
| ffiVar *FFI[func(*paimonTable, *byte, unsafe.Pointer, uintptr) (*paimonPredicate, error)], |
| column string, values []any, |
| ) (*Predicate, error) { |
| t := pb.table |
| if t.inner == nil { |
| return nil, ErrClosed |
| } |
| datums := make([]paimonDatumC, len(values)) |
| for i, v := range values { |
| d, err := toDatum(v) |
| if err != nil { |
| return nil, err |
| } |
| datums[i] = d.inner |
| } |
| createFn := ffiVar.symbol(t.ctx) |
| cCol := append([]byte(column), 0) |
| var datumsPtr unsafe.Pointer |
| if len(datums) > 0 { |
| datumsPtr = unsafe.Pointer(&datums[0]) |
| } |
| inner, err := createFn(t.inner, &cCol[0], datumsPtr, uintptr(len(datums))) |
| runtime.KeepAlive(cCol) |
| runtime.KeepAlive(datums) |
| runtime.KeepAlive(values) |
| if err != nil { |
| return nil, err |
| } |
| t.lib.acquire() |
| return &Predicate{ctx: t.ctx, lib: t.lib, inner: inner}, nil |
| } |
| |
| // combinePredicate is a shared helper for And/Or. |
| func (p *Predicate) combinePredicate( |
| other *Predicate, |
| ffiVar *FFI[func(*paimonPredicate, *paimonPredicate) *paimonPredicate], |
| ) (*Predicate, error) { |
| if p == nil || p.inner == nil { |
| return nil, errConsumedPredicate |
| } |
| if other == nil || other.inner == nil { |
| return nil, errConsumedPredicate |
| } |
| if p == other { |
| return nil, fmt.Errorf("paimon: cannot combine a predicate with itself") |
| } |
| combineFn := ffiVar.symbol(p.ctx) |
| p.inner = combineFn(p.inner, other.inner) |
| other.inner = nil |
| other.lib.release() |
| return p, nil |
| } |
| |
| // And combines this predicate with another using AND. Consumes both predicates |
| // (callers must NOT close either after this call). |
| func (p *Predicate) And(other *Predicate) (*Predicate, error) { |
| return p.combinePredicate(other, ffiPredicateAnd) |
| } |
| |
| // Or combines this predicate with another using OR. Consumes both predicates |
| // (callers must NOT close either after this call). |
| func (p *Predicate) Or(other *Predicate) (*Predicate, error) { |
| return p.combinePredicate(other, ffiPredicateOr) |
| } |
| |
| // Not negates this predicate. Consumes the input |
| // (caller must NOT close it after this call). |
| func (p *Predicate) Not() (*Predicate, error) { |
| if p == nil || p.inner == nil { |
| return nil, errConsumedPredicate |
| } |
| negateFn := ffiPredicateNot.symbol(p.ctx) |
| p.inner = negateFn(p.inner) |
| return p, nil |
| } |
| |
| // FFI wrappers for predicate functions. |
| |
| var ffiPredicateFree = newFFI(ffiOpts{ |
| sym: "paimon_predicate_free", |
| rType: &ffi.TypeVoid, |
| aTypes: []*ffi.Type{&ffi.TypePointer}, |
| }, func(_ context.Context, ffiCall ffiCall) func(p *paimonPredicate) { |
| return func(p *paimonPredicate) { |
| ffiCall(nil, unsafe.Pointer(&p)) |
| } |
| }) |
| |
| var ffiPredicateEqual = newPredicateLeafFFI("paimon_predicate_equal") |
| var ffiPredicateNotEqual = newPredicateLeafFFI("paimon_predicate_not_equal") |
| var ffiPredicateLessThan = newPredicateLeafFFI("paimon_predicate_less_than") |
| var ffiPredicateLessOrEqual = newPredicateLeafFFI("paimon_predicate_less_or_equal") |
| var ffiPredicateGreaterThan = newPredicateLeafFFI("paimon_predicate_greater_than") |
| var ffiPredicateGreaterOrEqual = newPredicateLeafFFI("paimon_predicate_greater_or_equal") |
| |
| // newPredicateLeafFFI creates an FFI wrapper for comparison predicate functions |
| // with signature: (table, column, datum) -> result_predicate. |
| func newPredicateLeafFFI(sym string) *FFI[func(*paimonTable, *byte, paimonDatumC) (*paimonPredicate, error)] { |
| return newFFI(ffiOpts{ |
| sym: contextKey(sym), |
| rType: &typeResultPredicate, |
| aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer, &typePaimonDatum}, |
| }, func(ctx context.Context, ffiCall ffiCall) func(*paimonTable, *byte, paimonDatumC) (*paimonPredicate, error) { |
| return func(table *paimonTable, column *byte, datum paimonDatumC) (*paimonPredicate, error) { |
| var result resultPredicate |
| ffiCall( |
| unsafe.Pointer(&result), |
| unsafe.Pointer(&table), |
| unsafe.Pointer(&column), |
| unsafe.Pointer(&datum), |
| ) |
| if result.error != nil { |
| return nil, parseError(ctx, result.error) |
| } |
| return result.predicate, nil |
| } |
| }) |
| } |
| |
| var ffiPredicateIsNull = newPredicateNullFFI("paimon_predicate_is_null") |
| var ffiPredicateIsNotNull = newPredicateNullFFI("paimon_predicate_is_not_null") |
| |
| // newPredicateNullFFI creates an FFI wrapper for null-check predicate functions |
| // with signature: (table, column) -> result_predicate. |
| func newPredicateNullFFI(sym string) *FFI[func(*paimonTable, *byte) (*paimonPredicate, error)] { |
| return newFFI(ffiOpts{ |
| sym: contextKey(sym), |
| rType: &typeResultPredicate, |
| aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer}, |
| }, func(ctx context.Context, ffiCall ffiCall) func(*paimonTable, *byte) (*paimonPredicate, error) { |
| return func(table *paimonTable, column *byte) (*paimonPredicate, error) { |
| var result resultPredicate |
| ffiCall( |
| unsafe.Pointer(&result), |
| unsafe.Pointer(&table), |
| unsafe.Pointer(&column), |
| ) |
| if result.error != nil { |
| return nil, parseError(ctx, result.error) |
| } |
| return result.predicate, nil |
| } |
| }) |
| } |
| |
| var ffiPredicateIsIn = newPredicateInFFI("paimon_predicate_is_in") |
| var ffiPredicateIsNotIn = newPredicateInFFI("paimon_predicate_is_not_in") |
| |
| // newPredicateInFFI creates an FFI wrapper for IN/NOT IN predicate functions |
| // with signature: (table, column, datums, datums_len) -> result_predicate. |
| func newPredicateInFFI(sym string) *FFI[func(*paimonTable, *byte, unsafe.Pointer, uintptr) (*paimonPredicate, error)] { |
| return newFFI(ffiOpts{ |
| sym: contextKey(sym), |
| rType: &typeResultPredicate, |
| aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer, &ffi.TypePointer, &ffi.TypePointer}, |
| }, func(ctx context.Context, ffiCall ffiCall) func(*paimonTable, *byte, unsafe.Pointer, uintptr) (*paimonPredicate, error) { |
| return func(table *paimonTable, column *byte, datums unsafe.Pointer, datumsLen uintptr) (*paimonPredicate, error) { |
| var result resultPredicate |
| ffiCall( |
| unsafe.Pointer(&result), |
| unsafe.Pointer(&table), |
| unsafe.Pointer(&column), |
| unsafe.Pointer(&datums), |
| unsafe.Pointer(&datumsLen), |
| ) |
| if result.error != nil { |
| return nil, parseError(ctx, result.error) |
| } |
| return result.predicate, nil |
| } |
| }) |
| } |
| |
| var ffiPredicateAnd = newFFI(ffiOpts{ |
| sym: "paimon_predicate_and", |
| rType: &ffi.TypePointer, |
| aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer}, |
| }, func(_ context.Context, ffiCall ffiCall) func(*paimonPredicate, *paimonPredicate) *paimonPredicate { |
| return func(a, b *paimonPredicate) *paimonPredicate { |
| var result *paimonPredicate |
| ffiCall( |
| unsafe.Pointer(&result), |
| unsafe.Pointer(&a), |
| unsafe.Pointer(&b), |
| ) |
| return result |
| } |
| }) |
| |
| var ffiPredicateOr = newFFI(ffiOpts{ |
| sym: "paimon_predicate_or", |
| rType: &ffi.TypePointer, |
| aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer}, |
| }, func(_ context.Context, ffiCall ffiCall) func(*paimonPredicate, *paimonPredicate) *paimonPredicate { |
| return func(a, b *paimonPredicate) *paimonPredicate { |
| var result *paimonPredicate |
| ffiCall( |
| unsafe.Pointer(&result), |
| unsafe.Pointer(&a), |
| unsafe.Pointer(&b), |
| ) |
| return result |
| } |
| }) |
| |
| var ffiPredicateNot = newFFI(ffiOpts{ |
| sym: "paimon_predicate_not", |
| rType: &ffi.TypePointer, |
| aTypes: []*ffi.Type{&ffi.TypePointer}, |
| }, func(_ context.Context, ffiCall ffiCall) func(*paimonPredicate) *paimonPredicate { |
| return func(p *paimonPredicate) *paimonPredicate { |
| var result *paimonPredicate |
| ffiCall( |
| unsafe.Pointer(&result), |
| unsafe.Pointer(&p), |
| ) |
| return result |
| } |
| }) |