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}