Skip to main content

laminar_db/ffi/
error.rs

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