1use std::ffi::{c_char, CString};
2use std::ptr;
3use std::sync::Arc;
4
5use crate::error::{EngineError, Error, Result};
6use crate::ffi::{
7 duckdb_append_data_chunk, duckdb_append_default, duckdb_append_default_to_chunk,
8 duckdb_appender, duckdb_appender_add_column, duckdb_appender_begin_row,
9 duckdb_appender_clear_columns, duckdb_appender_close, duckdb_appender_column_count,
10 duckdb_appender_column_type, duckdb_appender_create, duckdb_appender_create_ext,
11 duckdb_appender_create_query, duckdb_appender_destroy, duckdb_appender_end_row,
12 duckdb_appender_error_data, duckdb_appender_flush, duckdb_data_chunk_get_column_count,
13 duckdb_data_chunk_get_size, duckdb_logical_type, idx_t, DuckDBSuccess,
14};
15use crate::helpers::duck_result::result_from_duckdb_appender;
16use crate::raw::connection::ConnectionInner;
17use crate::raw::data_chunk::DataChunk;
18use crate::raw::error_data::ErrorData;
19use crate::types::appendable::AppendAble;
20use crate::types::LogicalType;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32enum AppenderState {
33 Ready,
35 Poisoned,
38 Closed,
41}
42
43pub struct Appender {
55 _connection: Arc<ConnectionInner>,
58 inn: duckdb_appender,
59 state: AppenderState,
60 rows_appended: bool,
64}
65
66impl Appender {
67 pub(crate) fn new(
81 connection: Arc<ConnectionInner>,
82 table: &str,
83 schema: &str,
84 ) -> Result<Appender> {
85 let mut appender: duckdb_appender = ptr::null_mut();
86 let c_table = CString::new(table)?;
87 let c_schema = CString::new(schema)?;
88 let res = unsafe {
92 duckdb_appender_create(
93 connection.handle(),
94 c_schema.as_ptr() as *const c_char,
95 c_table.as_ptr() as *const c_char,
96 &mut appender,
97 )
98 };
99 result_from_duckdb_appender(res, &mut appender).map(|_| Appender {
100 _connection: connection,
101 inn: appender,
102 state: AppenderState::Ready,
103 rows_appended: false,
104 })
105 }
106
107 pub(crate) fn new_ext(
115 connection: Arc<ConnectionInner>,
116 catalog: Option<&str>,
117 schema: &str,
118 table: &str,
119 ) -> Result<Appender> {
120 let c_catalog = catalog.map(CString::new).transpose()?;
121 let c_schema = CString::new(schema)?;
122 let c_table = CString::new(table)?;
123 let catalog_ptr = c_catalog.as_ref().map_or(ptr::null(), |c| c.as_ptr());
124 let mut appender: duckdb_appender = ptr::null_mut();
125 let res = unsafe {
130 duckdb_appender_create_ext(
131 connection.handle(),
132 catalog_ptr,
133 c_schema.as_ptr() as *const c_char,
134 c_table.as_ptr() as *const c_char,
135 &mut appender,
136 )
137 };
138 result_from_duckdb_appender(res, &mut appender).map(|_| Appender {
139 _connection: connection,
140 inn: appender,
141 state: AppenderState::Ready,
142 rows_appended: false,
143 })
144 }
145
146 pub(crate) fn new_query(
156 connection: Arc<ConnectionInner>,
157 query: &str,
158 types: &[LogicalType],
159 table_name: Option<&str>,
160 column_names: Option<&[&str]>,
161 ) -> Result<Appender> {
162 let c_query = CString::new(query)?;
163 let c_table = table_name.map(CString::new).transpose()?;
164 let table_ptr = c_table.as_ref().map_or(ptr::null(), |c| c.as_ptr());
165
166 let mut raw_types: Vec<duckdb_logical_type> =
168 types.iter().map(LogicalType::as_raw).collect();
169
170 let c_names: Option<Vec<CString>> = column_names
172 .map(|names| names.iter().map(|n| CString::new(*n)).collect::<Result<_, _>>())
173 .transpose()?;
174 let mut name_ptrs: Option<Vec<*const c_char>> =
175 c_names.as_ref().map(|cs| cs.iter().map(|c| c.as_ptr()).collect());
176 let names_ptr = name_ptrs.as_mut().map_or(ptr::null_mut(), |v| v.as_mut_ptr());
177
178 let mut appender: duckdb_appender = ptr::null_mut();
179 let res = unsafe {
185 duckdb_appender_create_query(
186 connection.handle(),
187 c_query.as_ptr(),
188 raw_types.len() as idx_t,
189 raw_types.as_mut_ptr(),
190 table_ptr,
191 names_ptr,
192 &mut appender,
193 )
194 };
195 result_from_duckdb_appender(res, &mut appender).map(|_| Appender {
196 _connection: connection,
197 inn: appender,
198 state: AppenderState::Ready,
199 rows_appended: false,
200 })
201 }
202
203 #[must_use]
206 pub fn column_count(&self) -> u64 {
207 unsafe { duckdb_appender_column_count(self.inn) as u64 }
209 }
210
211 #[must_use]
214 pub fn column_type(
215 &self,
216 col_idx: u64,
217 ) -> Option<LogicalType> {
218 LogicalType::from_raw(unsafe { duckdb_appender_column_type(self.inn, col_idx as idx_t) })
221 .ok()
222 }
223
224 pub fn add_column(
234 &mut self,
235 name: &str,
236 ) -> Result<()> {
237 self.ensure_configurable()?;
238 let c_name = CString::new(name)?;
239 let rc = unsafe { duckdb_appender_add_column(self.inn, c_name.as_ptr()) };
242 self.check(rc)
243 }
244
245 pub fn clear_columns(&mut self) -> Result<()> {
255 self.ensure_configurable()?;
256 let rc = unsafe { duckdb_appender_clear_columns(self.inn) };
258 self.check(rc)
259 }
260
261 pub fn append_default_row(&mut self) -> Result<()> {
273 self.ensure_ready()?;
274 let columns = self.column_count();
275
276 let begin = unsafe { duckdb_appender_begin_row(self.inn) };
278 self.check(begin)?;
279
280 let guard = RowGuard { appender: self, ended: false };
282 for _ in 0..columns {
283 let rc = unsafe { duckdb_append_default(guard.appender.inn) };
286 guard.appender.check(rc)?;
287 }
288 guard.end()?;
289 self.rows_appended = true;
290 Ok(())
291 }
292
293 pub fn append_chunk(
306 &mut self,
307 chunk: &DataChunk,
308 ) -> Result<()> {
309 self.ensure_ready()?;
310 let appender_cols = self.column_count();
311 let chunk_cols = unsafe { duckdb_data_chunk_get_column_count(chunk.0) } as u64;
313 if chunk_cols != appender_cols {
314 return Err(Error::Engine(EngineError::unavailable(Some(format!(
315 "data chunk has {chunk_cols} columns but the appender expects {appender_cols}"
316 )))));
317 }
318 let rc = unsafe { duckdb_append_data_chunk(self.inn, chunk.0) };
321 self.check(rc)?;
322 if unsafe { duckdb_data_chunk_get_size(chunk.0) } > 0 {
324 self.rows_appended = true;
325 }
326 Ok(())
327 }
328
329 pub fn append_default_to_chunk(
340 &mut self,
341 chunk: &mut DataChunk,
342 col: u64,
343 row: u64,
344 ) -> Result<()> {
345 self.ensure_ready()?;
346 let chunk_cols = unsafe { duckdb_data_chunk_get_column_count(chunk.0) } as u64;
348 if col >= chunk_cols {
349 return Err(Error::Engine(EngineError::unavailable(Some(format!(
350 "column {col} out of range for a {chunk_cols}-column chunk"
351 )))));
352 }
353 let rc = unsafe {
356 duckdb_append_default_to_chunk(self.inn, chunk.0, col as idx_t, row as idx_t)
357 };
358 self.check(rc)
359 }
360
361 fn ensure_configurable(&self) -> Result<()> {
364 self.ensure_ready()?;
365 if self.rows_appended {
366 return Err(Error::Engine(EngineError::unavailable(Some(
367 "appender columns must be configured before the first row is appended".to_owned(),
368 ))));
369 }
370 Ok(())
371 }
372
373 #[must_use = "append result should be checked"]
386 #[allow(dead_code)]
387 pub fn append<T: AppendAble>(
388 &mut self,
389 row: &mut T,
390 ) -> Result<()> {
391 self.ensure_ready()?;
392
393 let begin = unsafe { duckdb_appender_begin_row(self.inn) };
395 self.check(begin)?;
396
397 let guard = RowGuard { appender: self, ended: false };
400 guard.appender.row_result(row.appender_append(guard.appender.inn))?;
401 guard.end()?;
402 self.rows_appended = true;
404 Ok(())
405 }
406
407 #[must_use = "save result should be checked"]
414 #[allow(dead_code)]
415 pub fn save(&mut self) -> Result<()> {
416 self.ensure_ready()?;
417 self.flush()
418 }
419
420 #[allow(dead_code)]
430 pub fn finish(mut self) -> Result<()> {
431 self.ensure_ready()?;
432 let rc = unsafe { duckdb_appender_close(self.inn) };
434 self.check(rc)?;
435 self.state = AppenderState::Closed;
436 Ok(())
437 }
438
439 fn flush(&mut self) -> Result<()> {
441 let res = unsafe { duckdb_appender_flush(self.inn) };
444 self.check(res)
445 }
446
447 fn ensure_ready(&self) -> Result<()> {
449 match self.state {
450 AppenderState::Ready => Ok(()),
451 AppenderState::Poisoned => Err(Error::Engine(EngineError::unavailable(Some(
452 "appender was invalidated by an earlier failure".to_owned(),
453 )))),
454 AppenderState::Closed => Err(Error::Engine(EngineError::unavailable(Some(
455 "appender has been closed".to_owned(),
456 )))),
457 }
458 }
459
460 fn check(
469 &mut self,
470 code: crate::ffi::duckdb_state,
471 ) -> Result<()> {
472 if code == DuckDBSuccess {
473 return Ok(());
474 }
475 let engine = self.error_data().unwrap_or_else(|| {
476 EngineError::unavailable(Some(
477 "appender reported failure without error data".to_owned(),
478 ))
479 });
480 self.state = AppenderState::Poisoned;
481 Err(Error::Engine(engine))
482 }
483
484 fn row_result(
489 &mut self,
490 result: Result<()>,
491 ) -> Result<()> {
492 if result.is_err() {
493 self.state = AppenderState::Poisoned;
494 }
495 result
496 }
497
498 fn error_data(&mut self) -> Option<EngineError> {
504 let data = unsafe { ErrorData::from_raw(duckdb_appender_error_data(self.inn)) }?;
508 data.has_error().then(|| data.to_engine_error())
509 }
510}
511
512struct RowGuard<'a> {
520 appender: &'a mut Appender,
521 ended: bool,
522}
523
524impl RowGuard<'_> {
525 fn end(mut self) -> Result<()> {
527 self.ended = true;
528 let rc = unsafe { duckdb_appender_end_row(self.appender.inn) };
530 self.appender.check(rc)
531 }
532}
533
534impl Drop for RowGuard<'_> {
535 fn drop(&mut self) {
536 if self.ended {
537 return;
538 }
539 unsafe { duckdb_appender_end_row(self.appender.inn) };
543 self.appender.state = AppenderState::Poisoned;
544 }
545}
546
547impl Drop for Appender {
548 fn drop(&mut self) {
549 if self.inn.is_null() {
550 return;
551 }
552
553 if self.state == AppenderState::Ready {
558 if let Err(e) = self.flush() {
560 eprintln!("[better-duck] appender flush on drop failed: {e}");
561 }
562 }
563
564 unsafe {
569 duckdb_appender_destroy(&mut self.inn);
570 }
571 }
572}
573
574#[cfg(test)]
575mod appender_tests {
576 use crate::{
577 error::{DuckDBConversionError, Error},
578 ffi::{duckdb_append_int32, duckdb_append_varchar, duckdb_bind_int32, duckdb_bind_varchar},
579 raw::connection::RawConnection,
580 types::value::DuckValue,
581 };
582
583 use super::*;
584 use crate::{config::Config, helpers::path::path_to_cstring};
585
586 const ROW_FAILURE: &str = "intentional row failure";
587
588 #[derive(Debug)]
589 struct Row(i32, &'static str);
590
591 struct FailingRow;
592
593 impl AppendAble for FailingRow {
594 fn appender_append(
595 &mut self,
596 _appender: duckdb_appender,
597 ) -> Result<()> {
598 Err(Error::ConversionError(DuckDBConversionError::ConversionError(
599 ROW_FAILURE.to_owned(),
600 )))
601 }
602
603 fn stmt_append(
604 &mut self,
605 _idx: u64,
606 _stmt: crate::ffi::duckdb_prepared_statement,
607 ) -> Result<()> {
608 unreachable!("FailingRow is only used with Appender")
609 }
610 }
611
612 impl AppendAble for Row {
613 fn appender_append(
614 &mut self,
615 appender: duckdb_appender,
616 ) -> crate::error::Result<()> {
617 unsafe {
621 duckdb_append_int32(appender, self.0);
622 let st = CString::new(self.1)
623 .map_err(|e| DuckDBConversionError::ConversionError(e.to_string()))
624 .unwrap();
625 duckdb_append_varchar(appender, st.as_ptr());
626 }
627 Ok(())
628 }
629 fn stmt_append(
630 &mut self,
631 idx: u64,
632 stmt: crate::ffi::duckdb_prepared_statement,
633 ) -> Result<()> {
634 unsafe {
637 duckdb_bind_int32(stmt, idx, self.0);
638 let st = CString::new(self.1)
639 .map_err(|e| DuckDBConversionError::ConversionError(e.to_string()))
640 .unwrap();
641 duckdb_bind_varchar(stmt, idx + 1, st.as_ptr());
642 }
643 Ok(())
644 }
645 }
646
647 fn get_test_connection() -> RawConnection {
648 let c_path = path_to_cstring(":memory:".as_ref()).unwrap();
649 let config = Config::default().with("duckdb_api", "rust").unwrap();
650 RawConnection::open_with_flags(&c_path, config).unwrap()
651 }
652
653 fn single_int(
654 con: &RawConnection,
655 sql: &str,
656 column: &str,
657 ) -> i32 {
658 let mut result = con.prepare(sql).unwrap().execute().unwrap();
659 let row = result.next().expect("expected one row").unwrap();
660 match row.get(column).unwrap() {
661 DuckValue::Int(value) => *value,
662 other => panic!("Expected Int for '{column}', got {other:?}"),
663 }
664 }
665
666 fn assert_row_exists(
667 con: &RawConnection,
668 table: &str,
669 id: i32,
670 ) {
671 let sql = format!("SELECT id FROM {table} WHERE id = {id}");
672 assert_eq!(single_int(con, &sql, "id"), id);
673 }
674
675 #[test]
676 fn test_appender_create_and_drop() {
677 let mut con = get_test_connection();
678
679 let create_sql = "CREATE TABLE test_appender (id INTEGER, name VARCHAR)";
680 let _ = con.query(create_sql).unwrap();
681
682 let appender = con.appender("test_appender", "main");
683 assert!(appender.is_ok());
684 }
685
686 #[test]
687 fn test_appender_append_and_flush() {
688 let mut con = get_test_connection();
689
690 let _ = con.query("CREATE TABLE test_append (id INTEGER, name VARCHAR)").unwrap();
691
692 let mut appender = con.appender("test_append", "main").unwrap();
693 let mut row = Row(1, "Alice");
694 let mut row2 = Row(2, "Sara");
695 let mut row3 = Row(3, "Charlie");
696
697 appender.append(&mut row).unwrap();
698 appender.append(&mut row2).unwrap();
699 appender.append(&mut row3).unwrap();
700 appender.save().unwrap();
701
702 let mut stmt = con.prepare("SELECT id,name FROM test_append WHERE id=123").unwrap();
703 let mut rows = stmt.execute().unwrap();
704 assert!(rows.next().is_none(), "Row with id=123 should not exist");
705
706 let mut stmt = con.prepare("SELECT id,name FROM test_append").unwrap();
707 let rows = stmt.execute().unwrap();
708 for row in rows {
709 assert!(row.is_ok());
710 let row = row.unwrap();
711 let id = match row.get("id").unwrap() {
712 DuckValue::Int(id) => id,
713 other => panic!("Expected Int for 'id', got {:?}", other),
714 };
715 assert!([1, 2, 3].contains(id), "Row with id={} should exist", id);
716 let name = match row.get("name").unwrap() {
717 DuckValue::Text(name) => name.as_str(),
718 other => panic!("Expected Str for 'name', got {:?}", other),
719 };
720 match id {
721 1 => assert_eq!(name, "Alice"),
722 2 => assert_eq!(name, "Sara"),
723 3 => assert_eq!(name, "Charlie"),
724 _ => panic!("Unexpected row id: {}", id),
725 }
726 }
727 }
728
729 #[test]
730 fn test_appender_rejects_interior_nul_table_name() {
731 let error = get_test_connection()
732 .appender("test\0table", "main")
733 .err()
734 .expect("interior NUL table name should fail");
735 assert!(matches!(error, Error::NulError(_)));
736 }
737
738 #[test]
739 fn test_appender_rejects_interior_nul_schema_name() {
740 let error = get_test_connection()
741 .appender("test_table", "ma\0in")
742 .err()
743 .expect("interior NUL schema name should fail");
744 assert!(matches!(error, Error::NulError(_)));
745 }
746
747 #[test]
748 fn test_appender_error_on_nonexistent_schema() {
749 let mut con = get_test_connection();
750 con.query("CREATE TABLE test_table (id INTEGER)").unwrap();
751
752 let appender = con.appender("test_table", "nonexistent_schema");
753 assert!(appender.is_err());
754 }
755
756 #[test]
757 fn test_appender_error_on_nonexistent_table() {
758 let mut con = get_test_connection();
759
760 let appender = con.appender("nonexistent_table", "main");
761 assert!(appender.is_err());
762 }
763
764 #[test]
765 fn test_save_flushes_rows_for_another_connection() {
766 let mut con = get_test_connection();
767 con.query("CREATE TABLE saved_rows (id INTEGER, name VARCHAR)").unwrap();
768 let reader = con.try_clone().unwrap();
769 let mut appender = con.appender("saved_rows", "main").unwrap();
770
771 appender.append(&mut Row(11, "saved")).unwrap();
772 appender.save().unwrap();
773
774 assert_row_exists(&reader, "saved_rows", 11);
775 }
776
777 #[test]
778 fn test_drop_flushes_rows_for_another_connection() {
779 let mut con = get_test_connection();
780 con.query("CREATE TABLE dropped_rows (id INTEGER, name VARCHAR)").unwrap();
781 let reader = con.try_clone().unwrap();
782
783 {
784 let mut appender = con.appender("dropped_rows", "main").unwrap();
785 appender.append(&mut Row(12, "dropped")).unwrap();
786 }
787
788 assert_row_exists(&reader, "dropped_rows", 12);
789 }
790
791 #[test]
792 fn test_append_propagates_row_error_and_connection_remains_usable() {
793 let mut con = get_test_connection();
794 con.query("CREATE TABLE failed_rows (id INTEGER, name VARCHAR)").unwrap();
795 let mut appender = con.appender("failed_rows", "main").unwrap();
796
797 let error = appender.append(&mut FailingRow).unwrap_err();
798 assert!(matches!(
799 error,
800 Error::ConversionError(DuckDBConversionError::ConversionError(ref message))
801 if message == ROW_FAILURE
802 ));
803 drop(appender);
804
805 con.query("INSERT INTO failed_rows VALUES (13, 'usable')").unwrap();
806 assert_row_exists(&con, "failed_rows", 13);
807 }
808
809 #[test]
812 fn engine_append_failure_surfaces_typed_error_data() {
813 use crate::error::EngineErrorKind;
814
815 let mut con = get_test_connection();
816 con.query("CREATE TABLE typed_fail (id INTEGER CHECK (id > 0))").unwrap();
817 let mut appender = con.appender("typed_fail", "main").unwrap();
818
819 appender.append(&mut DuckValue::Int(-1)).unwrap();
822 let error = appender.save().unwrap_err();
823
824 let engine = match error {
825 Error::Engine(engine) => engine,
826 other => panic!("expected a typed engine error, got {other:?}"),
827 };
828 assert_ne!(engine.kind, EngineErrorKind::Unavailable, "kind should be typed by DuckDB");
831 assert!(engine.message.is_some(), "DuckDB should supply a message");
832 }
833
834 #[test]
849 fn poisoned_appender_rejects_further_operations() {
850 let mut con = get_test_connection();
851 con.query("CREATE TABLE reuse_fail (id INTEGER CHECK (id > 0))").unwrap();
852 let mut appender = con.appender("reuse_fail", "main").unwrap();
853
854 appender.append(&mut DuckValue::Int(-1)).unwrap();
855 appender.save().unwrap_err(); let err = appender.append(&mut DuckValue::Int(5)).unwrap_err();
859 assert!(
860 matches!(
861 err,
862 Error::Engine(ref e) if e.message.as_deref() == Some("appender was invalidated by an earlier failure")
863 ),
864 "unexpected error: {err:?}"
865 );
866 assert!(appender.save().is_err());
867 }
868
869 #[test]
872 fn row_side_failure_poisons_and_leaves_connection_usable() {
873 let mut con = get_test_connection();
874 con.query("CREATE TABLE row_poison (id INTEGER, name VARCHAR)").unwrap();
875 let mut appender = con.appender("row_poison", "main").unwrap();
876
877 appender.append(&mut FailingRow).unwrap_err();
878 assert!(appender.append(&mut Row(1, "later")).is_err());
880 drop(appender);
881
882 con.query("INSERT INTO row_poison VALUES (7, 'ok')").unwrap();
884 assert_row_exists(&con, "row_poison", 7);
885 }
886
887 #[test]
890 fn finish_commits_rows_and_reports_success() {
891 let mut con = get_test_connection();
892 con.query("CREATE TABLE finished (id INTEGER, name VARCHAR)").unwrap();
893 let reader = con.try_clone().unwrap();
894
895 let mut appender = con.appender("finished", "main").unwrap();
896 appender.append(&mut Row(21, "done")).unwrap();
897 appender.finish().unwrap();
898
899 assert_row_exists(&reader, "finished", 21);
900 }
901
902 #[test]
905 fn finish_reports_flush_failure() {
906 let mut con = get_test_connection();
907 con.query("CREATE TABLE finish_fail (id INTEGER CHECK (id > 0))").unwrap();
908 let mut appender = con.appender("finish_fail", "main").unwrap();
909
910 appender.append(&mut DuckValue::Int(-5)).unwrap();
911 let error = appender.finish().unwrap_err();
912 assert!(matches!(error, Error::Engine(_)), "expected typed engine error, got {error:?}");
913 }
914
915 #[test]
919 fn finish_on_poisoned_appender_reports_poison_without_reclose() {
920 let mut con = get_test_connection();
921 con.query("CREATE TABLE finish_poisoned (id INTEGER CHECK (id > 0))").unwrap();
922 let mut appender = con.appender("finish_poisoned", "main").unwrap();
923
924 appender.append(&mut DuckValue::Int(-1)).unwrap();
925 appender.save().unwrap_err(); let error = appender.finish().unwrap_err();
928 assert!(
929 matches!(error, Error::Engine(ref e)
930 if e.message.as_deref() == Some("appender was invalidated by an earlier failure")),
931 "finish on a poisoned appender must report the poison, got {error:?}"
932 );
933 }
934
935 #[test]
939 fn drop_with_pending_violation_logs_and_leaves_connection_usable() {
940 let mut con = get_test_connection();
941 con.query("CREATE TABLE drop_fail (id INTEGER CHECK (id > 0))").unwrap();
942 {
943 let mut appender = con.appender("drop_fail", "main").unwrap();
944 appender.append(&mut DuckValue::Int(-9)).unwrap();
946 }
948 con.query("INSERT INTO drop_fail VALUES (3)").unwrap();
950 assert_row_exists(&con, "drop_fail", 3);
951 let mut rows = con.query("SELECT count(*) AS n FROM drop_fail").unwrap();
952 let row = rows.next().unwrap().unwrap();
953 assert_eq!(row.get("n").unwrap(), &DuckValue::BigInt(1), "only the valid row persisted");
954 }
955
956 mod builders {
958 use crate::connection::Connection;
959 use crate::ffi::DUCKDB_TYPE_DUCKDB_TYPE_INTEGER;
960 use crate::types::value::DuckValue;
961 use crate::types::LogicalType;
962
963 #[test]
964 fn appender_ext_default_and_named_catalog() {
965 let mut conn = Connection::open_in_memory().unwrap();
966 conn.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
967
968 {
970 let mut app = conn.appender_ext(None, "main", "t").unwrap();
971 app.append(&mut DuckValue::Int(1)).unwrap();
972 app.save().unwrap();
973 }
974 {
976 let mut app = conn.appender_ext(Some("memory"), "main", "t").unwrap();
977 app.append(&mut DuckValue::Int(2)).unwrap();
978 app.save().unwrap();
979 }
980 let rows: Vec<_> = conn
981 .execute("SELECT id FROM t ORDER BY id")
982 .unwrap()
983 .collect::<Result<_, _>>()
984 .unwrap();
985 assert_eq!(rows.len(), 2);
986 assert_eq!(rows[0].get("id"), Some(&DuckValue::Int(1)));
987 assert_eq!(rows[1].get("id"), Some(&DuckValue::Int(2)));
988 }
989
990 #[test]
991 fn column_count_and_type_reflect_the_table() {
992 let mut conn = Connection::open_in_memory().unwrap();
993 conn.execute_batch("CREATE TABLE t (id INTEGER, name VARCHAR)").unwrap();
994 let app = conn.appender("t", "main").unwrap();
995 assert_eq!(app.column_count(), 2);
996 let ty = app.column_type(0).expect("column 0 type");
997 assert_eq!(ty.type_id(), DUCKDB_TYPE_DUCKDB_TYPE_INTEGER);
998 }
999
1000 #[test]
1001 fn add_column_projects_and_fills_defaults() {
1002 let mut conn = Connection::open_in_memory().unwrap();
1003 conn.execute_batch("CREATE TABLE t (a INTEGER, b INTEGER DEFAULT 99)").unwrap();
1004 {
1005 let mut app = conn.appender("t", "main").unwrap();
1006 app.add_column("a").unwrap();
1008 assert_eq!(app.column_count(), 1, "active column list is just `a`");
1009 app.append(&mut DuckValue::Int(7)).unwrap();
1010 app.save().unwrap();
1011 }
1012 let mut rows = conn.execute("SELECT a, b FROM t").unwrap();
1013 let row = rows.next().unwrap().unwrap();
1014 assert_eq!(row.get("a"), Some(&DuckValue::Int(7)));
1015 assert_eq!(
1016 row.get("b"),
1017 Some(&DuckValue::Int(99)),
1018 "DEFAULT filled the unprojected column"
1019 );
1020 }
1021
1022 #[test]
1023 fn clear_columns_restores_full_projection() {
1024 let mut conn = Connection::open_in_memory().unwrap();
1025 conn.execute_batch("CREATE TABLE t (a INTEGER, b INTEGER)").unwrap();
1026 let mut app = conn.appender("t", "main").unwrap();
1027 app.add_column("a").unwrap();
1028 assert_eq!(app.column_count(), 1);
1029 app.clear_columns().unwrap();
1030 assert_eq!(app.column_count(), 2, "clear_columns restores every column");
1031 }
1032
1033 #[test]
1034 fn configuration_after_first_row_is_rejected() {
1035 let mut conn = Connection::open_in_memory().unwrap();
1036 conn.execute_batch("CREATE TABLE t (a INTEGER)").unwrap();
1038 let mut app = conn.appender("t", "main").unwrap();
1039 app.append(&mut DuckValue::Int(1)).unwrap();
1040 assert!(app.add_column("a").is_err(), "add_column after a row must be rejected");
1042 assert!(app.clear_columns().is_err(), "clear_columns after a row must be rejected");
1043 }
1044
1045 #[test]
1046 fn appender_query_feeds_an_insert() {
1047 let mut conn = Connection::open_in_memory().unwrap();
1048 conn.execute_batch("CREATE TABLE dest (v INTEGER)").unwrap();
1049 let types = [LogicalType::of::<i32>().unwrap()];
1050 {
1051 let mut app = conn
1052 .appender_query(
1053 "INSERT INTO dest SELECT * FROM appended_data",
1054 &types,
1055 None,
1056 None,
1057 )
1058 .unwrap();
1059 app.append(&mut DuckValue::Int(42)).unwrap();
1060 app.append(&mut DuckValue::Int(43)).unwrap();
1061 app.save().unwrap();
1062 }
1063 let rows: Vec<_> = conn
1064 .execute("SELECT v FROM dest ORDER BY v")
1065 .unwrap()
1066 .collect::<Result<_, _>>()
1067 .unwrap();
1068 assert_eq!(rows.len(), 2);
1069 assert_eq!(rows[0].get("v"), Some(&DuckValue::Int(42)));
1070 assert_eq!(rows[1].get("v"), Some(&DuckValue::Int(43)));
1071 }
1072 }
1073
1074 mod chunk_ingestion {
1076 use crate::connection::Connection;
1077 use crate::raw::data_chunk::DataChunk;
1078 use crate::types::value::DuckValue;
1079 use crate::types::LogicalType;
1080
1081 #[test]
1082 fn append_default_row_fills_table_defaults_and_nulls() {
1083 let mut conn = Connection::open_in_memory().unwrap();
1084 conn.execute_batch("CREATE TABLE t (a INTEGER DEFAULT 5, b INTEGER)").unwrap();
1086 {
1087 let mut app = conn.appender("t", "main").unwrap();
1088 app.append_default_row().unwrap();
1089 app.save().unwrap();
1090 }
1091 let mut rows = conn.execute("SELECT a, b FROM t").unwrap();
1092 let row = rows.next().unwrap().unwrap();
1093 assert_eq!(row.get("a"), Some(&DuckValue::Int(5)), "DEFAULT applied");
1094 assert_eq!(row.get("b"), Some(&DuckValue::Null), "no DEFAULT -> NULL");
1095 }
1096
1097 #[test]
1098 fn append_chunk_copies_rows_and_validates_column_count() {
1099 let mut conn = Connection::open_in_memory().unwrap();
1100 conn.execute_batch("CREATE TABLE src (v INTEGER)").unwrap();
1101 conn.execute_batch("INSERT INTO src VALUES (1), (2), (3)").unwrap();
1102 conn.execute_batch("CREATE TABLE dst (v INTEGER)").unwrap();
1103
1104 let chunk = {
1106 let result = conn.execute("SELECT v FROM src ORDER BY v").unwrap();
1107 DataChunk::from_result(&result).expect("a chunk").unwrap()
1108 };
1109
1110 {
1111 let mut app = conn.appender("dst", "main").unwrap();
1112 app.append_chunk(&chunk).unwrap();
1113 app.save().unwrap();
1114 }
1115 let rows: Vec<_> = conn
1116 .execute("SELECT v FROM dst ORDER BY v")
1117 .unwrap()
1118 .collect::<Result<_, _>>()
1119 .unwrap();
1120 assert_eq!(rows.len(), 3);
1121 assert_eq!(rows[0].get("v"), Some(&DuckValue::Int(1)));
1122 assert_eq!(rows[2].get("v"), Some(&DuckValue::Int(3)));
1123
1124 conn.execute_batch("CREATE TABLE dst2 (v INTEGER, w INTEGER)").unwrap();
1127 let mut app2 = conn.appender("dst2", "main").unwrap();
1128 assert!(app2.append_chunk(&chunk).is_err(), "column-count mismatch must error");
1129 }
1130
1131 #[test]
1132 fn append_default_to_chunk_fills_a_cell() {
1133 let mut conn = Connection::open_in_memory().unwrap();
1134 conn.execute_batch("CREATE TABLE t (a INTEGER DEFAULT 42)").unwrap();
1135
1136 let int_ty = LogicalType::of::<i32>().unwrap();
1138 let mut raw_types = [int_ty.as_raw()];
1139 let raw = unsafe { crate::ffi::duckdb_create_data_chunk(raw_types.as_mut_ptr(), 1) };
1143 let mut chunk = DataChunk::new(raw).unwrap();
1144 drop(int_ty);
1145
1146 {
1147 let mut app = conn.appender("t", "main").unwrap();
1148 app.append_default_to_chunk(&mut chunk, 0, 0).unwrap();
1150 unsafe { crate::ffi::duckdb_data_chunk_set_size(*chunk, 1) };
1152 app.append_chunk(&chunk).unwrap();
1153 app.save().unwrap();
1154
1155 assert!(app.append_default_to_chunk(&mut chunk, 5, 0).is_err());
1157 }
1158 let mut rows = conn.execute("SELECT a FROM t").unwrap();
1159 let row = rows.next().unwrap().unwrap();
1160 assert_eq!(row.get("a"), Some(&DuckValue::Int(42)));
1161 }
1162 }
1163}