This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 5c22b080 feat(go): expose string and range predicates through the Go
binding (#1022)
5c22b080 is described below
commit 5c22b080930e4090eddc79d72609f5babd0ec951
Author: jackylee <[email protected]>
AuthorDate: Sun Oct 4 22:21:37 2026 +0800
feat(go): expose string and range predicates through the Go binding (#1022)
---
bindings/go/predicate.go | 162 +++++++++++++++++++++++++++++++++++++++
bindings/go/tests/paimon_test.go | 47 ++++++++++++
2 files changed, 209 insertions(+)
diff --git a/bindings/go/predicate.go b/bindings/go/predicate.go
index d2c03d33..6df94003 100644
--- a/bindings/go/predicate.go
+++ b/bindings/go/predicate.go
@@ -344,6 +344,40 @@ func (pb *PredicateBuilder) NotIn(column string, values
...any) (*Predicate, err
return pb.buildInPredicate(ffiPredicateIsNotIn, column, values)
}
+// StartsWith creates a predicate matching rows where the string column starts
+// with prefix.
+func (pb *PredicateBuilder) StartsWith(column string, prefix string)
(*Predicate, error) {
+ return pb.buildLeafPredicate(ffiPredicateStartsWith, column,
StringDatum(prefix))
+}
+
+// EndsWith creates a predicate matching rows where the string column ends with
+// suffix.
+func (pb *PredicateBuilder) EndsWith(column string, suffix string)
(*Predicate, error) {
+ return pb.buildLeafPredicate(ffiPredicateEndsWith, column,
StringDatum(suffix))
+}
+
+// Contains creates a predicate matching rows where the string column contains
+// substr.
+func (pb *PredicateBuilder) Contains(column string, substr string)
(*Predicate, error) {
+ return pb.buildLeafPredicate(ffiPredicateContains, column,
StringDatum(substr))
+}
+
+// Like creates a SQL LIKE predicate: column LIKE pattern, using the default
+// escape character.
+func (pb *PredicateBuilder) Like(column string, pattern string) (*Predicate,
error) {
+ return pb.buildLikePredicate(ffiPredicateLike, column,
StringDatum(pattern))
+}
+
+// Between creates an inclusive range predicate: low <= column <= high.
+func (pb *PredicateBuilder) Between(column string, low, high any) (*Predicate,
error) {
+ return pb.buildBetweenPredicate(ffiPredicateBetween, column, low, high)
+}
+
+// NotBetween creates a predicate: column < low OR column > high.
+func (pb *PredicateBuilder) NotBetween(column string, low, high any)
(*Predicate, error) {
+ return pb.buildBetweenPredicate(ffiPredicateNotBetween, column, low,
high)
+}
+
// buildLeafPredicate is a helper for comparison predicates that take (table,
column, datum).
func (pb *PredicateBuilder) buildLeafPredicate(
ffiVar *FFI[func(*paimonTable, *byte, paimonDatumC, bool)
(*paimonPredicate, error)],
@@ -418,6 +452,60 @@ func (pb *PredicateBuilder) buildInPredicate(
return &Predicate{ctx: t.ctx, lib: t.lib, inner: inner}, nil
}
+// buildLikePredicate is a helper for LIKE predicates that take
+// (table, column, pattern, escape, case_sensitive). escape 0 selects the
+// default escape character.
+func (pb *PredicateBuilder) buildLikePredicate(
+ ffiVar *FFI[func(*paimonTable, *byte, paimonDatumC, int8, bool)
(*paimonPredicate, error)],
+ column string, pattern Datum,
+) (*Predicate, error) {
+ t := pb.table
+ if t.inner == nil {
+ return nil, ErrClosed
+ }
+ cCol := append([]byte(column), 0)
+ inner, err := ffiVar.symbol(t.ctx)(t.inner, &cCol[0], pattern.inner, 0,
pb.caseSensitive)
+ runtime.KeepAlive(cCol)
+ runtime.KeepAlive(pattern)
+ if err != nil {
+ return nil, err
+ }
+ t.lib.acquire()
+ return &Predicate{ctx: t.ctx, lib: t.lib, inner: inner}, nil
+}
+
+// buildBetweenPredicate is a helper for BETWEEN / NOT BETWEEN predicates that
+// take (table, column, low, high, case_sensitive).
+func (pb *PredicateBuilder) buildBetweenPredicate(
+ ffiVar *FFI[func(*paimonTable, *byte, paimonDatumC, paimonDatumC, bool)
(*paimonPredicate, error)],
+ column string, low, high any,
+) (*Predicate, error) {
+ t := pb.table
+ if t.inner == nil {
+ return nil, ErrClosed
+ }
+ lowDatum, err := toDatum(low)
+ if err != nil {
+ return nil, err
+ }
+ highDatum, err := toDatum(high)
+ if err != nil {
+ return nil, err
+ }
+ cCol := append([]byte(column), 0)
+ inner, err := ffiVar.symbol(t.ctx)(
+ t.inner, &cCol[0], lowDatum.inner, highDatum.inner,
pb.caseSensitive,
+ )
+ runtime.KeepAlive(cCol)
+ runtime.KeepAlive(lowDatum)
+ runtime.KeepAlive(highDatum)
+ 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,
@@ -496,6 +584,9 @@ var ffiPredicateLessThan =
newPredicateLeafFFI("paimon_predicate_less_than_with_
var ffiPredicateLessOrEqual =
newPredicateLeafFFI("paimon_predicate_less_or_equal_with_case_sensitive")
var ffiPredicateGreaterThan =
newPredicateLeafFFI("paimon_predicate_greater_than_with_case_sensitive")
var ffiPredicateGreaterOrEqual =
newPredicateLeafFFI("paimon_predicate_greater_or_equal_with_case_sensitive")
+var ffiPredicateStartsWith =
newPredicateLeafFFI("paimon_predicate_starts_with_with_case_sensitive")
+var ffiPredicateEndsWith =
newPredicateLeafFFI("paimon_predicate_ends_with_with_case_sensitive")
+var ffiPredicateContains =
newPredicateLeafFFI("paimon_predicate_contains_with_case_sensitive")
// newPredicateLeafFFI wraps the (table, column, datum, case_sensitive) form.
func newPredicateLeafFFI(
@@ -528,6 +619,41 @@ func newPredicateLeafFFI(
})
}
+var ffiPredicateLike =
newPredicateLikeFFI("paimon_predicate_like_with_case_sensitive")
+
+// newPredicateLikeFFI wraps the (table, column, pattern, escape,
case_sensitive)
+// form. escape is a C char; 0 selects the default escape character.
+func newPredicateLikeFFI(
+ sym string,
+) *FFI[func(*paimonTable, *byte, paimonDatumC, int8, bool) (*paimonPredicate,
error)] {
+ return newFFI(ffiOpts{
+ sym: contextKey(sym),
+ rType: &typeResultPredicate,
+ aTypes: []*ffi.Type{
+ &ffi.TypePointer, &ffi.TypePointer, &typePaimonDatum,
&ffi.TypeSint8, &ffi.TypeUint8,
+ },
+ }, func(ctx context.Context, ffiCall ffiCall) func(*paimonTable, *byte,
paimonDatumC, int8, bool) (*paimonPredicate, error) {
+ return func(
+ table *paimonTable, column *byte, pattern paimonDatumC,
escape int8, caseSensitive bool,
+ ) (*paimonPredicate, error) {
+ flag := boolByte(caseSensitive)
+ var result resultPredicate
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&table),
+ unsafe.Pointer(&column),
+ unsafe.Pointer(&pattern),
+ unsafe.Pointer(&escape),
+ unsafe.Pointer(&flag),
+ )
+ if result.error != nil {
+ return nil, parseError(ctx, result.error)
+ }
+ return result.predicate, nil
+ }
+ })
+}
+
var ffiPredicateIsNull =
newPredicateNullFFI("paimon_predicate_is_null_with_case_sensitive")
var ffiPredicateIsNotNull =
newPredicateNullFFI("paimon_predicate_is_not_null_with_case_sensitive")
@@ -593,6 +719,42 @@ func newPredicateInFFI(
})
}
+var ffiPredicateBetween =
newPredicateBetweenFFI("paimon_predicate_between_with_case_sensitive")
+var ffiPredicateNotBetween =
newPredicateBetweenFFI("paimon_predicate_not_between_with_case_sensitive")
+
+// newPredicateBetweenFFI wraps the (table, column, low, high, case_sensitive)
+// form shared by BETWEEN and NOT BETWEEN.
+func newPredicateBetweenFFI(
+ sym string,
+) *FFI[func(*paimonTable, *byte, paimonDatumC, paimonDatumC, bool)
(*paimonPredicate, error)] {
+ return newFFI(ffiOpts{
+ sym: contextKey(sym),
+ rType: &typeResultPredicate,
+ aTypes: []*ffi.Type{
+ &ffi.TypePointer, &ffi.TypePointer, &typePaimonDatum,
&typePaimonDatum, &ffi.TypeUint8,
+ },
+ }, func(ctx context.Context, ffiCall ffiCall) func(*paimonTable, *byte,
paimonDatumC, paimonDatumC, bool) (*paimonPredicate, error) {
+ return func(
+ table *paimonTable, column *byte, low paimonDatumC,
high paimonDatumC, caseSensitive bool,
+ ) (*paimonPredicate, error) {
+ flag := boolByte(caseSensitive)
+ var result resultPredicate
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&table),
+ unsafe.Pointer(&column),
+ unsafe.Pointer(&low),
+ unsafe.Pointer(&high),
+ unsafe.Pointer(&flag),
+ )
+ 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,
diff --git a/bindings/go/tests/paimon_test.go b/bindings/go/tests/paimon_test.go
index 16968094..2b6a3651 100644
--- a/bindings/go/tests/paimon_test.go
+++ b/bindings/go/tests/paimon_test.go
@@ -1080,3 +1080,50 @@ func TestPredicateBuilderCaseSensitivity(t *testing.T) {
})
}
}
+
+// TestPredicateBuilderStringAndRange exercises the string and range predicates
+// (StartsWith / EndsWith / Contains / Like / Between / NotBetween) added for
+// parity with the C and Python bindings. simple_log_table holds
+// (1,'alice'), (2,'bob'), (3,'carol').
+func TestPredicateBuilderStringAndRange(t *testing.T) {
+ table := openTestTable(t)
+ pb := table.PredicateBuilder()
+
+ for _, tc := range []struct {
+ name string
+ build func() (*paimon.Predicate, error)
+ want []int32
+ }{
+ {"StartsWith", func() (*paimon.Predicate, error) { return
pb.StartsWith("name", "a") }, []int32{1}},
+ {"EndsWith", func() (*paimon.Predicate, error) { return
pb.EndsWith("name", "b") }, []int32{2}},
+ {"Contains", func() (*paimon.Predicate, error) { return
pb.Contains("name", "ro") }, []int32{3}},
+ {"Like", func() (*paimon.Predicate, error) { return
pb.Like("name", "b%") }, []int32{2}},
+ {"Between", func() (*paimon.Predicate, error) { return
pb.Between("id", int32(1), int32(2)) }, []int32{1, 2}},
+ {"NotBetween", func() (*paimon.Predicate, error) { return
pb.NotBetween("id", int32(1), int32(2)) }, []int32{3}},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ pred, err := tc.build()
+ if err != nil {
+ t.Fatalf("Failed to create predicate: %v", err)
+ }
+ defer pred.Close()
+
+ rb, err := table.NewReadBuilder()
+ if err != nil {
+ t.Fatalf("Failed to create read builder: %v",
err)
+ }
+ defer rb.Close()
+ if err := rb.WithFilter(pred); err != nil {
+ t.Fatalf("WithFilter failed: %v", err)
+ }
+ var ids []int32
+ for _, r := range readRows(t, rb) {
+ ids = append(ids, r.id)
+ }
+ sort.Slice(ids, func(i, j int) bool { return ids[i] <
ids[j] })
+ if !reflect.DeepEqual(ids, tc.want) {
+ t.Fatalf("Expected IDs %v, got %v", tc.want,
ids)
+ }
+ })
+ }
+}