Skip to main content

better_duck_core/raw/
appender.rs

1use std::ffi::{c_char, CString};
2use std::ptr;
3
4use crate::error::Result;
5use crate::ffi::{
6    duckdb_appender, duckdb_appender_begin_row, duckdb_appender_close, duckdb_appender_create,
7    duckdb_appender_destroy, duckdb_appender_end_row, duckdb_appender_flush,
8};
9use crate::helpers::duck_result::result_from_duckdb_appender;
10use crate::raw::connection::RawConnection;
11use crate::types::appendable::AppendAble;
12
13/// A DuckDB appender for bulk-inserting rows into a table without going through
14/// the SQL parser.
15///
16/// Call [`append`](Appender::append) for each row and [`save`](Appender::save)
17/// to flush the data to the database. Rows are also flushed automatically on drop
18/// (errors during the implicit flush are logged to stderr).
19pub struct Appender {
20    _con: RawConnection,
21    inn: duckdb_appender,
22}
23
24impl Appender {
25    /// Creates a new `Appender` for the given table and schema.
26    ///
27    /// # Errors
28    ///
29    /// Returns an error if the table does not exist or the DuckDB appender cannot
30    /// be created.
31    pub fn new(
32        con: RawConnection,
33        table: &str,
34        schema: &str,
35    ) -> Result<Appender> {
36        let mut appender: duckdb_appender = ptr::null_mut();
37        let c_table = CString::new(table)?;
38        let c_schema = CString::new(schema)?;
39        // SAFETY: `con.con` is a valid open duckdb_connection. `c_schema` and `c_table`
40        // are valid null-terminated C strings. `appender` is a valid output pointer.
41        let res = unsafe {
42            duckdb_appender_create(
43                con.con,
44                c_schema.as_ptr() as *const c_char,
45                c_table.as_ptr() as *const c_char,
46                &mut appender,
47            )
48        };
49        result_from_duckdb_appender(res, &mut appender)
50            .map(|_| Appender { _con: con, inn: appender })
51    }
52
53    /// Appends a row to the table.
54    ///
55    /// Calls `duckdb_appender_begin_row`, then the value appender, then
56    /// `duckdb_appender_end_row`.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if the row cannot be appended.
61    #[must_use = "append result should be checked"]
62    #[allow(dead_code)]
63    pub fn append<T: AppendAble>(
64        &mut self,
65        row: &mut T,
66    ) -> Result<()> {
67        // SAFETY: `self.inn` is a valid duckdb_appender created in `new`.
68        let _ = unsafe { duckdb_appender_begin_row(self.inn) };
69        row.appender_append(self.inn)?;
70        // SAFETY: `self.inn` is a valid duckdb_appender; `begin_row` was called above.
71        let rc = unsafe { duckdb_appender_end_row(self.inn) };
72        result_from_duckdb_appender(rc, &mut self.inn)
73    }
74
75    /// Flushes all buffered rows to the database.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the flush fails.
80    #[must_use = "save result should be checked"]
81    #[allow(dead_code)]
82    pub fn save(&mut self) -> Result<()> {
83        // SAFETY: `self.inn` is a valid duckdb_appender.
84        self.flush()
85    }
86
87    /// Flushes the appender's internal buffer.
88    ///
89    /// # Safety
90    ///
91    /// `self.inn` must be a valid, non-null `duckdb_appender`.
92    fn flush(&mut self) -> Result<()> {
93        // SAFETY: `self.inn` is a valid duckdb_appender (enforced by the caller).
94        let res = unsafe { duckdb_appender_flush(self.inn) };
95        result_from_duckdb_appender(res, &mut self.inn)
96    }
97}
98
99impl Drop for Appender {
100    fn drop(&mut self) {
101        if self.inn.is_null() {
102            return;
103        }
104        // [err-result-over-panic] — log on flush failure; never panic in Drop.
105        // SAFETY: `self.inn` is non-null (checked above); it is a valid duckdb_appender
106        // created in `new`. After close and destroy it is invalidated. The null guard
107        // above ensures this runs at most once.
108        if let Err(e) = self.flush() {
109            eprintln!("[better-duck] appender flush on drop failed: {e}");
110        }
111        // SAFETY: `self.inn` is a valid, non-null duckdb_appender (null guard above).
112        // Close and destroy are safe to call in sequence; after destroy the handle is
113        // invalid and will not be used again.
114        unsafe {
115            duckdb_appender_close(self.inn);
116            duckdb_appender_destroy(&mut self.inn);
117        }
118    }
119}
120
121#[cfg(test)]
122mod appender_tests {
123    use crate::{
124        ffi::{duckdb_append_int32, duckdb_append_varchar, duckdb_bind_int32, duckdb_bind_varchar},
125        types::value::DuckValue,
126    };
127
128    use super::*;
129    use crate::{config::Config, error::DuckDBConversionError, helpers::path::path_to_cstring};
130
131    #[derive(Debug)]
132    struct Row(i32, &'static str);
133
134    impl AppendAble for Row {
135        fn appender_append(
136            &mut self,
137            appender: duckdb_appender,
138        ) -> crate::error::Result<()> {
139            // SAFETY: `appender` is a valid duckdb_appender from `Appender::new`;
140            // we are inside a begin_row/end_row pair. The int32 and varchar values are
141            // valid for their respective columns.
142            unsafe {
143                duckdb_append_int32(appender, self.0);
144                let st = CString::new(self.1)
145                    .map_err(|e| DuckDBConversionError::ConversionError(e.to_string()))
146                    .unwrap();
147                duckdb_append_varchar(appender, st.as_ptr());
148            }
149            Ok(())
150        }
151        fn stmt_append(
152            &mut self,
153            idx: u64,
154            stmt: crate::ffi::duckdb_prepared_statement,
155        ) -> Result<()> {
156            // SAFETY: `stmt` is a valid prepared statement; `idx` is a 1-based parameter
157            // index within the statement's parameter count.
158            unsafe {
159                duckdb_bind_int32(stmt, idx, self.0);
160                let st = CString::new(self.1)
161                    .map_err(|e| DuckDBConversionError::ConversionError(e.to_string()))
162                    .unwrap();
163                duckdb_bind_varchar(stmt, idx + 1, st.as_ptr());
164            }
165            Ok(())
166        }
167    }
168
169    fn get_test_connection() -> RawConnection {
170        let c_path = path_to_cstring(":memory:".as_ref()).unwrap();
171        let config = Config::default().with("duckdb_api", "rust").unwrap();
172        RawConnection::open_with_flags(&c_path, config).unwrap()
173    }
174
175    #[test]
176    fn test_appender_create_and_drop() {
177        let mut con = get_test_connection();
178
179        let create_sql = "CREATE TABLE test_appender (id INTEGER, name VARCHAR)";
180        let _ = con.query(create_sql).unwrap();
181
182        let appender = Appender::new(con.clone(), "test_appender", "main");
183        assert!(appender.is_ok());
184    }
185
186    #[test]
187    fn test_appender_append_and_flush() {
188        let mut con = get_test_connection();
189
190        let _ = con.query("CREATE TABLE test_append (id INTEGER, name VARCHAR)").unwrap();
191
192        let mut appender = Appender::new(con.clone(), "test_append", "main").unwrap();
193        let mut row = Row(1, "Alice");
194        let mut row2 = Row(2, "Sara");
195        let mut row3 = Row(3, "Charlie");
196
197        appender.append(&mut row).unwrap();
198        appender.append(&mut row2).unwrap();
199        appender.append(&mut row3).unwrap();
200        appender.save().unwrap();
201
202        let mut stmt = con.prepare("SELECT id,name FROM test_append WHERE id=123").unwrap();
203        let mut rows = stmt.execute().unwrap();
204        assert!(rows.next().is_none(), "Row with id=123 should not exist");
205
206        let mut stmt = con.prepare("SELECT id,name FROM test_append").unwrap();
207        let rows = stmt.execute().unwrap();
208        for row in rows {
209            assert!(row.is_ok());
210            let row = row.unwrap();
211            let id = match row.get("id").unwrap() {
212                DuckValue::Int(id) => id,
213                other => panic!("Expected Int for 'id', got {:?}", other),
214            };
215            assert!([1, 2, 3].contains(id), "Row with id={} should exist", id);
216            let name = match row.get("name").unwrap() {
217                DuckValue::Text(name) => name.as_str(),
218                other => panic!("Expected Str for 'name', got {:?}", other),
219            };
220            match id {
221                1 => assert_eq!(name, "Alice"),
222                2 => assert_eq!(name, "Sara"),
223                3 => assert_eq!(name, "Charlie"),
224                _ => panic!("Unexpected row id: {}", id),
225            }
226        }
227    }
228
229    #[test]
230    fn test_appender_error_on_invalid_table() {
231        let c_path = path_to_cstring(":memory:".as_ref()).unwrap();
232        let config = Config::default().with("duckdb_api", "rust").unwrap();
233        let con = RawConnection::open_with_flags(&c_path, config).unwrap();
234
235        let appender = Appender::new(con, "nonexistent_table", "main");
236        assert!(appender.is_err());
237    }
238}