Skip to main content

laminar_db/ffi/
schema.rs

1//! FFI schema inspection functions.
2
3use std::ffi::{c_char, CStr, CString};
4
5use arrow::datatypes::SchemaRef;
6
7use super::connection::LaminarConnection;
8use super::error::{
9    clear_last_error, set_last_error, LAMINAR_ERR_INVALID_UTF8, LAMINAR_ERR_NULL_POINTER,
10    LAMINAR_OK,
11};
12use super::memory::take_ownership_string;
13
14/// Opaque schema handle for FFI.
15#[repr(C)]
16pub struct LaminarSchema {
17    inner: SchemaRef,
18}
19
20impl LaminarSchema {
21    pub(crate) fn new(schema: SchemaRef) -> Self {
22        Self { inner: schema }
23    }
24
25    #[allow(dead_code)] // used in arrow_ffi
26    pub(crate) fn schema(&self) -> &SchemaRef {
27        &self.inner
28    }
29}
30
31/// Get schema for a source.
32///
33/// # Safety
34///
35/// `conn`, `name`, and `out` must be valid non-null pointers; `name` must be null-terminated UTF-8.
36#[no_mangle]
37pub unsafe extern "C" fn laminar_get_schema(
38    conn: *mut LaminarConnection,
39    name: *const c_char,
40    out: *mut *mut LaminarSchema,
41) -> i32 {
42    clear_last_error();
43
44    if conn.is_null() || name.is_null() || out.is_null() {
45        return LAMINAR_ERR_NULL_POINTER;
46    }
47
48    let Ok(name_str) = (unsafe { CStr::from_ptr(name) }).to_str() else {
49        return LAMINAR_ERR_INVALID_UTF8;
50    };
51
52    let conn_ref = unsafe { &(*conn).inner };
53
54    match conn_ref.get_schema(name_str) {
55        Ok(schema) => {
56            let handle = Box::new(LaminarSchema::new(schema));
57            unsafe { *out = Box::into_raw(handle) };
58            LAMINAR_OK
59        }
60        Err(e) => {
61            let code = e.code();
62            set_last_error(e);
63            code
64        }
65    }
66}
67
68/// List all sources as a JSON array (e.g., `["src1","src2"]`). Caller frees with `laminar_string_free`.
69///
70/// # Safety
71///
72/// `conn` and `out` must be valid non-null pointers.
73#[no_mangle]
74pub unsafe extern "C" fn laminar_list_sources(
75    conn: *mut LaminarConnection,
76    out: *mut *mut c_char,
77) -> i32 {
78    clear_last_error();
79
80    if conn.is_null() || out.is_null() {
81        return LAMINAR_ERR_NULL_POINTER;
82    }
83
84    let conn_ref = unsafe { &(*conn).inner };
85
86    let sources = conn_ref.list_sources();
87    let json = format!(
88        "[{}]",
89        sources
90            .iter()
91            .map(|s| format!("\"{s}\""))
92            .collect::<Vec<_>>()
93            .join(", ")
94    );
95
96    match CString::new(json) {
97        Ok(c_str) => {
98            unsafe { *out = take_ownership_string(c_str) };
99            LAMINAR_OK
100        }
101        Err(_) => LAMINAR_ERR_INVALID_UTF8,
102    }
103}
104
105/// Get the number of fields in a schema.
106///
107/// # Safety
108///
109/// `schema` and `out` must be valid non-null pointers.
110#[no_mangle]
111pub unsafe extern "C" fn laminar_schema_num_fields(
112    schema: *mut LaminarSchema,
113    out: *mut usize,
114) -> i32 {
115    clear_last_error();
116
117    if schema.is_null() || out.is_null() {
118        return LAMINAR_ERR_NULL_POINTER;
119    }
120
121    unsafe {
122        *out = (*schema).inner.fields().len();
123    }
124    LAMINAR_OK
125}
126
127/// Get the name of a field by index. Caller frees the result with `laminar_string_free`.
128///
129/// # Safety
130///
131/// `schema` and `out` must be valid non-null pointers; `index` must be in range.
132#[no_mangle]
133pub unsafe extern "C" fn laminar_schema_field_name(
134    schema: *mut LaminarSchema,
135    index: usize,
136    out: *mut *mut c_char,
137) -> i32 {
138    clear_last_error();
139
140    if schema.is_null() || out.is_null() {
141        return LAMINAR_ERR_NULL_POINTER;
142    }
143
144    let schema_ref = unsafe { &(*schema).inner };
145
146    if index >= schema_ref.fields().len() {
147        return LAMINAR_ERR_NULL_POINTER;
148    }
149
150    let name = schema_ref.field(index).name();
151    match CString::new(name.as_str()) {
152        Ok(c_str) => {
153            unsafe { *out = take_ownership_string(c_str) };
154            LAMINAR_OK
155        }
156        Err(_) => LAMINAR_ERR_INVALID_UTF8,
157    }
158}
159
160/// Get the Arrow data type of a field by index as a string. Caller frees with `laminar_string_free`.
161///
162/// # Safety
163///
164/// `schema` and `out` must be valid non-null pointers; `index` must be in range.
165#[no_mangle]
166pub unsafe extern "C" fn laminar_schema_field_type(
167    schema: *mut LaminarSchema,
168    index: usize,
169    out: *mut *mut c_char,
170) -> i32 {
171    clear_last_error();
172
173    if schema.is_null() || out.is_null() {
174        return LAMINAR_ERR_NULL_POINTER;
175    }
176
177    let schema_ref = unsafe { &(*schema).inner };
178
179    if index >= schema_ref.fields().len() {
180        return LAMINAR_ERR_NULL_POINTER;
181    }
182
183    let data_type = schema_ref.field(index).data_type();
184    let type_str = format!("{data_type:?}");
185    match CString::new(type_str) {
186        Ok(c_str) => {
187            unsafe { *out = take_ownership_string(c_str) };
188            LAMINAR_OK
189        }
190        Err(_) => LAMINAR_ERR_INVALID_UTF8,
191    }
192}
193
194/// Free a schema handle.
195///
196/// # Safety
197///
198/// `schema` must be a valid handle from a laminar function, or NULL.
199#[no_mangle]
200pub unsafe extern "C" fn laminar_schema_free(schema: *mut LaminarSchema) {
201    if !schema.is_null() {
202        drop(unsafe { Box::from_raw(schema) });
203    }
204}
205
206#[cfg(test)]
207#[allow(clippy::borrow_as_ptr)]
208mod tests {
209    use std::ptr;
210
211    use super::*;
212    use crate::ffi::connection::laminar_open;
213    use crate::ffi::memory::laminar_string_free;
214
215    #[test]
216    fn test_list_sources_empty() {
217        let mut conn: *mut LaminarConnection = ptr::null_mut();
218        let mut sources: *mut c_char = ptr::null_mut();
219
220        // SAFETY: Test code with valid pointers
221        unsafe {
222            laminar_open(&mut conn);
223            let rc = laminar_list_sources(conn, &mut sources);
224            assert_eq!(rc, LAMINAR_OK);
225            assert!(!sources.is_null());
226
227            let sources_str = CStr::from_ptr(sources).to_str().unwrap();
228            assert_eq!(sources_str, "[]");
229
230            laminar_string_free(sources);
231            crate::ffi::connection::laminar_close(conn);
232        }
233    }
234
235    #[test]
236    fn test_get_schema() {
237        let mut conn: *mut LaminarConnection = ptr::null_mut();
238        let mut schema: *mut LaminarSchema = ptr::null_mut();
239
240        // SAFETY: Test code with valid pointers
241        unsafe {
242            laminar_open(&mut conn);
243
244            // Create a source
245            let sql = b"CREATE SOURCE schema_ffi_test (id BIGINT, name VARCHAR)\0";
246            crate::ffi::connection::laminar_execute(conn, sql.as_ptr().cast(), ptr::null_mut());
247
248            // Get schema
249            let name = b"schema_ffi_test\0";
250            let rc = laminar_get_schema(conn, name.as_ptr().cast(), &mut schema);
251            assert_eq!(rc, LAMINAR_OK);
252            assert!(!schema.is_null());
253
254            // Check field count
255            let mut num_fields: usize = 0;
256            let rc = laminar_schema_num_fields(schema, &mut num_fields);
257            assert_eq!(rc, LAMINAR_OK);
258            assert_eq!(num_fields, 2);
259
260            // Check field names
261            let mut field_name: *mut c_char = ptr::null_mut();
262            laminar_schema_field_name(schema, 0, &mut field_name);
263            assert_eq!(CStr::from_ptr(field_name).to_str().unwrap(), "id");
264            laminar_string_free(field_name);
265
266            laminar_schema_field_name(schema, 1, &mut field_name);
267            assert_eq!(CStr::from_ptr(field_name).to_str().unwrap(), "name");
268            laminar_string_free(field_name);
269
270            laminar_schema_free(schema);
271            crate::ffi::connection::laminar_close(conn);
272        }
273    }
274
275    #[test]
276    fn test_schema_null_pointer() {
277        let mut num_fields: usize = 0;
278        // SAFETY: Testing null pointer handling
279        let rc = unsafe { laminar_schema_num_fields(ptr::null_mut(), &mut num_fields) };
280        assert_eq!(rc, LAMINAR_ERR_NULL_POINTER);
281    }
282}