Skip to content

Commit c387895

Browse files
committed
feat(c): build a catalog-free Table from a resolved schema JSON
Add a catalog-free way to build a Table from an already-resolved Paimon TableSchema, so a non-Java engine (e.g. Doris via the C FFI) can rebuild a table for scan/read without a catalog lookup. Today the only way to obtain a Table is Catalog::get_table, which re-resolves the table (risking schema/ snapshot drift versus what the planner already resolved) and ties the reader to a filesystem-catalog layout. The caller passes everything a reader needs directly: table path, the full TableSchema JSON (preserved as-is, not re-loaded or normalized), database/ table name, branch, and filesystem options. The Rust side constructs the Table with a plain FileIO and rest_env = None; branch only selects the branch-scoped schema/snapshot/tag managers. Reads can still use FE-generated splits via paimon_plan_from_split_bytes. Mirroring Java FileStoreTableFactory.create(fileIO, path, schema), the table stays a normal (writable) table; existing non-main-branch / time-travel write guards apply. Validate the externally supplied schema's structural invariants without normalizing it (TableSchema::validate_resolved_structure): reject duplicate field names, duplicate field ids (including nested), primary-key/partition columns that do not exist, primary keys that are exactly the partition columns (the read path would panic on a zero-column key), and reserved system field names/ids (e.g. a user _ROW_ID column would be silently replaced by the system row number on read). Create-time policy checks (merge-engine/changelog/ aggregation/rowkind/blob strategy, bucket-key existence) are intentionally not run, since they normalize the schema or reject shapes that are valid to read. - crates/paimon/src/table/mod.rs: Table::from_resolved_schema. - crates/paimon/src/spec/schema.rs: TableSchema::validate_resolved_structure. - bindings/c/src/table.rs: paimon_table_from_schema_json (nullable branch defaults to main), mirroring the paimon_catalog_get_table handle/free contract with an ABI signature guard; freed via paimon_table_free. Known pre-existing limitations, unreachable via the split-bytes read path Doris uses, are left for follow-ups: incremental scan uses a root SnapshotManager for non-main branches; time-travel selectors embedded in schema JSON options are not honored by the plain read builder; a stale path option is preferred by FormatTableScan over the table location; a non-existent bucket-key is silently dropped on write (write path only); full Java-parity SchemaValidation is not mirrored.
1 parent 4a933ec commit c387895

8 files changed

Lines changed: 809 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/c/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ futures = "0.3"
3737
arrow = { workspace = true }
3838
arrow-array = { workspace = true }
3939
arrow-schema = { workspace = true }
40+
serde_json = "1.0.120"
4041

4142
[dev-dependencies]
4243
# Test-only: the vector-search integration tests build a real primary-key vindex

bindings/c/src/table.rs

Lines changed: 175 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@
1616
// under the License.
1717

1818
use std::collections::HashMap;
19-
use std::ffi::c_void;
19+
use std::ffi::{c_char, c_void};
2020

2121
use arrow_array::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
2222
use arrow_array::{Array, StructArray};
2323
use futures::StreamExt;
24-
use paimon::spec::{DataField, DataType, Datum, Predicate, PredicateBuilder};
24+
use paimon::catalog::{Identifier, DEFAULT_MAIN_BRANCH};
25+
use paimon::io::FileIO;
26+
use paimon::spec::{DataField, DataType, Datum, Predicate, PredicateBuilder, TableSchema};
2527
use paimon::table::{ArrowRecordBatchStream, DataSplit, Table};
2628
use paimon::Plan;
2729

2830
use crate::error::{check_non_null, paimon_error, validate_cstr, PaimonErrorCode};
2931
use crate::result::{
30-
paimon_result_new_read, paimon_result_next_batch, paimon_result_plan, paimon_result_predicate,
31-
paimon_result_read_builder, paimon_result_record_batch_reader, paimon_result_table_scan,
32+
paimon_result_get_table, paimon_result_new_read, paimon_result_next_batch, paimon_result_plan,
33+
paimon_result_predicate, paimon_result_read_builder, paimon_result_record_batch_reader,
34+
paimon_result_table_scan,
3235
};
3336
use crate::runtime;
3437
use crate::types::*;
@@ -58,10 +61,166 @@ unsafe fn box_table_read_state(state: TableReadState) -> *mut paimon_table_read
5861

5962
// ======================= Table ===============================
6063

