Skip to main content

laminar_db/ffi/
connection.rs

1//! FFI connection functions.
2//!
3//! Provides `extern "C"` wrappers for database connection operations.
4
5use std::ffi::{c_char, CStr};
6use std::ptr;
7
8use crate::api::Connection;
9
10use super::error::{
11    clear_last_error, set_last_error, LAMINAR_ERR_INVALID_UTF8, LAMINAR_ERR_NULL_POINTER,
12    LAMINAR_OK,
13};
14use super::query::{LaminarQueryResult, LaminarQueryStream};
15
16/// Opaque connection handle for FFI.
17///
18/// This wraps a `Connection` for C callers. Create with `laminar_open()`,
19/// free with `laminar_close()`.
20#[repr(C)]
21pub struct LaminarConnection {
22    pub(crate) inner: Connection,
23}
24
25/// Open a new database connection.
26///
27/// # Arguments
28///
29/// * `out` - Pointer to receive the connection handle
30///
31/// # Returns
32///
33/// `LAMINAR_OK` on success, or an error code. On error, call `laminar_last_error()`
34/// for details.
35///
36/// # Safety
37///
38/// `out` must be a valid pointer to a `*mut LaminarConnection`.
39#[no_mangle]
40pub unsafe extern "C" fn laminar_open(out: *mut *mut LaminarConnection) -> i32 {
41    clear_last_error();
42
43    if out.is_null() {
44        return LAMINAR_ERR_NULL_POINTER;
45    }
46
47    match Connection::open() {
48        Ok(conn) => {
49            let handle = Box::new(LaminarConnection { inner: conn });
50            // SAFETY: out is non-null (checked above)
51            unsafe { *out = Box::into_raw(handle) };
52            LAMINAR_OK
53        }
54        Err(e) => {
55            let code = e.code();
56            set_last_error(e);
57            code
58        }
59    }
60}
61
62/// Close a database connection.
63///
64/// This frees the connection and all associated resources. The connection
65/// handle becomes invalid after this call.
66///
67/// # Arguments
68///
69/// * `conn` - Connection handle to close
70///
71/// # Returns
72///
73/// `LAMINAR_OK` on success, or an error code.
74///
75/// # Safety
76///
77/// `conn` must be a valid handle from `laminar_open()` that has not been closed.
78#[no_mangle]
79pub unsafe extern "C" fn laminar_close(conn: *mut LaminarConnection) -> i32 {
80    clear_last_error();
81
82    if conn.is_null() {
83        return LAMINAR_ERR_NULL_POINTER;
84    }
85
86    // SAFETY: conn is non-null and was created by laminar_open
87    let handle = unsafe { Box::from_raw(conn) };
88    match handle.inner.close() {
89        Ok(()) => LAMINAR_OK,
90        Err(e) => {
91            let code = e.code();
92            set_last_error(e);
93            code
94        }
95    }
96}
97
98/// Execute a SQL statement.
99///
100/// For DDL statements (CREATE, DROP), `out` may be NULL. For queries,
101/// `out` receives a `LaminarQueryResult` handle.
102///
103/// # Arguments
104///
105/// * `conn` - Database connection
106/// * `sql` - Null-terminated SQL string
107/// * `out` - Optional pointer to receive query result (may be NULL for DDL)
108///
109/// # Returns
110///
111/// `LAMINAR_OK` on success, or an error code.
112///
113/// # Safety
114///
115/// * `conn` must be a valid connection handle
116/// * `sql` must be a valid null-terminated UTF-8 string
117/// * If `out` is non-null, it must be a valid pointer
118#[no_mangle]
119pub unsafe extern "C" fn laminar_execute(
120    conn: *mut LaminarConnection,
121    sql: *const c_char,
122    out: *mut *mut LaminarQueryResult,
123) -> i32 {
124    clear_last_error();
125
126    if conn.is_null() || sql.is_null() {
127        return LAMINAR_ERR_NULL_POINTER;
128    }
129
130    // SAFETY: sql is non-null (checked above)
131    let Ok(sql_str) = (unsafe { CStr::from_ptr(sql) }).to_str() else {
132        return LAMINAR_ERR_INVALID_UTF8;
133    };
134
135    // SAFETY: conn is non-null (checked above)
136    let conn_ref = unsafe { &(*conn).inner };
137
138    match conn_ref.execute(sql_str) {
139        Ok(result) => {
140            use crate::api::ExecuteResult;
141            match result {
142                ExecuteResult::Query(stream) => {
143                    // Collect to materialized result
144                    match stream.collect() {
145                        Ok(query_result) => {
146                            if !out.is_null() {
147                                let handle = Box::new(LaminarQueryResult::new(query_result));
148                                // SAFETY: out is non-null (checked)
149                                unsafe { *out = Box::into_raw(handle) };
150                            }
151                            LAMINAR_OK
152                        }
153                        Err(e) => {
154                            let code = e.code();
155                            set_last_error(e);
156                            code
157                        }
158                    }
159                }
160                ExecuteResult::Metadata(batch) => {
161                    if !out.is_null() {
162                        let query_result = crate::api::QueryResult::from_batch(batch);
163                        let handle = Box::new(LaminarQueryResult::new(query_result));
164                        // SAFETY: out is non-null (checked)
165                        unsafe { *out = Box::into_raw(handle) };
166                    }
167                    LAMINAR_OK
168                }
169                ExecuteResult::Ddl(_) | ExecuteResult::RowsAffected(_) => {
170                    // DDL or DML - no result to return
171                    if !out.is_null() {
172                        // SAFETY: out is non-null
173                        unsafe { *out = ptr::null_mut() };
174                    }
175                    LAMINAR_OK
176                }
177            }
178        }
179        Err(e) => {
180            let code = e.code();
181            set_last_error(e);
182            code
183        }
184    }
185}
186
187/// Execute a query and get materialized results.
188///
189/// This executes the SQL and waits for all results before returning.
190///
191/// # Arguments
192///
193/// * `conn` - Database connection
194/// * `sql` - Null-terminated SQL string
195/// * `out` - Pointer to receive query result
196///
197/// # Returns
198///
199/// `LAMINAR_OK` on success, or an error code.
200///
201/// # Safety
202///
203/// * `conn` must be a valid connection handle
204/// * `sql` must be a valid null-terminated UTF-8 string
205/// * `out` must be a valid pointer
206#[no_mangle]
207pub unsafe extern "C" fn laminar_query(
208    conn: *mut LaminarConnection,
209    sql: *const c_char,
210    out: *mut *mut LaminarQueryResult,
211) -> i32 {
212    clear_last_error();
213
214    if conn.is_null() || sql.is_null() || out.is_null() {
215        return LAMINAR_ERR_NULL_POINTER;
216    }
217
218    // SAFETY: sql is non-null (checked above)
219    let Ok(sql_str) = (unsafe { CStr::from_ptr(sql) }).to_str() else {
220        return LAMINAR_ERR_INVALID_UTF8;
221    };
222
223    // SAFETY: conn is non-null (checked above)
224    let conn_ref = unsafe { &(*conn).inner };
225
226    match conn_ref.query(sql_str) {
227        Ok(result) => {
228            let handle = Box::new(LaminarQueryResult::new(result));
229            // SAFETY: out is non-null (checked above)
230            unsafe { *out = Box::into_raw(handle) };
231            LAMINAR_OK
232        }
233        Err(e) => {
234            let code = e.code();
235            set_last_error(e);
236            code
237        }
238    }
239}
240
241/// Execute a query with streaming results.
242///
243/// Returns a stream handle for incremental result retrieval.
244///
245/// # Arguments
246///
247/// * `conn` - Database connection
248/// * `sql` - Null-terminated SQL string
249/// * `out` - Pointer to receive query stream
250///
251/// # Returns
252///
253/// `LAMINAR_OK` on success, or an error code.
254///
255/// # Safety
256///
257/// * `conn` must be a valid connection handle
258/// * `sql` must be a valid null-terminated UTF-8 string
259/// * `out` must be a valid pointer
260#[no_mangle]
261pub unsafe extern "C" fn laminar_query_stream(
262    conn: *mut LaminarConnection,
263    sql: *const c_char,
264    out: *mut *mut LaminarQueryStream,
265) -> i32 {
266    clear_last_error();
267
268    if conn.is_null() || sql.is_null() || out.is_null() {
269        return LAMINAR_ERR_NULL_POINTER;
270    }
271
272    // SAFETY: sql is non-null (checked above)
273    let Ok(sql_str) = (unsafe { CStr::from_ptr(sql) }).to_str() else {
274        return LAMINAR_ERR_INVALID_UTF8;
275    };
276
277    // SAFETY: conn is non-null (checked above)
278    let conn_ref = unsafe { &(*conn).inner };
279
280    match conn_ref.query_stream(sql_str) {
281        Ok(stream) => {
282            let handle = Box::new(LaminarQueryStream::new(stream));
283            // SAFETY: out is non-null (checked above)
284            unsafe { *out = Box::into_raw(handle) };
285            LAMINAR_OK
286        }
287        Err(e) => {
288            let code = e.code();
289            set_last_error(e);
290            code
291        }
292    }
293}
294
295/// Start the streaming pipeline.
296///
297/// # Arguments
298///
299/// * `conn` - Database connection
300///
301/// # Returns
302///
303/// `LAMINAR_OK` on success, or an error code.
304///
305/// # Safety
306///
307/// `conn` must be a valid connection handle.
308#[no_mangle]
309pub unsafe extern "C" fn laminar_start(conn: *mut LaminarConnection) -> i32 {
310    clear_last_error();
311
312    if conn.is_null() {
313        return LAMINAR_ERR_NULL_POINTER;
314    }
315
316    // SAFETY: conn is non-null (checked above)
317    let conn_ref = unsafe { &(*conn).inner };
318
319    match conn_ref.start() {
320        Ok(()) => LAMINAR_OK,
321        Err(e) => {
322            let code = e.code();
323            set_last_error(e);
324            code
325        }
326    }
327}
328
329/// Check if the connection is closed.
330///
331/// # Arguments
332///
333/// * `conn` - Database connection
334/// * `out` - Pointer to receive result (true if closed)
335///
336/// # Returns
337///
338/// `LAMINAR_OK` on success, or an error code.
339///
340/// # Safety
341///
342/// * `conn` must be a valid connection handle
343/// * `out` must be a valid pointer
344#[no_mangle]
345pub unsafe extern "C" fn laminar_is_closed(conn: *mut LaminarConnection, out: *mut bool) -> i32 {
346    clear_last_error();
347
348    if conn.is_null() || out.is_null() {
349        return LAMINAR_ERR_NULL_POINTER;
350    }
351
352    // SAFETY: conn and out are non-null (checked above)
353    unsafe {
354        *out = (*conn).inner.is_closed();
355    }
356    LAMINAR_OK
357}
358
359#[cfg(test)]
360#[allow(clippy::borrow_as_ptr)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_open_close() {
366        let mut conn: *mut LaminarConnection = ptr::null_mut();
367        // SAFETY: Test code with valid pointer
368        let rc = unsafe { laminar_open(&mut conn) };
369        assert_eq!(rc, LAMINAR_OK);
370        assert!(!conn.is_null());
371
372        // SAFETY: conn is valid from laminar_open
373        let rc = unsafe { laminar_close(conn) };
374        assert_eq!(rc, LAMINAR_OK);
375    }
376
377    #[test]
378    fn test_open_null_pointer() {
379        // SAFETY: Testing null pointer handling
380        let rc = unsafe { laminar_open(ptr::null_mut()) };
381        assert_eq!(rc, LAMINAR_ERR_NULL_POINTER);
382    }
383
384    #[test]
385    fn test_close_null_pointer() {
386        // SAFETY: Testing null pointer handling
387        let rc = unsafe { laminar_close(ptr::null_mut()) };
388        assert_eq!(rc, LAMINAR_ERR_NULL_POINTER);
389    }
390
391    #[test]
392    fn test_execute_create_source() {
393        let mut conn: *mut LaminarConnection = ptr::null_mut();
394        // SAFETY: Test code with valid pointers
395        unsafe {
396            let rc = laminar_open(&mut conn);
397            assert_eq!(rc, LAMINAR_OK);
398
399            let sql = b"CREATE SOURCE ffi_test (id BIGINT, name VARCHAR)\0";
400            let rc = laminar_execute(conn, sql.as_ptr().cast(), ptr::null_mut());
401            assert_eq!(rc, LAMINAR_OK);
402
403            laminar_close(conn);
404        }
405    }
406
407    #[test]
408    fn test_is_closed() {
409        let mut conn: *mut LaminarConnection = ptr::null_mut();
410        let mut is_closed = true;
411
412        // SAFETY: Test code with valid pointers
413        unsafe {
414            laminar_open(&mut conn);
415            let rc = laminar_is_closed(conn, &mut is_closed);
416            assert_eq!(rc, LAMINAR_OK);
417            assert!(!is_closed);
418
419            laminar_close(conn);
420        }
421    }
422}