Skip to main content

laminar_db/ffi/
schema.rs

1//! FFI schema inspection functions.
2//!
3//! Provides `extern "C"` wrappers for schema inspection operations.
4
5use std::ffi::{c_char, CStr, CString};
6
7use arrow::datatypes::SchemaRef;
8
9use super::connection::LaminarConnection;
10use super::error::{
11    clear_last_error, set_last_error, LAMINAR_ERR_INVALID_UTF8, LAMINAR_ERR_NULL_POINTER,
12    LAMINAR_OK,
13};
14use super::memory::take_ownership_string;
15
16/// Opaque schema handle for FFI.
17#[repr(C)]
18pub struct LaminarSchema {
19    inner: SchemaRef,
20}
21
22impl LaminarSchema {
23    /// Create from Arrow schema.
24    pub(crate) fn new(schema: SchemaRef) -> Self {
25        Self { inner: schema }
26    }
27
28    /// Get inner schema reference.
29    #[allow(dead_code)] // Used for Arrow C Data Interface export
30    pub(crate) fn schema(&self) -> &SchemaRef {
31        &self.inner
32    }
33}
34
35/// Get schema for a source.
36///
37/// # Arguments
38///
39/// * `conn` - Database connection
40/// * `name` - Null-terminated source name
41/// * `out` - Pointer to receive schema handle
42///
43/// # Returns
44///
45/// `LAMINAR_OK` on success, or an error code.
46///
47/// # Safety
48///
49/// * `conn` must be a valid connection handle
50/// * `name` must be a valid null-terminated UTF-8 string
51/// * `out` must be a valid pointer
52#[no_mangle]
53pub unsafe extern "C" fn laminar_get_schema(
54    conn: *mut LaminarConnection,
55    name: *const c_char,
56    out: *mut *mut LaminarSchema,
57) -> i32 {
58    clear_last_error();
59
60    if conn.is_null() || name.is_null() || out.is_null() {
61        return LAMINAR_ERR_NULL_POINTER;
62    }
63
64    // SAFETY: name is non-null (checked above)
65    let Ok(name_str) = (unsafe { CStr::from_ptr(name) }).to_str() else {
66        return LAMINAR_ERR_INVALID_UTF8;
67    };
68
69    // SAFETY: conn is non-null (checked above)
70    let conn_ref = unsafe { &(*conn).inner };
71
72    match conn_ref.get_schema(name_str) {
73        Ok(schema) => {
74            let handle = Box::new(LaminarSchema::new(schema));
75            // SAFETY: out is non-null (checked above)
76            unsafe { *out = Box::into_raw(handle) };
77            LAMINAR_OK
78        }
79        Err(e) => {
80            let code = e.code();
81            set_last_error(e);
82            code
83        }
84    }
85}
86
87/// List all sources as JSON array.
88///
89/// Returns a JSON array like `["source1", "source2"]`.
90///
91/// # Arguments
92///
93/// * `conn` - Database connection
94/// * `out` - Pointer to receive JSON string (caller frees with `laminar_string_free`)
95///
96/// # Returns
97///
98/// `LAMINAR_OK` on success, or an error code.
99///
100/// # Safety
101///
102/// * `conn` must be a valid connection handle
103/// * `out` must be a valid pointer
104#[no_mangle]
105pub unsafe extern "C" fn laminar_list_sources(
106    conn: *mut LaminarConnection,
107    out: *mut *mut c_char,
108) -> i32 {
109    clear_last_error();
110
111    if conn.is_null() || out.is_null() {
112        return LAMINAR_ERR_NULL_POINTER;
113    }
114
115    // SAFETY: conn is non-null (checked above)
116    let conn_ref = unsafe { &(*conn).inner };
117
118    let sources = conn_ref.list_sources();
119    let json = format!(
120        "[{}]",
121        sources
122            .iter()
123            .map(|s| format!("\"{s}\""))
124            .collect::<Vec<_>>()
125            .join(", ")
126    );
127
128    match CString::new(json) {
129        Ok(c_str) => {
130            // SAFETY: out is non-null (checked above)
131            unsafe { *out = take_ownership_string(c_str) };
132            LAMINAR_OK
133        }
134        Err(_) => LAMINAR_ERR_INVALID_UTF8,
135    }
136}
137
138/// Get the number of fields in a schema.
139///
140/// # Arguments
141///
142/// * `schema` - Schema handle
143/// * `out` - Pointer to receive field count
144///
145/// # Returns
146///
147/// `LAMINAR_OK` on success, or an error code.
148///
149/// # Safety
150///
151/// * `schema` must be a valid schema handle
152/// * `out` must be a valid pointer
153#[no_mangle]
154pub unsafe extern "C" fn laminar_schema_num_fields(
155    schema: *mut LaminarSchema,
156    out: *mut usize,
157) -> i32 {
158    clear_last_error();
159
160    if schema.is_null() || out.is_null() {
161        return LAMINAR_ERR_NULL_POINTER;
162    }
163
164    // SAFETY: schema and out are non-null (checked above)
165    unsafe {
166        *out = (*schema).inner.fields().len();
167    }
168    LAMINAR_OK
169}
170
171/// Get the name of a field by index.
172///
173/// # Arguments
174///
175/// * `schema` - Schema handle
176/// * `index` - Field index (0-based)
177/// * `out` - Pointer to receive field name (caller frees with `laminar_string_free`)
178///
179/// # Returns
180///
181/// `LAMINAR_OK` on success, or an error code.
182///
183/// # Safety
184///
185/// * `schema` must be a valid schema handle
186/// * `index` must be less than the number of fields
187/// * `out` must be a valid pointer
188#[no_mangle]
189pub unsafe extern "C" fn laminar_schema_field_name(
190    schema: *mut LaminarSchema,
191    index: usize,
192    out: *mut *mut c_char,
193) -> i32 {
194    clear_last_error();
195
196    if schema.is_null() || out.is_null() {
197        return LAMINAR_ERR_NULL_POINTER;
198    }
199
200    // SAFETY: schema is non-null (checked above)
201    let schema_ref = unsafe { &(*schema).inner };
202
203    if index >= schema_ref.fields().len() {
204        return LAMINAR_ERR_NULL_POINTER; // Index out of bounds
205    }
206
207    let name = schema_ref.field(index).name();
208    match CString::new(name.as_str()) {
209        Ok(c_str) => {
210            // SAFETY: out is non-null (checked above)
211            unsafe { *out = take_ownership_string(c_str) };
212            LAMINAR_OK
213        }
214        Err(_) => LAMINAR_ERR_INVALID_UTF8,
215    }
216}
217
218/// Get the type of a field by index.
219///
220/// Returns the Arrow data type as a string (e.g., "Int64", "Utf8", "Float64").
221///
222/// # Arguments
223///
224/// * `schema` - Schema handle
225/// * `index` - Field index (0-based)
226/// * `out` - Pointer to receive type name (caller frees with `laminar_string_free`)
227///
228/// # Returns
229///
230/// `LAMINAR_OK` on success, or an error code.
231///
232/// # Safety
233///
234/// * `schema` must be a valid schema handle
235/// * `index` must be less than the number of fields
236/// * `out` must be a valid pointer
237#[no_mangle]
238pub unsafe extern "C" fn laminar_schema_field_type(
239    schema: *mut LaminarSchema,
240    index: usize,
241    out: *mut *mut c_char,
242) -> i32 {
243    clear_last_error();
244
245    if schema.is_null() || out.is_null() {
246        return LAMINAR_ERR_NULL_POINTER;
247    }
248
249    // SAFETY: schema is non-null (checked above)
250    let schema_ref = unsafe { &(*schema).inner };
251
252    if index >= schema_ref.fields().len() {
253        return LAMINAR_ERR_NULL_POINTER; // Index out of bounds
254    }
255
256    let data_type = schema_ref.field(index).data_type();
257    let type_str = format!("{data_type:?}");
258    match CString::new(type_str) {
259        Ok(c_str) => {
260            // SAFETY: out is non-null (checked above)
261            unsafe { *out = take_ownership_string(c_str) };
262            LAMINAR_OK
263        }
264        Err(_) => LAMINAR_ERR_INVALID_UTF8,
265    }
266}
267
268/// Free a schema handle.
269///
270/// # Arguments
271///
272/// * `schema` - Schema handle to free
273///
274/// # Safety
275///
276/// `schema` must be a valid handle from a laminar function, or NULL.
277#[no_mangle]
278pub unsafe extern "C" fn laminar_schema_free(schema: *mut LaminarSchema) {
279    if !schema.is_null() {
280        // SAFETY: schema is non-null and was allocated by Box
281        drop(unsafe { Box::from_raw(schema) });
282    }
283}
284
285#[cfg(test)]
286#[allow(clippy::borrow_as_ptr)]
287mod tests {
288    use std::ptr;
289
290    use super::*;
291    use crate::ffi::connection::laminar_open;
292    use crate::ffi::memory::laminar_string_free;
293
294    #[test]
295    fn test_list_sources_empty() {
296        let mut conn: *mut LaminarConnection = ptr::null_mut();
297        let mut sources: *mut c_char = ptr::null_mut();
298
299        // SAFETY: Test code with valid pointers
300        unsafe {
301            laminar_open(&mut conn);
302            let rc = laminar_list_sources(conn, &mut sources);
303            assert_eq!(rc, LAMINAR_OK);
304            assert!(!sources.is_null());
305
306            let sources_str = CStr::from_ptr(sources).to_str().unwrap();
307            assert_eq!(sources_str, "[]");
308
309            laminar_string_free(sources);
310            crate::ffi::connection::laminar_close(conn);
311        }
312    }
313
314    #[test]
315    fn test_get_schema() {
316        let mut conn: *mut LaminarConnection = ptr::null_mut();
317        let mut schema: *mut LaminarSchema = ptr::null_mut();
318
319        // SAFETY: Test code with valid pointers
320        unsafe {
321            laminar_open(&mut conn);
322
323            // Create a source
324            let sql = b"CREATE SOURCE schema_ffi_test (id BIGINT, name VARCHAR)\0";
325            crate::ffi::connection::laminar_execute(conn, sql.as_ptr().cast(), ptr::null_mut());
326
327            // Get schema
328            let name = b"schema_ffi_test\0";
329            let rc = laminar_get_schema(conn, name.as_ptr().cast(), &mut schema);
330            assert_eq!(rc, LAMINAR_OK);
331            assert!(!schema.is_null());
332
333            // Check field count
334            let mut num_fields: usize = 0;
335            let rc = laminar_schema_num_fields(schema, &mut num_fields);
336            assert_eq!(rc, LAMINAR_OK);
337            assert_eq!(num_fields, 2);
338
339            // Check field names
340            let mut field_name: *mut c_char = ptr::null_mut();
341            laminar_schema_field_name(schema, 0, &mut field_name);
342            assert_eq!(CStr::from_ptr(field_name).to_str().unwrap(), "id");
343            laminar_string_free(field_name);
344
345            laminar_schema_field_name(schema, 1, &mut field_name);
346            assert_eq!(CStr::from_ptr(field_name).to_str().unwrap(), "name");
347            laminar_string_free(field_name);
348
349            laminar_schema_free(schema);
350            crate::ffi::connection::laminar_close(conn);
351        }
352    }
353
354    #[test]
355    fn test_schema_null_pointer() {
356        let mut num_fields: usize = 0;
357        // SAFETY: Testing null pointer handling
358        let rc = unsafe { laminar_schema_num_fields(ptr::null_mut(), &mut num_fields) };
359        assert_eq!(rc, LAMINAR_ERR_NULL_POINTER);
360    }
361}