better_duck_core/raw/
appender.rs1use 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
13pub struct Appender {
20 _con: RawConnection,
21 inn: duckdb_appender,
22}
23
24impl Appender {
25 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 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 #[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 let _ = unsafe { duckdb_appender_begin_row(self.inn) };
69 row.appender_append(self.inn)?;
70 let rc = unsafe { duckdb_appender_end_row(self.inn) };
72 result_from_duckdb_appender(rc, &mut self.inn)
73 }
74
75 #[must_use = "save result should be checked"]
81 #[allow(dead_code)]
82 pub fn save(&mut self) -> Result<()> {
83 self.flush()
85 }
86
87 fn flush(&mut self) -> Result<()> {
93 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 if let Err(e) = self.flush() {
109 eprintln!("[better-duck] appender flush on drop failed: {e}");
110 }
111 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 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 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}