Skip to main content

laminar_db/ffi/
error.rs

1//! Thread-local error storage for FFI.
2//!
3//! C functions return error codes; this module stores the last error message
4//! so callers can retrieve it via `laminar_last_error()`.
5
6use std::cell::RefCell;
7use std::ffi::{c_char, CString};
8use std::ptr;
9
10use crate::api::ApiError;
11
12// Error code constants
13
14/// Success return code.
15pub const LAMINAR_OK: i32 = 0;
16/// Null pointer was passed to a function.
17pub const LAMINAR_ERR_NULL_POINTER: i32 = -1;
18/// Invalid UTF-8 string.
19pub const LAMINAR_ERR_INVALID_UTF8: i32 = -2;
20/// Connection error.
21pub const LAMINAR_ERR_CONNECTION: i32 = 100;
22/// Table/source not found.
23pub const LAMINAR_ERR_TABLE_NOT_FOUND: i32 = 200;
24/// Table/source already exists.
25pub const LAMINAR_ERR_TABLE_EXISTS: i32 = 201;
26/// Schema mismatch error.
27pub const LAMINAR_ERR_SCHEMA_MISMATCH: i32 = 202;
28/// Data ingestion error.
29pub const LAMINAR_ERR_INGESTION: i32 = 300;
30/// Query execution error.
31pub const LAMINAR_ERR_QUERY: i32 = 400;
32/// Subscription error.
33pub const LAMINAR_ERR_SUBSCRIPTION: i32 = 500;
34/// Internal error.
35pub const LAMINAR_ERR_INTERNAL: i32 = 900;
36/// Database is shutting down.
37pub const LAMINAR_ERR_SHUTDOWN: i32 = 901;
38
39// Thread-local storage for the last error.
40thread_local! {
41    static LAST_ERROR: RefCell<Option<StoredError>> = const { RefCell::new(None) };
42}
43
44/// Stored error with pre-allocated C string.
45struct StoredError {
46    error: ApiError,
47    c_message: CString,
48}
49
50/// Store an error for later retrieval.
51pub(crate) fn set_last_error(err: ApiError) {
52    let c_message = CString::new(err.message())
53        .unwrap_or_else(|_| CString::new("Error message contained null byte").unwrap());
54    LAST_ERROR.with(|e| {
55        *e.borrow_mut() = Some(StoredError {
56            error: err,
57            c_message,
58        });
59    });
60}
61
62/// Clear the last error.
63pub(crate) fn clear_last_error() {
64    LAST_ERROR.with(|e| *e.borrow_mut() = None);
65}
66
67/// Get the last error code, or 0 if none.
68#[must_use]
69pub(crate) fn last_error_code() -> i32 {
70    LAST_ERROR.with(|e| {
71        e.borrow()
72            .as_ref()
73            .map_or(LAMINAR_OK, |stored| api_error_to_ffi_code(&stored.error))
74    })
75}
76
77/// Convert `ApiError` to FFI error code.
78fn api_error_to_ffi_code(err: &ApiError) -> i32 {
79    use crate::api::codes;
80    let code = err.code();
81    match code {
82        codes::CONNECTION_FAILED => LAMINAR_ERR_CONNECTION,
83        codes::TABLE_NOT_FOUND => LAMINAR_ERR_TABLE_NOT_FOUND,
84        codes::TABLE_EXISTS => LAMINAR_ERR_TABLE_EXISTS,
85        codes::SCHEMA_MISMATCH => LAMINAR_ERR_SCHEMA_MISMATCH,
86        codes::INGESTION_FAILED => LAMINAR_ERR_INGESTION,
87        codes::QUERY_FAILED => LAMINAR_ERR_QUERY,
88        codes::SUBSCRIPTION_FAILED | codes::SUBSCRIPTION_CLOSED | codes::SUBSCRIPTION_TIMEOUT => {
89            LAMINAR_ERR_SUBSCRIPTION
90        }
91        codes::SHUTDOWN => LAMINAR_ERR_SHUTDOWN,
92        // All other codes (including INTERNAL_ERROR) map to INTERNAL
93        _ => LAMINAR_ERR_INTERNAL,
94    }
95}
96
97/// Get the last error message.
98///
99/// Returns a pointer to a null-terminated string, or null if no error.
100/// The pointer is valid until the next FFI call on the same thread.
101///
102/// # Safety
103///
104/// The returned pointer is valid until the next laminar_* call on this thread.
105#[no_mangle]
106pub extern "C" fn laminar_last_error() -> *const c_char {
107    LAST_ERROR.with(|e| match &*e.borrow() {
108        Some(stored) => stored.c_message.as_ptr(),
109        None => ptr::null(),
110    })
111}
112
113/// Get the last error code.
114///
115/// Returns 0 (`LAMINAR_OK`) if no error has occurred.
116#[no_mangle]
117pub extern "C" fn laminar_last_error_code() -> i32 {
118    last_error_code()
119}
120
121/// Clear the last error.
122///
123/// Call this to reset error state before a sequence of operations.
124#[no_mangle]
125pub extern "C" fn laminar_clear_error() {
126    clear_last_error();
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_no_error_returns_null() {
135        clear_last_error();
136        assert!(laminar_last_error().is_null());
137        assert_eq!(laminar_last_error_code(), LAMINAR_OK);
138    }
139
140    #[test]
141    fn test_set_and_get_error() {
142        let err = ApiError::table_not_found("test_table");
143        set_last_error(err);
144
145        let code = laminar_last_error_code();
146        assert_eq!(code, LAMINAR_ERR_TABLE_NOT_FOUND);
147
148        let error_ptr = laminar_last_error();
149        assert!(!error_ptr.is_null());
150
151        // SAFETY: We just set this error, pointer is valid
152        let error_cstr = unsafe { std::ffi::CStr::from_ptr(error_ptr) };
153        let message = error_cstr.to_str().unwrap();
154        assert!(message.contains("test_table"));
155    }
156
157    #[test]
158    fn test_clear_error() {
159        set_last_error(ApiError::internal("test"));
160        assert!(!laminar_last_error().is_null());
161
162        laminar_clear_error();
163        assert!(laminar_last_error().is_null());
164        assert_eq!(laminar_last_error_code(), LAMINAR_OK);
165    }
166
167    #[test]
168    fn test_error_code_mapping() {
169        // Connection error
170        set_last_error(ApiError::connection("conn fail"));
171        assert_eq!(laminar_last_error_code(), LAMINAR_ERR_CONNECTION);
172
173        // Query error
174        set_last_error(ApiError::Query {
175            code: crate::api::codes::QUERY_FAILED,
176            message: "query fail".into(),
177        });
178        assert_eq!(laminar_last_error_code(), LAMINAR_ERR_QUERY);
179
180        // Shutdown
181        set_last_error(ApiError::shutdown());
182        assert_eq!(laminar_last_error_code(), LAMINAR_ERR_SHUTDOWN);
183    }
184}