64+
/// Create a table directly from a resolved Paimon table schema JSON.
65+
///
66+
/// This constructor does not create a catalog or derive a warehouse. Storage
67+
/// options are used only to build FileIO; they are not merged into the supplied
68+
/// table schema. `branch` selects the branch-scoped managers while preserving
69+
/// the supplied schema; pass null to default to the `main` branch.
70+
///
71+
/// # Safety
72+
/// All string pointers except `branch` must be valid null-terminated C strings.
73+
/// `branch` may be null to select the default `main` branch, or a valid
74+
/// null-terminated C string. `storage_options` must point to
75+
/// `storage_options_len` valid `paimon_option` values, or be null when
76+
/// `storage_options_len` is 0.
77+
#[no_mangle]
78+
pub unsafe extern "C" fn paimon_table_from_schema_json(
79+
table_path: *const c_char,
80+
table_schema_json: *const c_char,
81+
database: *const c_char,
82+
table_name: *const c_char,
83+
branch: *const c_char,
84+
storage_options: *const paimon_option,
85+
storage_options_len: usize,
86+
) -> paimon_result_get_table {
87+
let table_path = match validate_cstr(table_path, "table_path") {
88+
Ok(value) => value,
89+
Err(error) => {
90+
return paimon_result_get_table {
91+
table: std::ptr::null_mut(),
92+
error,
93+
}
94+
}
95+
};
96+
let table_schema_json = match validate_cstr(table_schema_json, "table_schema_json") {
97+
Ok(value) => value,
98+
Err(error) => {
99+
return paimon_result_get_table {
100+
table: std::ptr::null_mut(),
101+
error,
102+
}
103+
}
104+
};
105+
let database = match validate_cstr(database, "database") {
106+
Ok(value) => value,
107+
Err(error) => {
108+
return paimon_result_get_table {
109+
table: std::ptr::null_mut(),
110+
error,
111+
}
112+
}
113+
};
114+
let table_name = match validate_cstr(table_name, "table_name") {
115+
Ok(value) => value,
116+
Err(error) => {
117+
return paimon_result_get_table {
118+
table: std::ptr::null_mut(),
119+
error,
120+
}
121+
}
122+
};
123+
let branch = if branch.is_null() {
124+
DEFAULT_MAIN_BRANCH.to_string()
125+
} else {
126+
match validate_cstr(branch, "branch") {
127+
Ok(value) => value,
128+
Err(error) => {
129+
return paimon_result_get_table {
130+
table: std::ptr::null_mut(),
131+
error,
132+
}
133+
}
134+
}
135+
};
136+
if storage_options.is_null() && storage_options_len > 0 {
137+
return paimon_result_get_table {
138+
table: std::ptr::null_mut(),
139+
error: paimon_error::new(
140+
PaimonErrorCode::InvalidInput,
141+
"null storage_options pointer with non-zero length".to_string(),
142+
),
143+
};
144+
}
145+
146+
let schema = match serde_json::from_str::<TableSchema>(&table_schema_json) {
147+
Ok(schema) => schema,
148+
Err(error) => {
149+
return paimon_result_get_table {
150+
table: std::ptr::null_mut(),
151+
error: paimon_error::new(
152+
PaimonErrorCode::InvalidInput,
153+
format!("Failed to parse table schema JSON: {error}"),
154+
),
155+
}
156+
}
157+
};
158+
159+
let mut options = HashMap::with_capacity(storage_options_len);
160+
if storage_options_len > 0 {
161+
for option in std::slice::from_raw_parts(storage_options, storage_options_len) {
162+
let key = match validate_cstr(option.key, "storage option key") {
163+
Ok(value) => value,
164+
Err(error) => {
165+
return paimon_result_get_table {
166+
table: std::ptr::null_mut(),
167+
error,
168+
}
169+
}
170+
};
171+
let value = match validate_cstr(option.value, "storage option value") {
172+
Ok(value) => value,
173+
Err(error) => {
174+
return paimon_result_get_table {
175+
table: std::ptr::null_mut(),
176+
error,
177+
}
178+
}
179+
};
180+
options.insert(key, value);
181+
}
182+
}
183+
184+
let file_io = match FileIO::from_path(&table_path)
185+
.and_then(|builder| builder.with_props(options.iter()).build())
186+
{
187+
Ok(file_io) => file_io,
188+
Err(error) => {
189+
return paimon_result_get_table {
190+
table: std::ptr::null_mut(),
191+
error: paimon_error::from_paimon(error),
192+
}
193+
}
194+
};
195+
let table = match Table::from_resolved_schema(
196+
file_io,
197+
Identifier::new(database, table_name),
198+
table_path,
199+
schema,
200+
branch,
201+
) {
202+
Ok(table) => table,
203+
Err(error) => {
204+
return paimon_result_get_table {
205+
table: std::ptr::null_mut(),
206+
error: paimon_error::from_paimon(error),
207+
}
208+
}
209+
};
210+
let wrapper = Box::new(paimon_table {
211+
inner: Box::into_raw(Box::new(table)) as *mut c_void,
212+
});
213+
paimon_result_get_table {
214+
table: Box::into_raw(wrapper),
215+
error: std::ptr::null_mut(),
216+
}
217+
}
218+
61219
/// Free a paimon_table.
62220
///
63221
/// # Safety
64-
/// Only call with a table returned from `paimon_catalog_get_table`.
222+
/// Only call with a table returned from `paimon_catalog_get_table` or
223+
/// `paimon_table_from_schema_json`.
65224
#[no_mangle]
66225
pub unsafe extern "C" fn paimon_table_free(table: *mut paimon_table) {
67226
free_table_wrapper(table, |t| t.inner);
@@ -130,7 +289,8 @@ unsafe fn new_read_builder_state(
130289
/// Create a new ReadBuilder from a Table.
131290
///
132291
/// # Safety
133-
/// `table` must be a valid pointer from `paimon_catalog_get_table`, or null (returns error).
292+
/// `table` must be a valid pointer from `paimon_catalog_get_table` or
293+
/// `paimon_table_from_schema_json`, or null (returns error).
134294
#[no_mangle]
135295
pub unsafe extern "C" fn paimon_table_new_read_builder(
136296
table: *const paimon_table,
@@ -1773,6 +1933,15 @@ const _: unsafe extern "C" fn(
17731933
// constructors so an accidental signature change fails to compile rather than
17741934
// silently breaking header consumers. To add behavior, introduce a new
17751935
// `paimon_table_new_read_builder_*` symbol instead of changing one of these.
1936+
const _: unsafe extern "C" fn(
1937+
*const c_char,
1938+
*const c_char,
1939+
*const c_char,
1940+
*const c_char,
1941+
*const c_char,
1942+
*const paimon_option,
1943+
usize,
1944+
) -> paimon_result_get_table = paimon_table_from_schema_json;
17761945
const _: unsafe extern "C" fn(*const paimon_table) -> paimon_result_read_builder =
17771946
paimon_table_new_read_builder;
17781947
const _: unsafe extern "C" fn(

0 commit comments

Comments
 (0)