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))
+}

Reply via email to