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}