diff --git a/crates/duckdb/examples/appender.rs b/crates/duckdb/examples/appender.rs index c493e3cf..49b796b8 100644 --- a/crates/duckdb/examples/appender.rs +++ b/crates/duckdb/examples/appender.rs @@ -1,4 +1,4 @@ -use duckdb::{params, Connection, DropBehavior, Result}; +use duckdb::{params, types::AppendDefault, Connection, DropBehavior, Result}; fn main() -> Result<()> { //let mut db = Connection::open("10m.db")?; @@ -10,7 +10,7 @@ fn main() -> Result<()> { id INTEGER not null, -- primary key, area CHAR(6), age TINYINT not null, - active TINYINT not null + active TINYINT DEFAULT 1, );"; db.execute_batch(create_table_sql)?; @@ -25,12 +25,7 @@ fn main() -> Result<()> { // } for i in 0..row_count { - app.append_row(params![ - i, - get_random_area_code(), - get_random_age(), - get_random_active(), - ])?; + app.append_row(params![i, get_random_area_code(), get_random_age(), AppendDefault])?; } } diff --git a/crates/duckdb/src/appender/mod.rs b/crates/duckdb/src/appender/mod.rs index 34e10730..a3f5230f 100644 --- a/crates/duckdb/src/appender/mod.rs +++ b/crates/duckdb/src/appender/mod.rs @@ -83,6 +83,16 @@ impl Appender<'_> { result_from_duckdb_appender(rc, &mut self.app) } + /// Append a DEFAULT value to the current row + #[inline] + fn append_default(&mut self) -> Result<()> { + let rc = unsafe { ffi::duckdb_append_default(self.app) }; + if rc != 0 { + return Err(Error::AppendError); + } + Ok(()) + } + #[inline] pub(crate) fn bind_parameters

(&mut self, params: P) -> Result<()> where @@ -95,13 +105,14 @@ impl Appender<'_> { Ok(()) } - fn bind_parameter(&self, param: &P) -> Result<()> { + fn bind_parameter(&mut self, param: &P) -> Result<()> { let value = param.to_sql()?; let ptr = self.app; let value = match value { ToSqlOutput::Borrowed(v) => v, ToSqlOutput::Owned(ref v) => ValueRef::from(v), + ToSqlOutput::AppendDefault => return self.append_default(), }; // NOTE: we ignore the return value here // because if anything failed, end_row will fail @@ -189,7 +200,7 @@ impl fmt::Debug for Appender<'_> { #[cfg(test)] mod test { - use crate::{params, Connection, Error, Result}; + use crate::{params, types::AppendDefault, Connection, Error, Result}; #[test] fn test_append_one_row() -> Result<()> { @@ -389,4 +400,50 @@ mod test { Ok(()) } + + #[test] + fn test_append_default() -> Result<()> { + let db = Connection::open_in_memory()?; + db.execute_batch( + "CREATE TABLE test ( + id INTEGER, + name VARCHAR, + status VARCHAR DEFAULT 'active' + )", + )?; + + { + let mut app = db.appender("test")?; + app.append_row(params![1, "Alice", AppendDefault])?; + app.append_row(params![2, "Bob", AppendDefault])?; + app.append_row(params![3, AppendDefault, AppendDefault])?; + app.append_row(params![4, None::, "inactive"])?; + } + + let rows: Vec<(i32, Option, String)> = db + .prepare("SELECT id, name, status FROM test ORDER BY id")? + .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))? + .collect::>>()?; + + assert_eq!(rows.len(), 4); + assert_eq!(rows[0], (1, Some("Alice".to_string()), "active".to_string())); + assert_eq!(rows[1], (2, Some("Bob".to_string()), "active".to_string())); + assert_eq!(rows[2], (3, None, "active".to_string())); + assert_eq!(rows[3], (4, None, "inactive".to_string())); + + Ok(()) + } + + #[test] + fn test_append_default_in_prepared_statement_fails() -> Result<()> { + let db = Connection::open_in_memory()?; + db.execute_batch("CREATE TABLE test (id INTEGER, name VARCHAR DEFAULT 'test')")?; + + let mut stmt = db.prepare("INSERT INTO test VALUES (?, ?)")?; + let result = stmt.execute(params![1, AppendDefault]); + + assert!(matches!(result, Err(Error::ToSqlConversionFailure(_)))); + + Ok(()) + } } diff --git a/crates/duckdb/src/pragma.rs b/crates/duckdb/src/pragma.rs index 9b13f152..4cc89279 100644 --- a/crates/duckdb/src/pragma.rs +++ b/crates/duckdb/src/pragma.rs @@ -61,6 +61,12 @@ impl Sql { let value = match value { ToSqlOutput::Borrowed(v) => v, ToSqlOutput::Owned(ref v) => ValueRef::from(v), + ToSqlOutput::AppendDefault => { + return Err(Error::ToSqlConversionFailure(Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "AppendDefault is only valid for Appender operations, not for pragmas", + )))); + } }; match value { ValueRef::BigInt(i) => { diff --git a/crates/duckdb/src/statement.rs b/crates/duckdb/src/statement.rs index 527c1bb0..8126793f 100644 --- a/crates/duckdb/src/statement.rs +++ b/crates/duckdb/src/statement.rs @@ -534,6 +534,12 @@ impl Statement<'_> { let value = match value { ToSqlOutput::Borrowed(v) => v, ToSqlOutput::Owned(ref v) => ValueRef::from(v), + ToSqlOutput::AppendDefault => { + return Err(Error::ToSqlConversionFailure(Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "AppendDefault is only valid for Appender operations, not for prepared statements", + )))); + } }; // TODO: bind more let rc = match value { diff --git a/crates/duckdb/src/types/chrono.rs b/crates/duckdb/src/types/chrono.rs index 122f890b..061acb1e 100644 --- a/crates/duckdb/src/types/chrono.rs +++ b/crates/duckdb/src/types/chrono.rs @@ -284,6 +284,7 @@ mod test { let value = match sqled { ToSqlOutput::Borrowed(v) => v, ToSqlOutput::Owned(ref v) => ValueRef::from(v), + ToSqlOutput::AppendDefault => unreachable!(), }; let reversed = FromSql::column_result(value).unwrap(); diff --git a/crates/duckdb/src/types/mod.rs b/crates/duckdb/src/types/mod.rs index 427831b3..3a7c2441 100644 --- a/crates/duckdb/src/types/mod.rs +++ b/crates/duckdb/src/types/mod.rs @@ -6,7 +6,7 @@ pub use self::{ from_sql::{FromSql, FromSqlError, FromSqlResult}, ordered_map::OrderedMap, string::DuckString, - to_sql::{ToSql, ToSqlOutput}, + to_sql::{AppendDefault, ToSql, ToSqlOutput}, value::Value, value_ref::{EnumType, ListType, TimeUnit, ValueRef}, }; diff --git a/crates/duckdb/src/types/to_sql.rs b/crates/duckdb/src/types/to_sql.rs index 14264881..073fb05b 100644 --- a/crates/duckdb/src/types/to_sql.rs +++ b/crates/duckdb/src/types/to_sql.rs @@ -2,6 +2,37 @@ use super::{Null, TimeUnit, Value, ValueRef}; use crate::Result; use std::borrow::Cow; +/// Marker type that can be used in Appender params to indicate DEFAULT value. +/// +/// This is useful when you want to append a row with some columns using their +/// default values (as defined in the table schema). Unlike `Null` which explicitly +/// sets a column to NULL, `AppendDefault` uses the column's DEFAULT expression. +/// +/// ## Limitations +/// +/// `AppendDefault` only works with **constant** default values. Non-deterministic +/// defaults like `random()` or `nextval()` are not supported. Explicitly provide +/// values for those columns as a workaround. +/// +/// ## Example +/// +/// ```rust,no_run +/// # use duckdb::{Connection, Result, params}; +/// # use duckdb::types::AppendDefault; +/// +/// fn append_with_default(conn: &Connection) -> Result<()> { +/// conn.execute_batch( +/// "CREATE TABLE people (id INTEGER, name VARCHAR, status VARCHAR DEFAULT 'active')" +/// )?; +/// +/// let mut app = conn.appender("people")?; +/// app.append_row(params![1, "Alice", AppendDefault])?; // status will be 'active' +/// Ok(()) +/// } +/// ``` +#[derive(Copy, Clone, Debug)] +pub struct AppendDefault; + /// `ToSqlOutput` represents the possible output types for implementers of the /// [`ToSql`] trait. #[derive(Clone, Debug, PartialEq)] @@ -12,6 +43,10 @@ pub enum ToSqlOutput<'a> { /// An owned SQLite-representable value. Owned(Value), + + /// A marker indicating to use the column's DEFAULT value. + /// This is only valid for Appender operations. + AppendDefault, } // Generically allow any type that can be converted into a ValueRef @@ -66,6 +101,7 @@ impl ToSql for ToSqlOutput<'_> { Ok(match *self { ToSqlOutput::Borrowed(v) => ToSqlOutput::Borrowed(v), ToSqlOutput::Owned(ref v) => ToSqlOutput::Borrowed(ValueRef::from(v)), + ToSqlOutput::AppendDefault => ToSqlOutput::AppendDefault, }) } } @@ -211,6 +247,13 @@ impl ToSql for std::time::Duration { } } +impl ToSql for AppendDefault { + #[inline] + fn to_sql(&self) -> Result> { + Ok(ToSqlOutput::AppendDefault) + } +} + #[cfg(test)] mod test { use super::ToSql;