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}