Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ header:
- ".gitattributes"
- ".github/PULL_REQUEST_TEMPLATE.md"
- "crates/paimon/tests/**/*.json"
- "crates/paimon/src/table/goldens/*.json"
- "crates/paimon/testdata/**"
- "third-party-licenses/openssl-1.1.1.LICENSE"
- "**/go.sum"
Expand Down
102 changes: 99 additions & 3 deletions bindings/c/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

use std::collections::HashMap;
use std::ffi::c_void;
use std::ptr;

use arrow_array::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
use arrow_array::{Array, StructArray};
Expand All @@ -27,8 +28,9 @@ use paimon::Plan;

use crate::error::{check_non_null, paimon_error, validate_cstr, PaimonErrorCode};
use crate::result::{
paimon_result_new_read, paimon_result_next_batch, paimon_result_plan, paimon_result_predicate,
paimon_result_read_builder, paimon_result_record_batch_reader, paimon_result_table_scan,
paimon_result_get_table, paimon_result_new_read, paimon_result_next_batch, paimon_result_plan,
paimon_result_predicate, paimon_result_read_builder, paimon_result_record_batch_reader,
paimon_result_table_scan,
};
use crate::runtime;
use crate::types::*;
Expand Down Expand Up @@ -61,12 +63,96 @@ unsafe fn box_table_read_state(state: TableReadState) -> *mut paimon_table_read
/// Free a paimon_table.
///
/// # Safety
/// Only call with a table returned from `paimon_catalog_get_table`.
/// Only call with a table returned from `paimon_catalog_get_table` or
/// `paimon_table_from_descriptor`.
#[no_mangle]
pub unsafe extern "C" fn paimon_table_free(table: *mut paimon_table) {
free_table_wrapper(table, |t| t.inner);
}

/// Build a catalog-free `paimon_table` (intended for scan/read) from an
/// FE-provided `TableDescriptor` JSON, without any catalog lookup.
///
/// `descriptor_json` is a NUL-terminated UTF-8 JSON string (see the Rust
/// `paimon::table::TableDescriptor`). `options` is an array of `options_len`
/// storage/runtime `paimon_option` values (credentials, endpoints), or null
/// when `options_len` is 0; they configure `FileIO` only and never change table
/// semantics. The returned table is intended for scan/read.
///
/// On success `table` is non-null and `error` is null; free it with
/// `paimon_table_free`. On failure `table` is null and `error` is non-null.
///
/// # Safety
/// `descriptor_json` must be a valid C string. `options` must point to
/// `options_len` valid `paimon_option` values, or be null when `options_len` is 0.
#[no_mangle]
pub unsafe extern "C" fn paimon_table_from_descriptor(
descriptor_json: *const std::ffi::c_char,
options: *const paimon_option,
options_len: usize,
) -> paimon_result_get_table {
let json = match validate_cstr(descriptor_json, "descriptor_json") {
Ok(s) => s,
Err(e) => {
return paimon_result_get_table {
table: ptr::null_mut(),
error: e,
}
}
};

if options.is_null() && options_len > 0 {
return paimon_result_get_table {
table: ptr::null_mut(),
error: paimon_error::new(
PaimonErrorCode::InvalidInput,
"null options pointer with non-zero length".to_string(),
),
};
}
let mut opts: HashMap<String, String> = HashMap::new();
if options_len > 0 {
let options_slice = std::slice::from_raw_parts(options, options_len);
for opt in options_slice {
let key = match validate_cstr(opt.key, "option key") {
Ok(s) => s,
Err(e) => {
return paimon_result_get_table {
table: ptr::null_mut(),
error: e,
}
}
};
let value = match validate_cstr(opt.value, "option value") {
Ok(s) => s,
Err(e) => {
return paimon_result_get_table {
table: ptr::null_mut(),
error: e,
}
}
};
opts.insert(key, value);
}
}

match Table::from_descriptor_json(&json, opts) {
Ok(table) => {
let wrapper = Box::new(paimon_table {
inner: Box::into_raw(Box::new(table)) as *mut c_void,
});
paimon_result_get_table {
table: Box::into_raw(wrapper),
error: ptr::null_mut(),
}
}
Err(e) => paimon_result_get_table {
table: ptr::null_mut(),
error: paimon_error::from_paimon(e),
},
}
}

/// Time-travel selector option names, in the core's resolution priority order.
const TIME_TRAVEL_SELECTORS: [&str; 4] = [
"scan.timestamp-millis",
Expand Down Expand Up @@ -1787,6 +1873,16 @@ const _: unsafe extern "C" fn(
const _: unsafe extern "C" fn(*const u8, usize) -> paimon_result_plan =
paimon_plan_from_split_bytes;

// Descriptor table constructor ABI signature guard. Pins the catalog-free table
// constructor so an accidental signature change fails to compile rather than
// silently breaking header consumers. To add behavior, introduce a new symbol
// instead of changing this one.
const _: unsafe extern "C" fn(
*const std::ffi::c_char,
*const paimon_option,
usize,
) -> paimon_result_get_table = paimon_table_from_descriptor;

#[cfg(test)]
mod tests {
use super::*;
Expand Down
52 changes: 52 additions & 0 deletions bindings/c/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2474,3 +2474,55 @@ fn plan_from_split_bytes_rejects_garbage() {
assert!(!r.error.is_null());
unsafe { paimon_error_free(r.error) };
}

// A minimal but valid v1 TableDescriptor JSON (scalar schema; the descriptor
// parser's complex-type coverage lives in the paimon crate's unit tests).
const DESCRIPTOR_V1_JSON: &str = r#"{
"version": 1,
"path": "file:///tmp/paimon-c-test/db.db/t",
"database": "db",
"name": "t",
"tableSchema": {
"version": 3,
"id": 0,
"fields": [ { "id": 0, "name": "id", "type": "INT NOT NULL" } ],
"highestFieldId": 0,
"partitionKeys": [],
"primaryKeys": ["id"],
"options": {},
"timeMillis": 1
}
}"#;

#[test]
fn table_from_descriptor_builds_table() {
let json = CString::new(DESCRIPTOR_V1_JSON).unwrap();
unsafe {
let result = paimon_table_from_descriptor(json.as_ptr(), ptr::null(), 0);
assert!(result.error.is_null(), "from_descriptor should not error");
assert!(!result.table.is_null(), "table handle must be non-null");
paimon_table_free(result.table);
}
}

#[test]
fn table_from_descriptor_rejects_unknown_version() {
let json =
CString::new(DESCRIPTOR_V1_JSON.replace("\"version\": 1", "\"version\": 2")).unwrap();
unsafe {
let result = paimon_table_from_descriptor(json.as_ptr(), ptr::null(), 0);
assert!(!result.error.is_null(), "unknown version must error");
assert!(result.table.is_null());
paimon_error_free(result.error);
}
}

#[test]
fn table_from_descriptor_rejects_null_json() {
unsafe {
let result = paimon_table_from_descriptor(ptr::null(), ptr::null(), 0);
assert!(!result.error.is_null(), "null json must error");
assert!(result.table.is_null());
paimon_error_free(result.error);
}
}
Loading
Loading