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 ce13e8f9 feat(c,go): list databases and tables through the C and Go
bindings (#1024)
ce13e8f9 is described below
commit ce13e8f945e95fa8bdd92508725a126c0431c424
Author: jackylee <[email protected]>
AuthorDate: Sun Oct 4 22:20:32 2026 +0800
feat(c,go): list databases and tables through the C and Go bindings (#1024)
---
bindings/c/src/catalog.rs | 81 ++++++++++++++++++++++++++++++++++-
bindings/c/src/result.rs | 6 +++
bindings/c/src/tests.rs | 57 ++++++++++++++++++++++++
bindings/c/src/types.rs | 50 +++++++++++++++++++++
bindings/go/catalog.go | 71 ++++++++++++++++++++++++++++++
bindings/go/tests/catalog_tag_test.go | 35 +++++++++++++++
bindings/go/types.go | 57 ++++++++++++++++++++++++
7 files changed, 355 insertions(+), 2 deletions(-)
diff --git a/bindings/c/src/catalog.rs b/bindings/c/src/catalog.rs
index 49c2fb48..844119da 100644
--- a/bindings/c/src/catalog.rs
+++ b/bindings/c/src/catalog.rs
@@ -22,9 +22,15 @@ use paimon::catalog::Identifier;
use paimon::{Catalog, CatalogFactory, Options};
use crate::error::{check_non_null, paimon_error, validate_cstr,
PaimonErrorCode};
-use crate::result::{paimon_result_catalog_new, paimon_result_get_table,
paimon_result_get_tag};
+use crate::result::{
+ paimon_result_catalog_new, paimon_result_get_table, paimon_result_get_tag,
+ paimon_result_string_list,
+};
use crate::runtime;
-use crate::types::{paimon_bytes, paimon_catalog, paimon_identifier,
paimon_option, paimon_table};
+use crate::types::{
+ paimon_bytes, paimon_catalog, paimon_identifier, paimon_option,
paimon_string_list,
+ paimon_table,
+};
/// Create a catalog using CatalogFactory with the given options.
///
@@ -137,6 +143,77 @@ pub unsafe extern "C" fn paimon_catalog_get_table(
}
}
+/// List the databases in a catalog.
+///
+/// On success `error` is null and `string_list` owns the database names; free
it
+/// with `paimon_string_list_free`. On error `string_list` is empty.
+///
+/// # Safety
+/// `catalog` must be a valid pointer from a previous paimon C call, or null
(returns error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_catalog_list_databases(
+ catalog: *const paimon_catalog,
+) -> paimon_result_string_list {
+ if let Err(error) = check_non_null(catalog, "catalog") {
+ return paimon_result_string_list {
+ string_list: paimon_string_list::empty(),
+ error,
+ };
+ }
+ let catalog = &*((*catalog).inner as *const Arc<dyn Catalog>);
+ match runtime().block_on(catalog.list_databases()) {
+ Ok(names) => paimon_result_string_list {
+ string_list: paimon_string_list::from_strings(names),
+ error: std::ptr::null_mut(),
+ },
+ Err(error) => paimon_result_string_list {
+ string_list: paimon_string_list::empty(),
+ error: paimon_error::from_paimon(error),
+ },
+ }
+}
+
+/// List the tables in a database.
+///
+/// On success `error` is null and `string_list` owns the table names; free it
+/// with `paimon_string_list_free`. On error `string_list` is empty.
+///
+/// # Safety
+/// `catalog` must be a valid pointer from a previous paimon C call, or null
(returns error).
+/// `database` must be a valid null-terminated C string.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_catalog_list_tables(
+ catalog: *const paimon_catalog,
+ database: *const c_char,
+) -> paimon_result_string_list {
+ if let Err(error) = check_non_null(catalog, "catalog") {
+ return paimon_result_string_list {
+ string_list: paimon_string_list::empty(),
+ error,
+ };
+ }
+ let database = match validate_cstr(database, "database") {
+ Ok(database) => database,
+ Err(error) => {
+ return paimon_result_string_list {
+ string_list: paimon_string_list::empty(),
+ error,
+ }
+ }
+ };
+ let catalog = &*((*catalog).inner as *const Arc<dyn Catalog>);
+ match runtime().block_on(catalog.list_tables(&database)) {
+ Ok(names) => paimon_result_string_list {
+ string_list: paimon_string_list::from_strings(names),
+ error: std::ptr::null_mut(),
+ },
+ Err(error) => paimon_result_string_list {
+ string_list: paimon_string_list::empty(),
+ error: paimon_error::from_paimon(error),
+ },
+ }
+}
+
/// Create a tag for a snapshot, or the latest snapshot when `snapshot_id` is
null.
///
/// # Safety
diff --git a/bindings/c/src/result.rs b/bindings/c/src/result.rs
index 8572bf2d..bf1f0c4b 100644
--- a/bindings/c/src/result.rs
+++ b/bindings/c/src/result.rs
@@ -78,6 +78,12 @@ pub struct paimon_result_get_tag {
pub error: *mut paimon_error,
}
+#[repr(C)]
+pub struct paimon_result_string_list {
+ pub string_list: paimon_string_list,
+ pub error: *mut paimon_error,
+}
+
#[repr(C)]
pub struct paimon_result_latest_snapshot {
pub snapshot: paimon_bytes,
diff --git a/bindings/c/src/tests.rs b/bindings/c/src/tests.rs
index 22bba81f..d839a3ad 100644
--- a/bindings/c/src/tests.rs
+++ b/bindings/c/src/tests.rs
@@ -1004,6 +1004,63 @@ fn
test_table_from_schema_json_rejects_invalid_identifier() {
// Catalog tag tests
// =========================================================================
+#[test]
+fn test_catalog_list_databases_and_tables() {
+ let temp_dir = tempfile::tempdir().unwrap();
+ let mut options = Options::new();
+ options.set("warehouse", temp_dir.path().to_string_lossy());
+ let catalog = FileSystemCatalog::new(options).unwrap();
+ crate::runtime().block_on(async {
+ catalog
+ .create_database("default", false, HashMap::new())
+ .await
+ .unwrap();
+ catalog
+ .create_database("analytics", false, HashMap::new())
+ .await
+ .unwrap();
+ for table in ["orders", "customers"] {
+ catalog
+ .create_table(&Identifier::new("default", table),
simple_schema(), false)
+ .await
+ .unwrap();
+ }
+ });
+ unsafe {
+ let collect = |list: &paimon_string_list| -> Vec<String> {
+ (0..list.len)
+ .map(|i| {
+ std::ffi::CStr::from_ptr(*list.items.add(i))
+ .to_str()
+ .unwrap()
+ .to_string()
+ })
+ .collect()
+ };
+ let catalog = wrap_catalog(Arc::new(catalog));
+
+ let dbs = paimon_catalog_list_databases(catalog);
+ assert!(dbs.error.is_null());
+ let db_names = collect(&dbs.string_list);
+ assert!(db_names.contains(&"default".to_string()));
+ assert!(db_names.contains(&"analytics".to_string()));
+ paimon_string_list_free(dbs.string_list);
+
+ let db = CString::new("default").unwrap();
+ let tables = paimon_catalog_list_tables(catalog, db.as_ptr());
+ assert!(tables.error.is_null());
+ let mut table_names = collect(&tables.string_list);
+ table_names.sort();
+ assert_eq!(
+ table_names,
+ vec!["customers".to_string(), "orders".to_string()]
+ );
+ paimon_string_list_free(tables.string_list);
+
+ paimon_catalog_free(catalog);
+ }
+}
+
#[test]
fn test_catalog_tag_lifecycle() {
let temp_dir = tempfile::tempdir().unwrap();
diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs
index bedd0a66..90fe6670 100644
--- a/bindings/c/src/types.rs
+++ b/bindings/c/src/types.rs
@@ -70,6 +70,56 @@ pub unsafe extern "C" fn paimon_bytes_free(bytes:
paimon_bytes) {
}
}
+/// C-compatible list of owned, null-terminated C strings.
+///
+/// `items` points to `len` string pointers. Free the whole list, including
+/// every string, with [`paimon_string_list_free`].
+#[repr(C)]
+pub struct paimon_string_list {
+ pub items: *mut *mut std::ffi::c_char,
+ pub len: usize,
+}
+
+impl paimon_string_list {
+ /// An empty list, used for error returns.
+ pub(crate) fn empty() -> Self {
+ Self {
+ items: std::ptr::null_mut(),
+ len: 0,
+ }
+ }
+
+ /// Allocate a `paimon_string_list` from owned Rust strings. A string with
an
+ /// interior NUL (impossible for a validated catalog name) becomes empty
+ /// rather than aborting the whole call.
+ pub(crate) fn from_strings(strings: Vec<String>) -> Self {
+ let ptrs: Vec<*mut std::ffi::c_char> = strings
+ .into_iter()
+ .map(|s| std::ffi::CString::new(s).unwrap_or_default().into_raw())
+ .collect();
+ let len = ptrs.len();
+ let items = Box::into_raw(ptrs.into_boxed_slice()) as *mut *mut
std::ffi::c_char;
+ Self { items, len }
+ }
+}
+
+/// Free a paimon_string_list and every string it owns.
+///
+/// # Safety
+/// Only call with a list returned from a paimon C function, exactly once.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_string_list_free(list: paimon_string_list) {
+ if list.items.is_null() {
+ return;
+ }
+ let boxed = Box::from_raw(std::ptr::slice_from_raw_parts_mut(list.items,
list.len));
+ for &ptr in boxed.iter() {
+ if !ptr.is_null() {
+ drop(std::ffi::CString::from_raw(ptr));
+ }
+ }
+}
+
#[repr(C)]
#[derive(Clone, Copy)]
pub struct paimon_byte_slice {
diff --git a/bindings/go/catalog.go b/bindings/go/catalog.go
index b813a7ca..cadde62c 100644
--- a/bindings/go/catalog.go
+++ b/bindings/go/catalog.go
@@ -21,6 +21,7 @@ package paimon
import (
"context"
+ "runtime"
"sync"
"unsafe"
@@ -91,6 +92,22 @@ func (c *Catalog) newIdentifier(id Identifier)
(*paimonIdentifier, error) {
return ffiIdentifierNew.symbol(c.ctx)(id.database, id.object)
}
+// ListDatabases returns the names of all databases in the catalog.
+func (c *Catalog) ListDatabases() ([]string, error) {
+ if c.inner == nil {
+ return nil, ErrClosed
+ }
+ return ffiCatalogListDatabases.symbol(c.ctx)(c.inner)
+}
+
+// ListTables returns the names of all tables in the given database.
+func (c *Catalog) ListTables(database string) ([]string, error) {
+ if c.inner == nil {
+ return nil, ErrClosed
+ }
+ return ffiCatalogListTables.symbol(c.ctx)(c.inner, database)
+}
+
var ffiCatalogCreate = newFFI(ffiOpts{
sym: "paimon_catalog_create",
rType: &typeResultCatalogNew,
@@ -205,3 +222,57 @@ var ffiCatalogGetTable = newFFI(ffiOpts{
return result.table, nil
}
})
+
+var ffiCatalogListDatabases = newFFI(ffiOpts{
+ sym: "paimon_catalog_list_databases",
+ rType: &typeResultStringList,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(*paimonCatalog) ([]string,
error) {
+ return func(catalog *paimonCatalog) ([]string, error) {
+ var result resultStringList
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&catalog),
+ )
+ if result.error != nil {
+ return nil, parseError(ctx, result.error)
+ }
+ defer ffiStringListFree.symbol(ctx)(result.stringList)
+ return parseStringList(result.stringList), nil
+ }
+})
+
+var ffiCatalogListTables = newFFI(ffiOpts{
+ sym: "paimon_catalog_list_tables",
+ rType: &typeResultStringList,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(*paimonCatalog, string)
([]string, error) {
+ return func(catalog *paimonCatalog, database string) ([]string, error) {
+ dbPtr, err := bytePtrFromString(database)
+ if err != nil {
+ return nil, err
+ }
+ var result resultStringList
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&catalog),
+ unsafe.Pointer(&dbPtr),
+ )
+ runtime.KeepAlive(dbPtr)
+ if result.error != nil {
+ return nil, parseError(ctx, result.error)
+ }
+ defer ffiStringListFree.symbol(ctx)(result.stringList)
+ return parseStringList(result.stringList), nil
+ }
+})
+
+var ffiStringListFree = newFFI(ffiOpts{
+ sym: "paimon_string_list_free",
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&typePaimonStringList},
+}, func(_ context.Context, ffiCall ffiCall) func(paimonStringList) {
+ return func(list paimonStringList) {
+ ffiCall(nil, unsafe.Pointer(&list))
+ }
+})
diff --git a/bindings/go/tests/catalog_tag_test.go
b/bindings/go/tests/catalog_tag_test.go
index 0a84beb7..112c17e4 100644
--- a/bindings/go/tests/catalog_tag_test.go
+++ b/bindings/go/tests/catalog_tag_test.go
@@ -22,6 +22,7 @@ package paimon_test
import (
"errors"
"path/filepath"
+ "slices"
"testing"
paimon "github.com/apache/paimon-rust/bindings/go"
@@ -108,3 +109,37 @@ func TestCatalogTagLifecycle(t *testing.T) {
t.Fatalf("closed catalog error = %v", err)
}
}
+
+func TestCatalogListDatabasesAndTables(t *testing.T) {
+ 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)
+ }
+ catalog, err := paimon.NewCatalog(map[string]string{"warehouse":
warehouse})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer catalog.Close()
+
+ databases, err := catalog.ListDatabases()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !slices.Contains(databases, "default") {
+ t.Fatalf("ListDatabases = %v, want it to contain \"default\"",
databases)
+ }
+
+ tables, err := catalog.ListTables("default")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !slices.Contains(tables, "map_blob_table") {
+ t.Fatalf("ListTables(\"default\") = %v, want it to contain
\"map_blob_table\"", tables)
+ }
+
+ catalog.Close()
+ if _, err := catalog.ListTables("default"); !errors.Is(err,
paimon.ErrClosed) {
+ t.Fatalf("closed catalog ListTables error = %v", err)
+ }
+}
diff --git a/bindings/go/types.go b/bindings/go/types.go
index 21be0b79..fba5ac60 100644
--- a/bindings/go/types.go
+++ b/bindings/go/types.go
@@ -92,6 +92,27 @@ var (
}[0],
}
+ // paimon_string_list { items: **c_char, len: usize }
+ typePaimonStringList = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ // paimon_result_string_list { string_list: paimon_string_list, error:
*paimon_error }
+ typeResultStringList = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
typeResultReadBlobs = ffi.Type{
Type: ffi.Struct,
Elements: &[]*ffi.Type{
@@ -426,6 +447,16 @@ type resultGetTag struct {
error *paimonError
}
+type paimonStringList struct {
+ items *unsafe.Pointer
+ len uintptr
+}
+
+type resultStringList struct {
+ stringList paimonStringList
+ error *paimonError
+}
+
type resultLatestSnapshot struct {
snapshot paimonBytes
error *paimonError
@@ -552,3 +583,29 @@ func parseBytes(b paimonBytes) []byte {
copy(data, unsafe.Slice(b.data, b.len))
return data
}
+
+// parseStringList copies a paimon_string_list into a Go slice. It does not
free
+// the list; the caller frees it with paimon_string_list_free.
+func parseStringList(list paimonStringList) []string {
+ if list.len == 0 || list.items == nil {
+ return nil
+ }
+ ptrs := unsafe.Slice(list.items, list.len)
+ out := make([]string, len(ptrs))
+ for i, p := range ptrs {
+ out[i] = goStringFromCString((*byte)(p))
+ }
+ return out
+}
+
+// goStringFromCString copies a NUL-terminated C string into a Go string.
+func goStringFromCString(p *byte) string {
+ if p == nil {
+ return ""
+ }
+ var n int
+ for *(*byte)(unsafe.Add(unsafe.Pointer(p), uintptr(n))) != 0 {
+ n++
+ }
+ return string(unsafe.Slice(p, n))
+}