Skip to content

Commit aaf8a5c

Browse files
authored
feat(c): expose read builder scan options and time-travel through the C binding (#524)
1 parent 739d6f5 commit aaf8a5c

1 file changed

Lines changed: 292 additions & 9 deletions

File tree

bindings/c/src/table.rs

Lines changed: 292 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
use std::collections::HashMap;
1819
use std::ffi::c_void;
1920

2021
use arrow_array::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
@@ -66,30 +67,141 @@ pub unsafe extern "C" fn paimon_table_free(table: *mut paimon_table) {
6667
free_table_wrapper(table, |t| t.inner);
6768
}
6869

70+
/// Time-travel selector option names, in the core's resolution priority order.
71+
const TIME_TRAVEL_SELECTORS: [&str; 4] = [
72+
"scan.timestamp-millis",
73+
"scan.version",
74+
"scan.snapshot-id",
75+
"scan.tag-name",
76+
];
77+
78+
/// Build a `ReadBuilderState` from a table and a scan-option map, resolving any
79+
/// time-travel selector at construction time.
80+
///
81+
/// Rejects more than one time-travel selector up front (the core silently falls
82+
/// back on a conflict, which would misattribute the failure). Otherwise runs the
83+
/// core's `copy_with_time_travel`, which validates unsupported scan options and
84+
/// resolves the selector; a set selector that does not resolve to a snapshot is
85+
/// an error, so a mistyped or missing selector can never silently read latest.
86+
unsafe fn new_read_builder_state(
87+
table: &Table,
88+
options: HashMap<String, String>,
89+
) -> Result<ReadBuilderState, *mut paimon_error> {
90+
let present: Vec<&str> = TIME_TRAVEL_SELECTORS
91+
.iter()
92+
.copied()
93+
.filter(|name| options.contains_key(*name))
94+
.collect();
95+
if present.len() > 1 {
96+
return Err(paimon_error::new(
97+
PaimonErrorCode::InvalidInput,
98+
format!(
99+
"Only one time-travel selector may be set, found: {}",
100+
present.join(", ")
101+
),
102+
));
103+
}
104+
let selector = TIME_TRAVEL_SELECTORS
105+
.iter()
106+
.find_map(|&name| options.get(name).map(|v| (name.to_string(), v.clone())));
107+
108+
let resolved = match runtime().block_on(table.copy_with_time_travel(options)) {
109+
Ok(t) => t,
110+
Err(e) => return Err(paimon_error::from_paimon(e)),
111+
};
112+
113+
if let Some((name, value)) = selector {
114+
if !resolved.has_resolved_travel_snapshot() {
115+
return Err(paimon_error::new(
116+
PaimonErrorCode::InvalidInput,
117+
format!("time-travel selector {name}={value} did not resolve to any snapshot"),
118+
));
119+
}
120+
}
121+
122+
Ok(ReadBuilderState {
123+
table: resolved,
124+
projected_columns: None,
125+
filter: None,
126+
case_sensitive: true,
127+
})
128+
}
129+
69130
/// Create a new ReadBuilder from a Table.
70131
///
71132
/// # Safety
72133
/// `table` must be a valid pointer from `paimon_catalog_get_table`, or null (returns error).
73134
#[no_mangle]
74135
pub unsafe extern "C" fn paimon_table_new_read_builder(
75136
table: *const paimon_table,
137+
) -> paimon_result_read_builder {
138+
paimon_table_new_read_builder_with_options(table, std::ptr::null(), 0)
139+
}
140+
141+
/// Create a ReadBuilder from a Table with scan options (e.g. time-travel
142+
/// selectors `scan.snapshot-id` / `scan.tag-name` / `scan.timestamp-millis` /
143+
/// `scan.version`). At most one time-travel selector may be set. A selector that
144+
/// does not resolve to a snapshot is an error (never a silent read-of-latest).
145+
///
146+
/// # Safety
147+
/// `table` must be a valid pointer. `options` must be a valid pointer to
148+
/// `options_len` `paimon_option` values, or null when `options_len` is 0.
149+
#[no_mangle]
150+
pub unsafe extern "C" fn paimon_table_new_read_builder_with_options(
151+
table: *const paimon_table,
152+
options: *const paimon_option,
153+
options_len: usize,
76154
) -> paimon_result_read_builder {
77155
if let Err(e) = check_non_null(table, "table") {
78156
return paimon_result_read_builder {
79157
read_builder: std::ptr::null_mut(),
80158
error: e,
81159
};
82160
}
161+
if options.is_null() && options_len > 0 {
162+
return paimon_result_read_builder {
163+
read_builder: std::ptr::null_mut(),
164+
error: paimon_error::new(
165+
PaimonErrorCode::InvalidInput,
166+
"null options pointer with non-zero length".to_string(),
167+
),
168+
};
169+
}
170+
let mut map = HashMap::with_capacity(options_len);
171+
if options_len > 0 {
172+
let slice = std::slice::from_raw_parts(options, options_len);
173+
for opt in slice {
174+
let key = match validate_cstr(opt.key, "option key") {
175+
Ok(s) => s,
176+
Err(e) => {
177+
return paimon_result_read_builder {
178+
read_builder: std::ptr::null_mut(),
179+
error: e,
180+
}
181+
}
182+
};
183+
let value = match validate_cstr(opt.value, "option value") {
184+
Ok(s) => s,
185+
Err(e) => {
186+
return paimon_result_read_builder {
187+
read_builder: std::ptr::null_mut(),
188+
error: e,
189+
}
190+
}
191+
};
192+
map.insert(key, value);
193+
}
194+
}
83195
let table_ref = &*((*table).inner as *const Table);
84-
let state = ReadBuilderState {
85-
table: table_ref.clone(),
86-
projected_columns: None,
87-
filter: None,
88-
case_sensitive: true,
89-
};
90-
paimon_result_read_builder {
91-
read_builder: box_read_builder_state(state),
92-
error: std::ptr::null_mut(),
196+
match new_read_builder_state(table_ref, map) {
197+
Ok(state) => paimon_result_read_builder {
198+
read_builder: box_read_builder_state(state),
199+
error: std::ptr::null_mut(),
200+
},
201+
Err(e) => paimon_result_read_builder {
202+
read_builder: std::ptr::null_mut(),
203+
error: e,
204+
},
93205
}
94206
}
95207

@@ -1611,6 +1723,18 @@ const _: unsafe extern "C" fn(
16111723
paimon_datum,
16121724
) -> paimon_result_predicate = paimon_predicate_not_between;
16131725

1726+
// Read builder ABI signature guards. These pin the C-linked read-builder
1727+
// constructors so an accidental signature change fails to compile rather than
1728+
// silently breaking header consumers. To add behavior, introduce a new
1729+
// `paimon_table_new_read_builder_*` symbol instead of changing one of these.
1730+
const _: unsafe extern "C" fn(*const paimon_table) -> paimon_result_read_builder =
1731+
paimon_table_new_read_builder;
1732+
const _: unsafe extern "C" fn(
1733+
*const paimon_table,
1734+
*const paimon_option,
1735+
usize,
1736+
) -> paimon_result_read_builder = paimon_table_new_read_builder_with_options;
1737+
16141738
#[cfg(test)]
16151739
mod tests {
16161740
use super::*;
@@ -1848,4 +1972,163 @@ mod tests {
18481972
paimon_table_free(table);
18491973
}
18501974
}
1975+
1976+
/// Assert the result is a built read builder (read_builder non-null, error
1977+
/// null) and free it.
1978+
unsafe fn assert_rb_ok_and_free(r: paimon_result_read_builder) {
1979+
assert!(!r.read_builder.is_null(), "expected a read builder");
1980+
assert!(r.error.is_null(), "expected no error");
1981+
paimon_read_builder_free(r.read_builder);
1982+
}
1983+
1984+
/// Assert the result is an error (read_builder null, error non-null) and free
1985+
/// it.
1986+
unsafe fn assert_rb_err_and_free(r: paimon_result_read_builder) {
1987+
assert!(r.read_builder.is_null(), "expected no read builder");
1988+
assert!(!r.error.is_null(), "expected an error");
1989+
paimon_error_free(r.error);
1990+
}
1991+
1992+
/// Assert the result is an error, extract its code and message, then free the
1993+
/// error exactly once. Lets a test pin the error identity (which failure
1994+
/// fired) instead of only its shape.
1995+
unsafe fn assert_rb_err_code_message(r: paimon_result_read_builder) -> (i32, String) {
1996+
assert!(r.read_builder.is_null(), "expected no read builder");
1997+
assert!(!r.error.is_null(), "expected an error");
1998+
let err = &*r.error;
1999+
let code = err.code;
2000+
let message = if err.message.data.is_null() {
2001+
String::new()
2002+
} else {
2003+
let bytes = std::slice::from_raw_parts(err.message.data, err.message.len);
2004+
String::from_utf8_lossy(bytes).into_owned()
2005+
};
2006+
paimon_error_free(r.error);
2007+
(code, message)
2008+
}
2009+
2010+
/// A `paimon_option` borrowing `key`/`value` (kept alive by the caller).
2011+
fn opt(key: &std::ffi::CStr, value: &std::ffi::CStr) -> paimon_option {
2012+
paimon_option {
2013+
key: key.as_ptr(),
2014+
value: value.as_ptr(),
2015+
}
2016+
}
2017+
2018+
#[test]
2019+
fn empty_options_builds_like_plain_new_read_builder() {
2020+
unsafe {
2021+
let table = boxed_test_table();
2022+
// Null pointer + zero length.
2023+
assert_rb_ok_and_free(paimon_table_new_read_builder_with_options(
2024+
table,
2025+
std::ptr::null(),
2026+
0,
2027+
));
2028+
// Non-null pointer to a zero-length array + zero length.
2029+
let empty: [paimon_option; 0] = [];
2030+
assert_rb_ok_and_free(paimon_table_new_read_builder_with_options(
2031+
table,
2032+
empty.as_ptr(),
2033+
0,
2034+
));
2035+
// The plain entry point (delegates with an empty map).
2036+
assert_rb_ok_and_free(paimon_table_new_read_builder(table));
2037+
paimon_table_free(table);
2038+
}
2039+
}
2040+
2041+
#[test]
2042+
fn non_selector_option_builds() {
2043+
unsafe {
2044+
let table = boxed_test_table();
2045+
let k = CString::new("some.unrelated.option").unwrap();
2046+
let v = CString::new("value").unwrap();
2047+
let opts = [opt(&k, &v)];
2048+
assert_rb_ok_and_free(paimon_table_new_read_builder_with_options(
2049+
table,
2050+
opts.as_ptr(),
2051+
1,
2052+
));
2053+
paimon_table_free(table);
2054+
}
2055+
}
2056+
2057+
#[test]
2058+
fn more_than_one_selector_is_rejected() {
2059+
unsafe {
2060+
let table = boxed_test_table();
2061+
let k1 = CString::new("scan.snapshot-id").unwrap();
2062+
let v1 = CString::new("1").unwrap();
2063+
let k2 = CString::new("scan.tag-name").unwrap();
2064+
let v2 = CString::new("t").unwrap();
2065+
let opts = [opt(&k1, &v1), opt(&k2, &v2)];
2066+
let (code, message) = assert_rb_err_code_message(
2067+
paimon_table_new_read_builder_with_options(table, opts.as_ptr(), 2),
2068+
);
2069+
// Binding-constructed rejection: InvalidInput naming both selectors.
2070+
assert_eq!(code, PaimonErrorCode::InvalidInput as i32);
2071+
assert!(
2072+
message.contains("scan.snapshot-id") && message.contains("scan.tag-name"),
2073+
"message should name both selectors, got: {message}"
2074+
);
2075+
paimon_table_free(table);
2076+
}
2077+
}
2078+
2079+
#[test]
2080+
fn unsupported_scan_option_is_rejected() {
2081+
unsafe {
2082+
let table = boxed_test_table();
2083+
let k = CString::new("scan.watermark").unwrap();
2084+
let v = CString::new("0").unwrap();
2085+
let opts = [opt(&k, &v)];
2086+
// Core's validate_scan_options rejects this before resolution; the
2087+
// binding surfaces core's Unsupported code.
2088+
let (code, message) = assert_rb_err_code_message(
2089+
paimon_table_new_read_builder_with_options(table, opts.as_ptr(), 1),
2090+
);
2091+
assert_eq!(code, PaimonErrorCode::Unsupported as i32);
2092+
assert!(
2093+
message.contains("not supported"),
2094+
"message should say the option is not supported, got: {message}"
2095+
);
2096+
paimon_table_free(table);
2097+
}
2098+
}
2099+
2100+
#[test]
2101+
fn malformed_selector_value_does_not_silently_read_latest() {
2102+
unsafe {
2103+
let table = boxed_test_table();
2104+
let k = CString::new("scan.snapshot-id").unwrap();
2105+
let v = CString::new("abc").unwrap();
2106+
let opts = [opt(&k, &v)];
2107+
// Core swallows the parse error and falls back; the binding reports
2108+
// the unified "did not resolve" error rather than building a
2109+
// latest-reading builder.
2110+
let (code, message) = assert_rb_err_code_message(
2111+
paimon_table_new_read_builder_with_options(table, opts.as_ptr(), 1),
2112+
);
2113+
assert_eq!(code, PaimonErrorCode::InvalidInput as i32);
2114+
assert!(
2115+
message.contains("did not resolve"),
2116+
"message should report the selector did not resolve, got: {message}"
2117+
);
2118+
paimon_table_free(table);
2119+
}
2120+
}
2121+
2122+
#[test]
2123+
fn null_options_with_nonzero_length_is_rejected() {
2124+
unsafe {
2125+
let table = boxed_test_table();
2126+
assert_rb_err_and_free(paimon_table_new_read_builder_with_options(
2127+
table,
2128+
std::ptr::null(),
2129+
2,
2130+
));
2131+
paimon_table_free(table);
2132+
}
2133+
}
18512134
}

0 commit comments

Comments
 (0)