Skip to main content

laminar_db/ffi/
query.rs

1//! FFI query result functions.
2
3use std::ptr;
4
5use arrow::array::RecordBatch;
6
7use crate::api::{QueryResult, QueryStream};
8
9use super::error::{clear_last_error, set_last_error, LAMINAR_ERR_NULL_POINTER, LAMINAR_OK};
10use super::schema::LaminarSchema;
11
12/// Opaque handle for materialized query results.
13#[repr(C)]
14pub struct LaminarQueryResult {
15    inner: QueryResult,
16}
17
18impl LaminarQueryResult {
19    pub(crate) fn new(result: QueryResult) -> Self {
20        Self { inner: result }
21    }
22}
23
24/// Opaque handle for streaming query results.
25#[repr(C)]
26pub struct LaminarQueryStream {
27    inner: QueryStream,
28}
29
30impl LaminarQueryStream {
31    pub(crate) fn new(stream: QueryStream) -> Self {
32        Self { inner: stream }
33    }
34}
35
36/// Opaque Arrow `RecordBatch` handle. Free with `laminar_batch_free`.
37#[repr(C)]
38pub struct LaminarRecordBatch {
39    inner: RecordBatch,
40}
41
42impl LaminarRecordBatch {
43    pub(crate) fn new(batch: RecordBatch) -> Self {
44        Self { inner: batch }
45    }
46
47    pub(crate) fn into_inner(self) -> RecordBatch {
48        self.inner
49    }
50
51    pub(crate) fn inner(&self) -> &RecordBatch {
52        &self.inner
53    }
54}
55
56/// Get the schema from a query result.
57///
58/// # Safety
59///
60/// `result` and `out` must be valid non-null pointers.
61#[no_mangle]
62pub unsafe extern "C" fn laminar_result_schema(
63    result: *mut LaminarQueryResult,
64    out: *mut *mut LaminarSchema,
65) -> i32 {
66    clear_last_error();
67
68    if result.is_null() || out.is_null() {
69        return LAMINAR_ERR_NULL_POINTER;
70    }
71
72    let schema = unsafe { (*result).inner.schema() };
73    let handle = Box::new(LaminarSchema::new(schema));
74
75    unsafe { *out = Box::into_raw(handle) };
76    LAMINAR_OK
77}
78
79/// Get the total row count from a query result.
80///
81/// # Safety
82///
83/// `result` and `out` must be valid non-null pointers.
84#[no_mangle]
85pub unsafe extern "C" fn laminar_result_num_rows(
86    result: *mut LaminarQueryResult,
87    out: *mut usize,
88) -> i32 {
89    clear_last_error();
90
91    if result.is_null() || out.is_null() {
92        return LAMINAR_ERR_NULL_POINTER;
93    }
94
95    unsafe {
96        *out = (*result).inner.num_rows();
97    }
98    LAMINAR_OK
99}
100
101/// Get the number of batches in a query result.
102///
103/// # Safety
104///
105/// `result` and `out` must be valid non-null pointers.
106#[no_mangle]
107pub unsafe extern "C" fn laminar_result_num_batches(
108    result: *mut LaminarQueryResult,
109    out: *mut usize,
110) -> i32 {
111    clear_last_error();
112
113    if result.is_null() || out.is_null() {
114        return LAMINAR_ERR_NULL_POINTER;
115    }
116
117    unsafe {
118        *out = (*result).inner.num_batches();
119    }
120    LAMINAR_OK
121}
122
123/// Get a batch by index from a query result.
124///
125/// # Safety
126///
127/// `result` and `out` must be valid non-null pointers; `index` must be in range.
128#[no_mangle]
129pub unsafe extern "C" fn laminar_result_get_batch(
130    result: *mut LaminarQueryResult,
131    index: usize,
132    out: *mut *mut LaminarRecordBatch,
133) -> i32 {
134    clear_last_error();
135
136    if result.is_null() || out.is_null() {
137        return LAMINAR_ERR_NULL_POINTER;
138    }
139
140    let result_ref = unsafe { &(*result).inner };
141
142    if let Some(batch) = result_ref.batch(index) {
143        let handle = Box::new(LaminarRecordBatch::new(batch.clone()));
144        unsafe { *out = Box::into_raw(handle) };
145        LAMINAR_OK
146    } else {
147        unsafe { *out = ptr::null_mut() };
148        LAMINAR_ERR_NULL_POINTER
149    }
150}
151
152/// Free a query result handle.
153///
154/// # Safety
155///
156/// `result` must be a valid handle from a laminar function, or NULL.
157#[no_mangle]
158pub unsafe extern "C" fn laminar_result_free(result: *mut LaminarQueryResult) {
159    if !result.is_null() {
160        drop(unsafe { Box::from_raw(result) });
161    }
162}
163
164/// Get the schema from a query stream.
165///
166/// # Safety
167///
168/// `stream` and `out` must be valid non-null pointers.
169#[no_mangle]
170pub unsafe extern "C" fn laminar_stream_schema(
171    stream: *mut LaminarQueryStream,
172    out: *mut *mut LaminarSchema,
173) -> i32 {
174    clear_last_error();
175
176    if stream.is_null() || out.is_null() {
177        return LAMINAR_ERR_NULL_POINTER;
178    }
179
180    let schema = unsafe { (*stream).inner.schema() };
181    let handle = Box::new(LaminarSchema::new(schema));
182
183    unsafe { *out = Box::into_raw(handle) };
184    LAMINAR_OK
185}
186
187/// Get the next batch from a query stream (blocking). Sets `*out` to NULL when exhausted.
188///
189/// # Safety
190///
191/// `stream` and `out` must be valid non-null pointers.
192#[no_mangle]
193pub unsafe extern "C" fn laminar_stream_next(
194    stream: *mut LaminarQueryStream,
195    out: *mut *mut LaminarRecordBatch,
196) -> i32 {
197    clear_last_error();
198
199    if stream.is_null() || out.is_null() {
200        return LAMINAR_ERR_NULL_POINTER;
201    }
202
203    let stream_ref = unsafe { &mut (*stream).inner };
204
205    match stream_ref.next() {
206        Ok(Some(batch)) => {
207            let handle = Box::new(LaminarRecordBatch::new(batch));
208            unsafe { *out = Box::into_raw(handle) };
209            LAMINAR_OK
210        }
211        Ok(None) => {
212            unsafe { *out = ptr::null_mut() };
213            LAMINAR_OK
214        }
215        Err(e) => {
216            unsafe { *out = ptr::null_mut() };
217            let code = e.code();
218            set_last_error(e);
219            code
220        }
221    }
222}
223
224/// Try to get the next batch from a query stream (non-blocking). Sets `*out` to NULL if none available.
225///
226/// # Safety
227///
228/// `stream` and `out` must be valid non-null pointers.
229#[no_mangle]
230pub unsafe extern "C" fn laminar_stream_try_next(
231    stream: *mut LaminarQueryStream,
232    out: *mut *mut LaminarRecordBatch,
233) -> i32 {
234    clear_last_error();
235
236    if stream.is_null() || out.is_null() {
237        return LAMINAR_ERR_NULL_POINTER;
238    }
239
240    let stream_ref = unsafe { &mut (*stream).inner };
241
242    match stream_ref.try_next() {
243        Ok(Some(batch)) => {
244            let handle = Box::new(LaminarRecordBatch::new(batch));
245            unsafe { *out = Box::into_raw(handle) };
246            LAMINAR_OK
247        }
248        Ok(None) => {
249            unsafe { *out = ptr::null_mut() };
250            LAMINAR_OK
251        }
252        Err(e) => {
253            unsafe { *out = ptr::null_mut() };
254            let code = e.code();
255            set_last_error(e);
256            code
257        }
258    }
259}
260
261/// Check if a query stream is still active.
262///
263/// # Safety
264///
265/// `stream` and `out` must be valid non-null pointers.
266#[no_mangle]
267pub unsafe extern "C" fn laminar_stream_is_active(
268    stream: *mut LaminarQueryStream,
269    out: *mut bool,
270) -> i32 {
271    clear_last_error();
272
273    if stream.is_null() || out.is_null() {
274        return LAMINAR_ERR_NULL_POINTER;
275    }
276
277    unsafe {
278        *out = (*stream).inner.is_active();
279    }
280    LAMINAR_OK
281}
282
283/// Cancel a query stream.
284///
285/// # Safety
286///
287/// `stream` must be a valid query stream handle.
288#[no_mangle]
289pub unsafe extern "C" fn laminar_stream_cancel(stream: *mut LaminarQueryStream) -> i32 {
290    clear_last_error();
291
292    if stream.is_null() {
293        return LAMINAR_ERR_NULL_POINTER;
294    }
295
296    unsafe {
297        (*stream).inner.cancel();
298    }
299    LAMINAR_OK
300}
301
302/// Free a query stream handle.
303///
304/// # Safety
305///
306/// `stream` must be a valid handle from a laminar function, or NULL.
307#[no_mangle]
308pub unsafe extern "C" fn laminar_stream_free(stream: *mut LaminarQueryStream) {
309    if !stream.is_null() {
310        drop(unsafe { Box::from_raw(stream) });
311    }
312}
313
314/// Get the number of rows in a record batch.
315///
316/// # Safety
317///
318/// `batch` and `out` must be valid non-null pointers.
319#[no_mangle]
320pub unsafe extern "C" fn laminar_batch_num_rows(
321    batch: *mut LaminarRecordBatch,
322    out: *mut usize,
323) -> i32 {
324    clear_last_error();
325
326    if batch.is_null() || out.is_null() {
327        return LAMINAR_ERR_NULL_POINTER;
328    }
329
330    unsafe {
331        *out = (*batch).inner.num_rows();
332    }
333    LAMINAR_OK
334}
335
336/// Get the number of columns in a record batch.
337///
338/// # Safety
339///
340/// `batch` and `out` must be valid non-null pointers.
341#[no_mangle]
342pub unsafe extern "C" fn laminar_batch_num_columns(
343    batch: *mut LaminarRecordBatch,
344    out: *mut usize,
345) -> i32 {
346    clear_last_error();
347
348    if batch.is_null() || out.is_null() {
349        return LAMINAR_ERR_NULL_POINTER;
350    }
351
352    unsafe {
353        *out = (*batch).inner.num_columns();
354    }
355    LAMINAR_OK
356}
357
358/// Free a record batch handle.
359///
360/// # Safety
361///
362/// `batch` must be a valid handle from a laminar function, or NULL.
363#[no_mangle]
364pub unsafe extern "C" fn laminar_batch_free(batch: *mut LaminarRecordBatch) {
365    if !batch.is_null() {
366        drop(unsafe { Box::from_raw(batch) });
367    }
368}
369
370#[cfg(test)]
371#[allow(clippy::borrow_as_ptr)]
372mod tests {
373    use super::*;
374    use crate::ffi::connection::{laminar_close, laminar_open, laminar_query};
375    use crate::ffi::schema::laminar_schema_free;
376
377    #[test]
378    fn test_result_schema() {
379        let mut conn: *mut super::super::connection::LaminarConnection = ptr::null_mut();
380        let mut result: *mut LaminarQueryResult = ptr::null_mut();
381        let mut schema: *mut LaminarSchema = ptr::null_mut();
382
383        // SAFETY: Test code with valid pointers
384        unsafe {
385            laminar_open(&mut conn);
386
387            // Create table (not source) for point-in-time queries
388            let create_sql = b"CREATE TABLE query_test (id BIGINT PRIMARY KEY, val DOUBLE)\0";
389            crate::ffi::connection::laminar_execute(
390                conn,
391                create_sql.as_ptr().cast(),
392                ptr::null_mut(),
393            );
394
395            let query_sql = b"SELECT * FROM query_test\0";
396            let rc = laminar_query(conn, query_sql.as_ptr().cast(), &mut result);
397            assert_eq!(rc, LAMINAR_OK);
398
399            // Get schema
400            let rc = laminar_result_schema(result, &mut schema);
401            assert_eq!(rc, LAMINAR_OK);
402            assert!(!schema.is_null());
403
404            laminar_schema_free(schema);
405            laminar_result_free(result);
406            laminar_close(conn);
407        }
408    }
409
410    #[test]
411    fn test_result_counts() {
412        let mut conn: *mut super::super::connection::LaminarConnection = ptr::null_mut();
413        let mut result: *mut LaminarQueryResult = ptr::null_mut();
414
415        // SAFETY: Test code with valid pointers
416        unsafe {
417            laminar_open(&mut conn);
418
419            // Create table (not source) for point-in-time queries
420            let create_sql = b"CREATE TABLE count_test (id BIGINT PRIMARY KEY)\0";
421            crate::ffi::connection::laminar_execute(
422                conn,
423                create_sql.as_ptr().cast(),
424                ptr::null_mut(),
425            );
426
427            let query_sql = b"SELECT * FROM count_test\0";
428            laminar_query(conn, query_sql.as_ptr().cast(), &mut result);
429
430            let mut num_rows: usize = 999;
431            let rc = laminar_result_num_rows(result, &mut num_rows);
432            assert_eq!(rc, LAMINAR_OK);
433            assert_eq!(num_rows, 0); // Empty table
434
435            let mut num_batches: usize = 999;
436            let rc = laminar_result_num_batches(result, &mut num_batches);
437            assert_eq!(rc, LAMINAR_OK);
438
439            laminar_result_free(result);
440            laminar_close(conn);
441        }
442    }
443
444    #[test]
445    fn test_batch_free_null() {
446        // SAFETY: Testing null handling
447        unsafe {
448            laminar_batch_free(ptr::null_mut());
449        }
450        // Should not crash
451    }
452}