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