This is an automated email from the ASF dual-hosted git repository.
yuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 86e8e24 feat: introduce database level ops for cpp bindings (#286)
86e8e24 is described below
commit 86e8e241bc4101c322811837fe72ddb46342f9cf
Author: yuxia Luo <[email protected]>
AuthorDate: Sun Feb 8 20:21:19 2026 +0800
feat: introduce database level ops for cpp bindings (#286)
---
bindings/cpp/BUILD.bazel | 33 +++++
bindings/cpp/CMakeLists.txt | 6 +
bindings/cpp/examples/admin_example.cpp | 121 +++++++++++++++++++
bindings/cpp/include/fluss.hpp | 36 ++++++
bindings/cpp/src/admin.cpp | 120 ++++++++++++++++++
bindings/cpp/src/ffi_converter.hpp | 25 ++++
bindings/cpp/src/lib.rs | 208 ++++++++++++++++++++++++++++++++
bindings/cpp/src/types.rs | 43 +++++++
8 files changed, 592 insertions(+)
diff --git a/bindings/cpp/BUILD.bazel b/bindings/cpp/BUILD.bazel
index 81d483c..aff8f50 100644
--- a/bindings/cpp/BUILD.bazel
+++ b/bindings/cpp/BUILD.bazel
@@ -340,3 +340,36 @@ cc_binary(
visibility = ["//visibility:public"],
)
+cc_binary(
+ name = "fluss_cpp_admin_example",
+ srcs = [
+ "examples/admin_example.cpp",
+ ],
+ deps = [":fluss_cpp"],
+ copts = [
+ "-std=c++17",
+ ] + select({
+ ":debug_mode": [
+ "-g3",
+ "-O0",
+ "-ggdb",
+ "-fno-omit-frame-pointer",
+ "-DDEBUG",
+ ],
+ ":fastbuild_mode": [
+ "-g",
+ "-O0",
+ ],
+ ":release_mode": [
+ "-O2",
+ "-DNDEBUG",
+ ],
+ }),
+ linkopts = select({
+ ":debug_mode": ["-g"],
+ ":fastbuild_mode": ["-g"],
+ ":release_mode": [],
+ }),
+ visibility = ["//visibility:public"],
+)
+
diff --git a/bindings/cpp/CMakeLists.txt b/bindings/cpp/CMakeLists.txt
index 93cfc41..ae70842 100644
--- a/bindings/cpp/CMakeLists.txt
+++ b/bindings/cpp/CMakeLists.txt
@@ -102,6 +102,12 @@ target_link_libraries(fluss_cpp_example PRIVATE
Arrow::arrow_shared)
target_compile_definitions(fluss_cpp_example PRIVATE ARROW_FOUND)
target_include_directories(fluss_cpp_example PUBLIC ${CPP_INCLUDE_DIR})
+add_executable(fluss_cpp_admin_example examples/admin_example.cpp)
+target_link_libraries(fluss_cpp_admin_example PRIVATE fluss_cpp)
+target_link_libraries(fluss_cpp_admin_example PRIVATE Arrow::arrow_shared)
+target_compile_definitions(fluss_cpp_admin_example PRIVATE ARROW_FOUND)
+target_include_directories(fluss_cpp_admin_example PUBLIC ${CPP_INCLUDE_DIR})
+
set_target_properties(fluss_cpp
PROPERTIES ADDITIONAL_CLEAN_FILES ${CARGO_TARGET_DIR}
)
diff --git a/bindings/cpp/examples/admin_example.cpp
b/bindings/cpp/examples/admin_example.cpp
new file mode 100644
index 0000000..7b7a333
--- /dev/null
+++ b/bindings/cpp/examples/admin_example.cpp
@@ -0,0 +1,121 @@
+// 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.
+
+#include <iostream>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "fluss.hpp"
+
+static void check(const char* step, const fluss::Result& r) {
+ if (!r.Ok()) {
+ std::cerr << step << " failed: code=" << r.error_code << " msg=" <<
r.error_message
+ << std::endl;
+ std::exit(1);
+ }
+}
+
+int main() {
+ const std::string bootstrap = "127.0.0.1:9123";
+ const std::string db_name = "admin_example_db";
+ const std::string table_name = "admin_example_table";
+
+ // 1) Connect and get Admin
+ fluss::Connection conn;
+ check("connect", fluss::Connection::Connect(bootstrap, conn));
+
+ fluss::Admin admin;
+ check("get_admin", conn.GetAdmin(admin));
+
+ // 2) Database operations
+ std::cout << "--- Database operations ---" << std::endl;
+
+ bool exists = false;
+ check("database_exists (before create)", admin.DatabaseExists(db_name,
exists));
+ std::cout << "Database " << db_name << " exists before create: " <<
(exists ? "yes" : "no")
+ << std::endl;
+
+ fluss::DatabaseDescriptor db_desc;
+ db_desc.comment = "Example database for Admin API";
+ db_desc.properties["owner"] = "admin_example";
+ check("create_database", admin.CreateDatabase(db_name, db_desc, true));
+
+ check("database_exists (after create)", admin.DatabaseExists(db_name,
exists));
+ std::cout << "Database " << db_name << " exists after create: " << (exists
? "yes" : "no")
+ << std::endl;
+
+ fluss::DatabaseInfo db_info;
+ check("get_database_info", admin.GetDatabaseInfo(db_name, db_info));
+ std::cout << "Database info: name=" << db_info.database_name
+ << " comment=" << db_info.comment << " created_time=" <<
db_info.created_time
+ << std::endl;
+
+ std::vector<std::string> databases;
+ check("list_databases", admin.ListDatabases(databases));
+ std::cout << "List databases (" << databases.size() << "): ";
+ for (size_t i = 0; i < databases.size(); ++i) {
+ if (i > 0) std::cout << ", ";
+ std::cout << databases[i];
+ }
+ std::cout << std::endl;
+
+ // 3) Table operations in the new database
+ std::cout << "--- Table operations ---" << std::endl;
+
+ fluss::TablePath table_path(db_name, table_name);
+
+ bool table_exists_flag = false;
+ check("table_exists (before create)", admin.TableExists(table_path,
table_exists_flag));
+ std::cout << "Table " << db_name << "." << table_name
+ << " exists before create: " << (table_exists_flag ? "yes" :
"no") << std::endl;
+
+ auto schema = fluss::Schema::NewBuilder()
+ .AddColumn("id", fluss::DataType::Int())
+ .AddColumn("name", fluss::DataType::String())
+ .Build();
+ auto descriptor = fluss::TableDescriptor::NewBuilder()
+ .SetSchema(schema)
+ .SetBucketCount(1)
+ .SetComment("admin example table")
+ .Build();
+
+ check("create_table", admin.CreateTable(table_path, descriptor, true));
+
+ check("table_exists (after create)", admin.TableExists(table_path,
table_exists_flag));
+ std::cout << "Table exists after create: " << (table_exists_flag ? "yes" :
"no") << std::endl;
+
+ std::vector<std::string> tables;
+ check("list_tables", admin.ListTables(db_name, tables));
+ std::cout << "List tables in " << db_name << " (" << tables.size() << "):
";
+ for (size_t i = 0; i < tables.size(); ++i) {
+ if (i > 0) std::cout << ", ";
+ std::cout << tables[i];
+ }
+ std::cout << std::endl;
+
+ // 4) Cleanup: drop table, then drop database
+ std::cout << "--- Cleanup ---" << std::endl;
+ check("drop_table", admin.DropTable(table_path, true));
+ check("drop_database", admin.DropDatabase(db_name, true, true));
+
+ check("database_exists (after drop)", admin.DatabaseExists(db_name,
exists));
+ std::cout << "Database exists after drop: " << (exists ? "yes" : "no") <<
std::endl;
+
+ std::cout << "Admin example completed successfully." << std::endl;
+ return 0;
+}
diff --git a/bindings/cpp/include/fluss.hpp b/bindings/cpp/include/fluss.hpp
index 6b9d479..50dffae 100644
--- a/bindings/cpp/include/fluss.hpp
+++ b/bindings/cpp/include/fluss.hpp
@@ -709,6 +709,21 @@ struct PartitionInfo {
std::string partition_name;
};
+/// Descriptor for create_database (optional). Leave comment and properties
empty for default.
+struct DatabaseDescriptor {
+ std::string comment;
+ std::unordered_map<std::string, std::string> properties;
+};
+
+/// Metadata returned by GetDatabaseInfo.
+struct DatabaseInfo {
+ std::string database_name;
+ std::string comment;
+ std::unordered_map<std::string, std::string> properties;
+ int64_t created_time{0};
+ int64_t modified_time{0};
+};
+
class AppendWriter;
class WriteResult;
class LogScanner;
@@ -773,6 +788,27 @@ class Admin {
const std::unordered_map<std::string, std::string>&
partition_spec,
bool ignore_if_exists = false);
+ Result DropPartition(const TablePath& table_path,
+ const std::unordered_map<std::string, std::string>&
partition_spec,
+ bool ignore_if_not_exists = false);
+
+ Result CreateDatabase(const std::string& database_name,
+ const DatabaseDescriptor& descriptor,
+ bool ignore_if_exists = false);
+
+ Result DropDatabase(const std::string& database_name, bool
ignore_if_not_exists = false,
+ bool cascade = true);
+
+ Result ListDatabases(std::vector<std::string>& out);
+
+ Result DatabaseExists(const std::string& database_name, bool& out);
+
+ Result GetDatabaseInfo(const std::string& database_name, DatabaseInfo&
out);
+
+ Result ListTables(const std::string& database_name,
std::vector<std::string>& out);
+
+ Result TableExists(const TablePath& table_path, bool& out);
+
private:
Result DoListOffsets(const TablePath& table_path, const
std::vector<int32_t>& bucket_ids,
const OffsetQuery& offset_query,
std::unordered_map<int32_t, int64_t>& out,
diff --git a/bindings/cpp/src/admin.cpp b/bindings/cpp/src/admin.cpp
index 4aed78d..77c95d3 100644
--- a/bindings/cpp/src/admin.cpp
+++ b/bindings/cpp/src/admin.cpp
@@ -204,4 +204,124 @@ Result Admin::CreatePartition(const TablePath& table_path,
return utils::from_ffi_result(ffi_result);
}
+Result Admin::DropPartition(const TablePath& table_path,
+ const std::unordered_map<std::string,
std::string>& partition_spec,
+ bool ignore_if_not_exists) {
+ if (!Available()) {
+ return utils::make_error(1, "Admin not available");
+ }
+
+ auto ffi_path = utils::to_ffi_table_path(table_path);
+
+ rust::Vec<ffi::FfiPartitionKeyValue> rust_spec;
+ for (const auto& [key, value] : partition_spec) {
+ ffi::FfiPartitionKeyValue kv;
+ kv.key = rust::String(key);
+ kv.value = rust::String(value);
+ rust_spec.push_back(std::move(kv));
+ }
+
+ auto ffi_result =
+ admin_->drop_partition(ffi_path, std::move(rust_spec),
ignore_if_not_exists);
+ return utils::from_ffi_result(ffi_result);
+}
+
+Result Admin::CreateDatabase(const std::string& database_name,
+ const DatabaseDescriptor& descriptor,
+ bool ignore_if_exists) {
+ if (!Available()) {
+ return utils::make_error(1, "Admin not available");
+ }
+
+ auto ffi_desc = utils::to_ffi_database_descriptor(descriptor);
+ auto ffi_result =
+ admin_->create_database(rust::Str(database_name), ffi_desc,
ignore_if_exists);
+ return utils::from_ffi_result(ffi_result);
+}
+
+Result Admin::DropDatabase(const std::string& database_name, bool
ignore_if_not_exists,
+ bool cascade) {
+ if (!Available()) {
+ return utils::make_error(1, "Admin not available");
+ }
+
+ auto ffi_result =
+ admin_->drop_database(rust::Str(database_name), ignore_if_not_exists,
cascade);
+ return utils::from_ffi_result(ffi_result);
+}
+
+Result Admin::ListDatabases(std::vector<std::string>& out) {
+ if (!Available()) {
+ return utils::make_error(1, "Admin not available");
+ }
+
+ auto ffi_result = admin_->list_databases();
+ auto result = utils::from_ffi_result(ffi_result.result);
+ if (result.Ok()) {
+ out.clear();
+ out.reserve(ffi_result.database_names.size());
+ for (const auto& name : ffi_result.database_names) {
+ out.push_back(std::string(name));
+ }
+ }
+ return result;
+}
+
+Result Admin::DatabaseExists(const std::string& database_name, bool& out) {
+ if (!Available()) {
+ return utils::make_error(1, "Admin not available");
+ }
+
+ auto ffi_result = admin_->database_exists(rust::Str(database_name));
+ auto result = utils::from_ffi_result(ffi_result.result);
+ if (result.Ok()) {
+ out = ffi_result.value;
+ }
+ return result;
+}
+
+Result Admin::GetDatabaseInfo(const std::string& database_name, DatabaseInfo&
out) {
+ if (!Available()) {
+ return utils::make_error(1, "Admin not available");
+ }
+
+ auto ffi_result = admin_->get_database_info(rust::Str(database_name));
+ auto result = utils::from_ffi_result(ffi_result.result);
+ if (result.Ok()) {
+ out = utils::from_ffi_database_info(ffi_result.database_info);
+ }
+ return result;
+}
+
+Result Admin::ListTables(const std::string& database_name,
std::vector<std::string>& out) {
+ if (!Available()) {
+ return utils::make_error(1, "Admin not available");
+ }
+
+ auto ffi_result = admin_->list_tables(rust::Str(database_name));
+ auto result = utils::from_ffi_result(ffi_result.result);
+ if (result.Ok()) {
+ out.clear();
+ out.reserve(ffi_result.table_names.size());
+ for (const auto& name : ffi_result.table_names) {
+ out.push_back(std::string(name));
+ }
+ }
+ return result;
+}
+
+Result Admin::TableExists(const TablePath& table_path, bool& out) {
+ if (!Available()) {
+ return utils::make_error(1, "Admin not available");
+ }
+
+ auto ffi_path = utils::to_ffi_table_path(table_path);
+ auto ffi_result = admin_->table_exists(ffi_path);
+ auto result = utils::from_ffi_result(ffi_result.result);
+ if (result.Ok()) {
+ out = ffi_result.value;
+ }
+ return result;
+}
+
} // namespace fluss
diff --git a/bindings/cpp/src/ffi_converter.hpp
b/bindings/cpp/src/ffi_converter.hpp
index 8adcd01..8fc8415 100644
--- a/bindings/cpp/src/ffi_converter.hpp
+++ b/bindings/cpp/src/ffi_converter.hpp
@@ -290,5 +290,30 @@ inline LakeSnapshot from_ffi_lake_snapshot(const
ffi::FfiLakeSnapshot& ffi_snaps
return snapshot;
}
+inline ffi::FfiDatabaseDescriptor to_ffi_database_descriptor(
+ const DatabaseDescriptor& desc) {
+ ffi::FfiDatabaseDescriptor ffi_desc;
+ ffi_desc.comment = rust::String(desc.comment);
+ for (const auto& [k, v] : desc.properties) {
+ ffi::HashMapValue kv;
+ kv.key = rust::String(k);
+ kv.value = rust::String(v);
+ ffi_desc.properties.push_back(std::move(kv));
+ }
+ return ffi_desc;
+}
+
+inline DatabaseInfo from_ffi_database_info(const ffi::FfiDatabaseInfo&
ffi_info) {
+ DatabaseInfo info;
+ info.database_name = std::string(ffi_info.database_name);
+ info.comment = std::string(ffi_info.comment);
+ info.created_time = ffi_info.created_time;
+ info.modified_time = ffi_info.modified_time;
+ for (const auto& prop : ffi_info.properties) {
+ info.properties[std::string(prop.key)] = std::string(prop.value);
+ }
+ return info;
+}
+
} // namespace utils
} // namespace fluss
diff --git a/bindings/cpp/src/lib.rs b/bindings/cpp/src/lib.rs
index 235d282..5f3e7e9 100644
--- a/bindings/cpp/src/lib.rs
+++ b/bindings/cpp/src/lib.rs
@@ -202,6 +202,39 @@ mod ffi {
partition_infos: Vec<FfiPartitionInfo>,
}
+ struct FfiDatabaseDescriptor {
+ comment: String,
+ properties: Vec<HashMapValue>,
+ }
+
+ struct FfiDatabaseInfo {
+ database_name: String,
+ comment: String,
+ properties: Vec<HashMapValue>,
+ created_time: i64,
+ modified_time: i64,
+ }
+
+ struct FfiDatabaseInfoResult {
+ result: FfiResult,
+ database_info: FfiDatabaseInfo,
+ }
+
+ struct FfiListDatabasesResult {
+ result: FfiResult,
+ database_names: Vec<String>,
+ }
+
+ struct FfiListTablesResult {
+ result: FfiResult,
+ table_names: Vec<String>,
+ }
+
+ struct FfiBoolResult {
+ result: FfiResult,
+ value: bool,
+ }
+
extern "Rust" {
type Connection;
type Admin;
@@ -257,6 +290,29 @@ mod ffi {
partition_spec: Vec<FfiPartitionKeyValue>,
ignore_if_exists: bool,
) -> FfiResult;
+ fn drop_partition(
+ self: &Admin,
+ table_path: &FfiTablePath,
+ partition_spec: Vec<FfiPartitionKeyValue>,
+ ignore_if_not_exists: bool,
+ ) -> FfiResult;
+ fn create_database(
+ self: &Admin,
+ database_name: &str,
+ descriptor: &FfiDatabaseDescriptor,
+ ignore_if_exists: bool,
+ ) -> FfiResult;
+ fn drop_database(
+ self: &Admin,
+ database_name: &str,
+ ignore_if_not_exists: bool,
+ cascade: bool,
+ ) -> FfiResult;
+ fn list_databases(self: &Admin) -> FfiListDatabasesResult;
+ fn database_exists(self: &Admin, database_name: &str) -> FfiBoolResult;
+ fn get_database_info(self: &Admin, database_name: &str) ->
FfiDatabaseInfoResult;
+ fn list_tables(self: &Admin, database_name: &str) ->
FfiListTablesResult;
+ fn table_exists(self: &Admin, table_path: &FfiTablePath) ->
FfiBoolResult;
// Table
unsafe fn delete_table(table: *mut Table);
@@ -661,6 +717,158 @@ impl Admin {
Err(e) => err_result(1, e.to_string()),
}
}
+
+ fn drop_partition(
+ &self,
+ table_path: &ffi::FfiTablePath,
+ partition_spec: Vec<ffi::FfiPartitionKeyValue>,
+ ignore_if_not_exists: bool,
+ ) -> ffi::FfiResult {
+ let path = fcore::metadata::TablePath::new(
+ table_path.database_name.clone(),
+ table_path.table_name.clone(),
+ );
+ let spec_map: std::collections::HashMap<String, String> =
partition_spec
+ .into_iter()
+ .map(|kv| (kv.key, kv.value))
+ .collect();
+ let partition_spec = fcore::metadata::PartitionSpec::new(spec_map);
+
+ let result = RUNTIME.block_on(async {
+ self.inner
+ .drop_partition(&path, &partition_spec, ignore_if_not_exists)
+ .await
+ });
+
+ match result {
+ Ok(_) => ok_result(),
+ Err(e) => err_result(1, e.to_string()),
+ }
+ }
+
+ fn create_database(
+ &self,
+ database_name: &str,
+ descriptor: &ffi::FfiDatabaseDescriptor,
+ ignore_if_exists: bool,
+ ) -> ffi::FfiResult {
+ let descriptor_opt =
types::ffi_database_descriptor_to_core(descriptor);
+
+ let result = RUNTIME.block_on(async {
+ self.inner
+ .create_database(database_name, ignore_if_exists,
descriptor_opt.as_ref())
+ .await
+ });
+
+ match result {
+ Ok(_) => ok_result(),
+ Err(e) => err_result(1, e.to_string()),
+ }
+ }
+
+ fn drop_database(
+ &self,
+ database_name: &str,
+ ignore_if_not_exists: bool,
+ cascade: bool,
+ ) -> ffi::FfiResult {
+ let result = RUNTIME.block_on(async {
+ self.inner
+ .drop_database(database_name, ignore_if_not_exists, cascade)
+ .await
+ });
+
+ match result {
+ Ok(_) => ok_result(),
+ Err(e) => err_result(1, e.to_string()),
+ }
+ }
+
+ fn list_databases(&self) -> ffi::FfiListDatabasesResult {
+ let result = RUNTIME.block_on(async {
self.inner.list_databases().await });
+
+ match result {
+ Ok(names) => ffi::FfiListDatabasesResult {
+ result: ok_result(),
+ database_names: names,
+ },
+ Err(e) => ffi::FfiListDatabasesResult {
+ result: err_result(1, e.to_string()),
+ database_names: vec![],
+ },
+ }
+ }
+
+ fn database_exists(&self, database_name: &str) -> ffi::FfiBoolResult {
+ let result = RUNTIME.block_on(async {
self.inner.database_exists(database_name).await });
+
+ match result {
+ Ok(exists) => ffi::FfiBoolResult {
+ result: ok_result(),
+ value: exists,
+ },
+ Err(e) => ffi::FfiBoolResult {
+ result: err_result(1, e.to_string()),
+ value: false,
+ },
+ }
+ }
+
+ fn get_database_info(&self, database_name: &str) ->
ffi::FfiDatabaseInfoResult {
+ let result = RUNTIME.block_on(async {
self.inner.get_database_info(database_name).await });
+
+ match result {
+ Ok(info) => ffi::FfiDatabaseInfoResult {
+ result: ok_result(),
+ database_info: types::core_database_info_to_ffi(&info),
+ },
+ Err(e) => ffi::FfiDatabaseInfoResult {
+ result: err_result(1, e.to_string()),
+ database_info: ffi::FfiDatabaseInfo {
+ database_name: String::new(),
+ comment: String::new(),
+ properties: vec![],
+ created_time: 0,
+ modified_time: 0,
+ },
+ },
+ }
+ }
+
+ fn list_tables(&self, database_name: &str) -> ffi::FfiListTablesResult {
+ let result = RUNTIME.block_on(async {
self.inner.list_tables(database_name).await });
+
+ match result {
+ Ok(names) => ffi::FfiListTablesResult {
+ result: ok_result(),
+ table_names: names,
+ },
+ Err(e) => ffi::FfiListTablesResult {
+ result: err_result(1, e.to_string()),
+ table_names: vec![],
+ },
+ }
+ }
+
+ fn table_exists(&self, table_path: &ffi::FfiTablePath) ->
ffi::FfiBoolResult {
+ let path = fcore::metadata::TablePath::new(
+ table_path.database_name.clone(),
+ table_path.table_name.clone(),
+ );
+
+ let result = RUNTIME.block_on(async {
self.inner.table_exists(&path).await });
+
+ match result {
+ Ok(exists) => ffi::FfiBoolResult {
+ result: ok_result(),
+ value: exists,
+ },
+ Err(e) => ffi::FfiBoolResult {
+ result: err_result(1, e.to_string()),
+ value: false,
+ },
+ }
+ }
}
// Table implementation
diff --git a/bindings/cpp/src/types.rs b/bindings/cpp/src/types.rs
index 05d3d6a..65b9b04 100644
--- a/bindings/cpp/src/types.rs
+++ b/bindings/cpp/src/types.rs
@@ -250,6 +250,49 @@ pub fn empty_table_info() -> ffi::FfiTableInfo {
}
}
+/// Convert FFI database descriptor to core. Returns None if descriptor is
effectively empty
+/// (no comment and no properties), so create_database can pass Option::None
to core.
+pub fn ffi_database_descriptor_to_core(
+ d: &ffi::FfiDatabaseDescriptor,
+) -> Option<fcore::metadata::DatabaseDescriptor> {
+ if d.comment.is_empty() && d.properties.is_empty() {
+ return None;
+ }
+ let mut builder = fcore::metadata::DatabaseDescriptor::builder();
+ if !d.comment.is_empty() {
+ builder = builder.comment(&d.comment);
+ }
+ if !d.properties.is_empty() {
+ let props: std::collections::HashMap<String, String> = d
+ .properties
+ .iter()
+ .map(|kv| (kv.key.clone(), kv.value.clone()))
+ .collect();
+ builder = builder.custom_properties(props);
+ }
+ Some(builder.build())
+}
+
+/// Convert core DatabaseInfo to FFI.
+pub fn core_database_info_to_ffi(info: &fcore::metadata::DatabaseInfo) ->
ffi::FfiDatabaseInfo {
+ let desc = info.database_descriptor();
+ let properties: Vec<ffi::HashMapValue> = desc
+ .custom_properties()
+ .iter()
+ .map(|(k, v)| ffi::HashMapValue {
+ key: k.clone(),
+ value: v.clone(),
+ })
+ .collect();
+ ffi::FfiDatabaseInfo {
+ database_name: info.database_name().to_string(),
+ comment: desc.comment().unwrap_or("").to_string(),
+ properties,
+ created_time: info.created_time(),
+ modified_time: info.modified_time(),
+ }
+}
+
/// Look up decimal (precision, scale) from schema for column `idx`.
fn get_decimal_type(idx: usize, schema: Option<&fcore::metadata::Schema>) ->
Result<(u32, u32)> {
let col = schema