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;