Skip to main content

laminar_db/ffi/
query.rs

1//! FFI query result functions.
2//!
3//! Provides `extern "C"` wrappers for query result operations.
4
5use std::ptr;
6
7use arrow::array::RecordBatch;
8
9use crate::api::{QueryResult, QueryStream};
10
11use super::error::{clear_last_error, set_last_error, LAMINAR_ERR_NULL_POINTER, LAMINAR_OK};
12use super::schema::LaminarSchema;
13
14/// Opaque query result handle for FFI.
15///
16/// Contains materialized query results (all batches in memory).
17#[repr(C)]
18pub struct LaminarQueryResult {
19    inner: QueryResult,
20}
21
22impl LaminarQueryResult {
23    /// Create from `QueryResult`.
24    pub(crate) fn new(result: QueryResult) -> Self {
25        Self { inner: result }
26    }
27}
28
29/// Opaque query stream handle for FFI.
30///
31/// Provides streaming access to query results.
32#[repr(C)]
33pub struct LaminarQueryStream {
34    inner: QueryStream,
35}
36
37impl LaminarQueryStream {
38    /// Create from `QueryStream`.
39    pub(crate) fn new(stream: QueryStream) -> Self {
40        Self { inner: stream }
41    }
42}
43
44/// Opaque record batch handle for FFI.
45///
46/// Wraps an Arrow `RecordBatch`. Create from query results, free with `laminar_batch_free`.
47#[repr(C)]
48pub struct LaminarRecordBatch {
49    inner: RecordBatch,
50}
51
52impl LaminarRecordBatch {
53    /// Create from `RecordBatch`.
54    pub(crate) fn new(batch: RecordBatch) -> Self {
55        Self { inner: batch }
56    }
57
58    /// Consume and return inner `RecordBatch`.
59    pub(crate) fn into_inner(self) -> RecordBatch {
60        self.inner
61    }
62
63    /// Get reference to inner `RecordBatch`.
64    pub(crate) fn inner(&self) -> &RecordBatch {
65        &self.inner
66    }
67}
68
69// ============================================================================
70// Query Result Functions
71// ============================================================================
72
73/// Get the schema from a query result.
74///
75/// # Arguments
76///
77/// * `result` - Query result handle
78/// * `out` - Pointer to receive schema handle
79///
80/// # Returns
81///
82/// `LAMINAR_OK` on success, or an error code.
83///
84/// # Safety
85///
86/// * `result` must be a valid query result handle
87/// * `out` must be a valid pointer
88#[no_mangle]
89pub unsafe extern "C" fn laminar_result_schema(
90    result: *mut LaminarQueryResult,
91    out: *mut *mut LaminarSchema,
92) -> i32 {
93    clear_last_error();
94
95    if result.is_null() || out.is_null() {
96        return LAMINAR_ERR_NULL_POINTER;
97    }
98
99    // SAFETY: result is non-null (checked above)
100    let schema = unsafe { (*result).inner.schema() };
101    let handle = Box::new(LaminarSchema::new(schema));
102
103    // SAFETY: out is non-null (checked above)
104    unsafe { *out = Box::into_raw(handle) };
105    LAMINAR_OK
106}
107
108/// Get the total row count from a query result.
109///
110/// # Arguments
111///
112/// * `result` - Query result handle
113/// * `out` - Pointer to receive row count
114///
115/// # Returns
116///
117/// `LAMINAR_OK` on success, or an error code.
118///
119/// # Safety
120///
121/// * `result` must be a valid query result handle
122/// * `out` must be a valid pointer
123#[no_mangle]
124pub unsafe extern "C" fn laminar_result_num_rows(
125    result: *mut LaminarQueryResult,
126    out: *mut usize,
127) -> i32 {
128    clear_last_error();
129
130    if result.is_null() || out.is_null() {
131        return LAMINAR_ERR_NULL_POINTER;
132    }
133
134    // SAFETY: result and out are non-null (checked above)
135    unsafe {
136        *out = (*result).inner.num_rows();
137    }
138    LAMINAR_OK
139}
140
141/// Get the number of batches in a query result.
142///
143/// # Arguments
144///
145/// * `result` - Query result handle
146/// * `out` - Pointer to receive batch count
147///
148/// # Returns
149///
150/// `LAMINAR_OK` on success, or an error code.
151///
152/// # Safety
153///
154/// * `result` must be a valid query result handle
155/// * `out` must be a valid pointer
156#[no_mangle]
157pub unsafe extern "C" fn laminar_result_num_batches(
158    result: *mut LaminarQueryResult,
159    out: *mut usize,
160) -> i32 {
161    clear_last_error();
162
163    if result.is_null() || out.is_null() {
164        return LAMINAR_ERR_NULL_POINTER;
165    }
166
167    // SAFETY: result and out are non-null (checked above)
168    unsafe {
169        *out = (*result).inner.num_batches();
170    }
171    LAMINAR_OK
172}
173
174/// Get a batch by index from a query result.
175///
176/// # Arguments
177///
178/// * `result` - Query result handle
179/// * `index` - Batch index (0-based)
180/// * `out` - Pointer to receive batch handle
181///
182/// # Returns
183///
184/// `LAMINAR_OK` on success, or an error code.
185///
186/// # Safety
187///
188/// * `result` must be a valid query result handle
189/// * `index` must be less than the batch count
190/// * `out` must be a valid pointer
191#[no_mangle]
192pub unsafe extern "C" fn laminar_result_get_batch(
193    result: *mut LaminarQueryResult,
194    index: usize,
195    out: *mut *mut LaminarRecordBatch,
196) -> i32 {
197    clear_last_error();
198
199    if result.is_null() || out.is_null() {
200        return LAMINAR_ERR_NULL_POINTER;
201    }
202
203    // SAFETY: result is non-null (checked above)
204    let result_ref = unsafe { &(*result).inner };
205
206    if let Some(batch) = result_ref.batch(index) {
207        let handle = Box::new(LaminarRecordBatch::new(batch.clone()));
208        // SAFETY: out is non-null (checked above)
209        unsafe { *out = Box::into_raw(handle) };
210        LAMINAR_OK
211    } else {
212        // Index out of bounds
213        // SAFETY: out is non-null
214        unsafe { *out = ptr::null_mut() };
215        LAMINAR_ERR_NULL_POINTER
216    }
217}
218
219/// Free a query result handle.
220///
221/// # Arguments
222///
223/// * `result` - Query result handle to free
224///
225/// # Safety
226///
227/// `result` must be a valid handle from a laminar function, or NULL.
228#[no_mangle]
229pub unsafe extern "C" fn laminar_result_free(result: *mut LaminarQueryResult) {
230    if !result.is_null() {
231        // SAFETY: result is non-null and was allocated by Box
232        drop(unsafe { Box::from_raw(result) });
233    }
234}
235
236// ============================================================================
237// Query Stream Functions
238// ============================================================================
239
240/// Get the schema from a query stream.
241///
242/// # Arguments
243///
244/// * `stream` - Query stream handle
245/// * `out` - Pointer to receive schema handle
246///
247/// # Returns
248///
249/// `LAMINAR_OK` on success, or an error code.
250///
251/// # Safety
252///
253/// * `stream` must be a valid query stream handle
254/// * `out` must be a valid pointer
255#[no_mangle]
256pub unsafe extern "C" fn laminar_stream_schema(
257    stream: *mut LaminarQueryStream,
258    out: *mut *mut LaminarSchema,
259) -> i32 {
260    clear_last_error();
261
262    if stream.is_null() || out.is_null() {
263        return LAMINAR_ERR_NULL_POINTER;
264    }
265
266    // SAFETY: stream is non-null (checked above)
267    let schema = unsafe { (*stream).inner.schema() };
268    let handle = Box::new(LaminarSchema::new(schema));
269
270    // SAFETY: out is non-null (checked above)
271    unsafe { *out = Box::into_raw(handle) };
272    LAMINAR_OK
273}
274
275/// Get the next batch from a query stream (blocking).
276///
277/// # Arguments
278///
279/// * `stream` - Query stream handle
280/// * `out` - Pointer to receive batch handle (NULL when stream exhausted)
281///
282/// # Returns
283///
284/// `LAMINAR_OK` on success, or an error code.
285///
286/// # Safety
287///
288/// * `stream` must be a valid query stream handle
289/// * `out` must be a valid pointer
290#[no_mangle]
291pub unsafe extern "C" fn laminar_stream_next(
292    stream: *mut LaminarQueryStream,
293    out: *mut *mut LaminarRecordBatch,
294) -> i32 {
295    clear_last_error();
296
297    if stream.is_null() || out.is_null() {
298        return LAMINAR_ERR_NULL_POINTER;
299    }
300
301    // SAFETY: stream is non-null (checked above)
302    let stream_ref = unsafe { &mut (*stream).inner };
303
304    match stream_ref.next() {
305        Ok(Some(batch)) => {
306            let handle = Box::new(LaminarRecordBatch::new(batch));
307            // SAFETY: out is non-null (checked above)
308            unsafe { *out = Box::into_raw(handle) };
309            LAMINAR_OK
310        }
311        Ok(None) => {
312            // Stream exhausted
313            // SAFETY: out is non-null
314            unsafe { *out = ptr::null_mut() };
315            LAMINAR_OK
316        }
317        Err(e) => {
318            // SAFETY: out is non-null
319            unsafe { *out = ptr::null_mut() };
320            let code = e.code();
321            set_last_error(e);
322            code
323        }
324    }
325}
326
327/// Try to get the next batch from a query stream (non-blocking).
328///
329/// # Arguments
330///
331/// * `stream` - Query stream handle
332/// * `out` - Pointer to receive batch handle (NULL if none available)
333///
334/// # Returns
335///
336/// `LAMINAR_OK` on success, or an error code.
337///
338/// # Safety
339///
340/// * `stream` must be a valid query stream handle
341/// * `out` must be a valid pointer
342#[no_mangle]
343pub unsafe extern "C" fn laminar_stream_try_next(
344    stream: *mut LaminarQueryStream,
345    out: *mut *mut LaminarRecordBatch,
346) -> i32 {
347    clear_last_error();
348
349    if stream.is_null() || out.is_null() {
350        return LAMINAR_ERR_NULL_POINTER;
351    }
352
353    // SAFETY: stream is non-null (checked above)
354    let stream_ref = unsafe { &mut (*stream).inner };
355
356    match stream_ref.try_next() {
357        Ok(Some(batch)) => {
358            let handle = Box::new(LaminarRecordBatch::new(batch));
359            // SAFETY: out is non-null (checked above)
360            unsafe { *out = Box::into_raw(handle) };
361            LAMINAR_OK
362        }
363        Ok(None) => {
364            // No batch available
365            // SAFETY: out is non-null
366            unsafe { *out = ptr::null_mut() };
367            LAMINAR_OK
368        }
369        Err(e) => {
370            // SAFETY: out is non-null
371            unsafe { *out = ptr::null_mut() };
372            let code = e.code();
373            set_last_error(e);
374            code
375        }
376    }
377}
378
379/// Check if a query stream is still active.
380///
381/// # Arguments
382///
383/// * `stream` - Query stream handle
384/// * `out` - Pointer to receive result (true if active)
385///
386/// # Returns
387///
388/// `LAMINAR_OK` on success, or an error code.
389///
390/// # Safety
391///
392/// * `stream` must be a valid query stream handle
393/// * `out` must be a valid pointer
394#[no_mangle]
395pub unsafe extern "C" fn laminar_stream_is_active(
396    stream: *mut LaminarQueryStream,
397    out: *mut bool,
398) -> i32 {
399    clear_last_error();
400
401    if stream.is_null() || out.is_null() {
402        return LAMINAR_ERR_NULL_POINTER;
403    }
404
405    // SAFETY: stream and out are non-null (checked above)
406    unsafe {
407        *out = (*stream).inner.is_active();
408    }
409    LAMINAR_OK
410}
411
412/// Cancel a query stream.
413///
414/// # Arguments
415///
416/// * `stream` - Query stream handle
417///
418/// # Returns
419///
420/// `LAMINAR_OK` on success, or an error code.
421///
422/// # Safety
423///
424/// `stream` must be a valid query stream handle.
425#[no_mangle]
426pub unsafe extern "C" fn laminar_stream_cancel(stream: *mut LaminarQueryStream) -> i32 {
427    clear_last_error();
428
429    if stream.is_null() {
430        return LAMINAR_ERR_NULL_POINTER;
431    }
432
433    // SAFETY: stream is non-null (checked above)
434    unsafe {
435        (*stream).inner.cancel();
436    }
437    LAMINAR_OK
438}
439
440/// Free a query stream handle.
441///
442/// # Arguments
443///
444/// * `stream` - Query stream handle to free
445///
446/// # Safety
447///
448/// `stream` must be a valid handle from a laminar function, or NULL.
449#[no_mangle]
450pub unsafe extern "C" fn laminar_stream_free(stream: *mut LaminarQueryStream) {
451    if !stream.is_null() {
452        // SAFETY: stream is non-null and was allocated by Box
453        drop(unsafe { Box::from_raw(stream) });
454    }
455}
456
457// ============================================================================
458// Record Batch Functions
459// ============================================================================
460
461/// Get the number of rows in a record batch.
462///
463/// # Arguments
464///
465/// * `batch` - Record batch handle
466/// * `out` - Pointer to receive row count
467///
468/// # Returns
469///
470/// `LAMINAR_OK` on success, or an error code.
471///
472/// # Safety
473///
474/// * `batch` must be a valid record batch handle
475/// * `out` must be a valid pointer
476#[no_mangle]
477pub unsafe extern "C" fn laminar_batch_num_rows(
478    batch: *mut LaminarRecordBatch,
479    out: *mut usize,
480) -> i32 {
481    clear_last_error();
482
483    if batch.is_null() || out.is_null() {
484        return LAMINAR_ERR_NULL_POINTER;
485    }
486
487    // SAFETY: batch and out are non-null (checked above)
488    unsafe {
489        *out = (*batch).inner.num_rows();
490    }
491    LAMINAR_OK
492}
493
494/// Get the number of columns in a record batch.
495///
496/// # Arguments
497///
498/// * `batch` - Record batch handle
499/// * `out` - Pointer to receive column count
500///
501/// # Returns
502///
503/// `LAMINAR_OK` on success, or an error code.
504///
505/// # Safety
506///
507/// * `batch` must be a valid record batch handle
508/// * `out` must be a valid pointer
509#[no_mangle]
510pub unsafe extern "C" fn laminar_batch_num_columns(
511    batch: *mut LaminarRecordBatch,
512    out: *mut usize,
513) -> i32 {
514    clear_last_error();
515
516    if batch.is_null() || out.is_null() {
517        return LAMINAR_ERR_NULL_POINTER;
518    }
519
520    // SAFETY: batch and out are non-null (checked above)
521    unsafe {
522        *out = (*batch).inner.num_columns();
523    }
524    LAMINAR_OK
525}
526
527/// Free a record batch handle.
528///
529/// # Arguments
530///
531/// * `batch` - Record batch handle to free
532///
533/// # Safety
534///
535/// `batch` must be a valid handle from a laminar function, or NULL.
536#[no_mangle]
537pub unsafe extern "C" fn laminar_batch_free(batch: *mut LaminarRecordBatch) {
538    if !batch.is_null() {
539        // SAFETY: batch is non-null and was allocated by Box
540        drop(unsafe { Box::from_raw(batch) });
541    }
542}
543
544#[cfg(test)]
545#[allow(clippy::borrow_as_ptr)]
546mod tests {
547    use super::*;
548    use crate::ffi::connection::{laminar_close, laminar_open, laminar_query};
549    use crate::ffi::schema::laminar_schema_free;
550
551    #[test]
552    fn test_result_schema() {
553        let mut conn: *mut super::super::connection::LaminarConnection = ptr::null_mut();
554        let mut result: *mut LaminarQueryResult = ptr::null_mut();
555        let mut schema: *mut LaminarSchema = ptr::null_mut();
556
557        // SAFETY: Test code with valid pointers
558        unsafe {
559            laminar_open(&mut conn);
560
561            // Create table (not source) for point-in-time queries
562            let create_sql = b"CREATE TABLE query_test (id BIGINT, val DOUBLE)\0";
563            crate::ffi::connection::laminar_execute(
564                conn,
565                create_sql.as_ptr().cast(),
566                ptr::null_mut(),
567            );
568
569            let query_sql = b"SELECT * FROM query_test\0";
570            let rc = laminar_query(conn, query_sql.as_ptr().cast(), &mut result);
571            assert_eq!(rc, LAMINAR_OK);
572
573            // Get schema
574            let rc = laminar_result_schema(result, &mut schema);
575            assert_eq!(rc, LAMINAR_OK);
576            assert!(!schema.is_null());
577
578            laminar_schema_free(schema);
579            laminar_result_free(result);
580            laminar_close(conn);
581        }
582    }
583
584    #[test]
585    fn test_result_counts() {
586        let mut conn: *mut super::super::connection::LaminarConnection = ptr::null_mut();
587        let mut result: *mut LaminarQueryResult = ptr::null_mut();
588
589        // SAFETY: Test code with valid pointers
590        unsafe {
591            laminar_open(&mut conn);
592
593            // Create table (not source) for point-in-time queries
594            let create_sql = b"CREATE TABLE count_test (id BIGINT)\0";
595            crate::ffi::connection::laminar_execute(
596                conn,
597                create_sql.as_ptr().cast(),
598                ptr::null_mut(),
599            );
600
601            let query_sql = b"SELECT * FROM count_test\0";
602            laminar_query(conn, query_sql.as_ptr().cast(), &mut result);
603
604            let mut num_rows: usize = 999;
605            let rc = laminar_result_num_rows(result, &mut num_rows);
606            assert_eq!(rc, LAMINAR_OK);
607            assert_eq!(num_rows, 0); // Empty table
608
609            let mut num_batches: usize = 999;
610            let rc = laminar_result_num_batches(result, &mut num_batches);
611            assert_eq!(rc, LAMINAR_OK);
612
613            laminar_result_free(result);
614            laminar_close(conn);
615        }
616    }
617
618    #[test]
619    fn test_batch_free_null() {
620        // SAFETY: Testing null handling
621        unsafe {
622            laminar_batch_free(ptr::null_mut());
623        }
624        // Should not crash
625    }
626}