Skip to content

Commit 017333f

Browse files
committed
Add support for DEFAULT values in Appender
Implements `AppendDefault` marker type that allows appending a column's `DEFAULT` value (as defined in table schema) when using the Appender API. Fixes #551
1 parent 1a1c690 commit 017333f

File tree

7 files changed

+127
-11
lines changed

7 files changed

+127
-11
lines changed

crates/duckdb/examples/appender.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use duckdb::{params, Connection, DropBehavior, Result};
1+
use duckdb::{params, types::AppendDefault, Connection, DropBehavior, Result};
22

33
fn main() -> Result<()> {
44
//let mut db = Connection::open("10m.db")?;
@@ -10,7 +10,7 @@ fn main() -> Result<()> {
1010
id INTEGER not null, -- primary key,
1111
area CHAR(6),
1212
age TINYINT not null,
13-
active TINYINT not null
13+
active TINYINT DEFAULT 1,
1414
);";
1515
db.execute_batch(create_table_sql)?;
1616

@@ -25,12 +25,7 @@ fn main() -> Result<()> {
2525
// }
2626

2727
for i in 0..row_count {
28-
app.append_row(params![
29-
i,
30-
get_random_area_code(),
31-
get_random_age(),
32-
get_random_active(),
33-
])?;
28+
app.append_row(params![i, get_random_area_code(), get_random_age(), AppendDefault])?;
3429
}
3530
}
3631

crates/duckdb/src/appender/mod.rs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ impl Appender<'_> {
8383
result_from_duckdb_appender(rc, &mut self.app)
8484
}
8585

86+
/// Append a DEFAULT value to the current row
87+
///
88+
/// This is called internally when `AppendDefault` is used in `append_row()`.
89+
/// Users should use the `AppendDefault` marker type instead of calling this directly.
90+
#[inline]
91+
fn append_default(&mut self) -> Result<()> {
92+
let rc = unsafe { ffi::duckdb_append_default(self.app) };
93+
if rc != 0 {
94+
return Err(Error::AppendError);
95+
}
96+
Ok(())
97+
}
98+
8699
#[inline]
87100
pub(crate) fn bind_parameters<P>(&mut self, params: P) -> Result<()>
88101
where
@@ -95,13 +108,14 @@ impl Appender<'_> {
95108
Ok(())
96109
}
97110

98-
fn bind_parameter<P: ?Sized + ToSql>(&self, param: &P) -> Result<()> {
111+
fn bind_parameter<P: ?Sized + ToSql>(&mut self, param: &P) -> Result<()> {
99112
let value = param.to_sql()?;
100113

101114
let ptr = self.app;
102115
let value = match value {
103116
ToSqlOutput::Borrowed(v) => v,
104117
ToSqlOutput::Owned(ref v) => ValueRef::from(v),
118+
ToSqlOutput::AppendDefault => return self.append_default(),
105119
};
106120
// NOTE: we ignore the return value here
107121
// because if anything failed, end_row will fail
@@ -189,7 +203,7 @@ impl fmt::Debug for Appender<'_> {
189203

190204
#[cfg(test)]
191205
mod test {
192-
use crate::{params, Connection, Error, Result};
206+
use crate::{params, types::AppendDefault, Connection, Error, Result};
193207

194208
#[test]
195209
fn test_append_one_row() -> Result<()> {
@@ -389,4 +403,56 @@ mod test {
389403

390404
Ok(())
391405
}
406+
407+
#[test]
408+
fn test_append_default_with_const_default() -> Result<()> {
409+
let db = Connection::open_in_memory()?;
410+
db.execute_batch(
411+
"CREATE TABLE test (
412+
id INTEGER,
413+
name VARCHAR,
414+
status VARCHAR DEFAULT 'active'
415+
)",
416+
)?;
417+
418+
{
419+
let mut app = db.appender("test")?;
420+
app.append_row(params![1, "Alice", AppendDefault])?;
421+
app.append_row(params![2, "Bob", AppendDefault])?;
422+
app.append_row(params![3, AppendDefault, AppendDefault])?;
423+
app.append_row(params![4, None::<String>, "inactive"])?;
424+
}
425+
426+
let rows: Vec<(i32, Option<String>, String)> = db
427+
.prepare("SELECT id, name, status FROM test ORDER BY id")?
428+
.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?
429+
.collect::<Result<Vec<_>>>()?;
430+
431+
assert_eq!(rows.len(), 4);
432+
assert_eq!(rows[0], (1, Some("Alice".to_string()), "active".to_string()));
433+
assert_eq!(rows[1], (2, Some("Bob".to_string()), "active".to_string()));
434+
assert_eq!(rows[2], (3, None, "active".to_string()));
435+
assert_eq!(rows[3], (4, None, "inactive".to_string()));
436+
437+
Ok(())
438+
}
439+
440+
#[test]
441+
fn test_append_default_in_prepared_statement_fails() -> Result<()> {
442+
let db = Connection::open_in_memory()?;
443+
db.execute_batch("CREATE TABLE test (id INTEGER, name VARCHAR DEFAULT 'test')")?;
444+
445+
// AppendDefault should fail in prepared statements
446+
let mut stmt = db.prepare("INSERT INTO test VALUES (?, ?)")?;
447+
let result = stmt.execute(params![1, AppendDefault]);
448+
449+
assert!(result.is_err());
450+
if let Err(Error::ToSqlConversionFailure(e)) = result {
451+
assert!(e.to_string().contains("only valid for Appender operations"));
452+
} else {
453+
panic!("Expected ToSqlConversionFailure error");
454+
}
455+
456+
Ok(())
457+
}
392458
}

crates/duckdb/src/pragma.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,17 @@ impl Sql {
5858

5959
pub fn push_value(&mut self, value: &dyn ToSql) -> Result<()> {
6060
let value = value.to_sql()?;
61+
// Note: AppendDefault is only meaningful for data insertion operations (Appender).
62+
// Pragmas are configuration commands, so DEFAULT values don't apply.
6163
let value = match value {
6264
ToSqlOutput::Borrowed(v) => v,
6365
ToSqlOutput::Owned(ref v) => ValueRef::from(v),
66+
ToSqlOutput::AppendDefault => {
67+
return Err(Error::ToSqlConversionFailure(Box::new(std::io::Error::new(
68+
std::io::ErrorKind::InvalidInput,
69+
"AppendDefault is only valid for Appender operations, not for pragmas",
70+
))));
71+
}
6472
};
6573
match value {
6674
ValueRef::BigInt(i) => {

crates/duckdb/src/statement.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,9 +531,18 @@ impl Statement<'_> {
531531
let value = param.to_sql()?;
532532

533533
let ptr = unsafe { self.stmt.ptr() };
534+
// Note: DuckDB's C API only provides duckdb_append_default() for Appender operations.
535+
// There is no duckdb_bind_default() function for prepared statements, so we must
536+
// reject AppendDefault here.
534537
let value = match value {
535538
ToSqlOutput::Borrowed(v) => v,
536539
ToSqlOutput::Owned(ref v) => ValueRef::from(v),
540+
ToSqlOutput::AppendDefault => {
541+
return Err(Error::ToSqlConversionFailure(Box::new(std::io::Error::new(
542+
std::io::ErrorKind::InvalidInput,
543+
"AppendDefault is only valid for Appender operations, not for prepared statements",
544+
))));
545+
}
537546
};
538547
// TODO: bind more
539548
let rc = match value {

crates/duckdb/src/types/chrono.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ mod test {
284284
let value = match sqled {
285285
ToSqlOutput::Borrowed(v) => v,
286286
ToSqlOutput::Owned(ref v) => ValueRef::from(v),
287+
ToSqlOutput::AppendDefault => unreachable!(),
287288
};
288289
let reversed = FromSql::column_result(value).unwrap();
289290

crates/duckdb/src/types/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub use self::{
66
from_sql::{FromSql, FromSqlError, FromSqlResult},
77
ordered_map::OrderedMap,
88
string::DuckString,
9-
to_sql::{ToSql, ToSqlOutput},
9+
to_sql::{AppendDefault, ToSql, ToSqlOutput},
1010
value::Value,
1111
value_ref::{EnumType, ListType, TimeUnit, ValueRef},
1212
};

crates/duckdb/src/types/to_sql.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@ use super::{Null, TimeUnit, Value, ValueRef};
22
use crate::Result;
33
use std::borrow::Cow;
44

5+
/// Empty struct that can be used in Appender params to indicate DEFAULT value.
6+
///
7+
/// This is useful when you want to append a row with some columns using their
8+
/// default values (as defined in the table schema). Unlike `Null` which explicitly
9+
/// sets a column to NULL, `AppendDefault` uses the column's DEFAULT expression.
10+
///
11+
/// ## Example
12+
///
13+
/// ```rust,no_run
14+
/// # use duckdb::{Connection, Result, params};
15+
/// # use duckdb::types::AppendDefault;
16+
///
17+
/// fn append_with_default(conn: &Connection) -> Result<()> {
18+
/// conn.execute_batch(
19+
/// "CREATE TABLE people (id INTEGER, name VARCHAR, status VARCHAR DEFAULT 'active')"
20+
/// )?;
21+
///
22+
/// let mut app = conn.appender("people")?;
23+
/// app.append_row(params![1, "Alice", AppendDefault])?;
24+
/// Ok(())
25+
/// }
26+
/// ```
27+
#[derive(Copy, Clone, Debug)]
28+
pub struct AppendDefault;
29+
530
/// `ToSqlOutput` represents the possible output types for implementers of the
631
/// [`ToSql`] trait.
732
#[derive(Clone, Debug, PartialEq)]
@@ -12,6 +37,10 @@ pub enum ToSqlOutput<'a> {
1237

1338
/// An owned SQLite-representable value.
1439
Owned(Value),
40+
41+
/// A marker indicating to use the column's DEFAULT value.
42+
/// This is only valid for Appender operations.
43+
AppendDefault,
1544
}
1645

1746
// Generically allow any type that can be converted into a ValueRef
@@ -66,6 +95,7 @@ impl ToSql for ToSqlOutput<'_> {
6695
Ok(match *self {
6796
ToSqlOutput::Borrowed(v) => ToSqlOutput::Borrowed(v),
6897
ToSqlOutput::Owned(ref v) => ToSqlOutput::Borrowed(ValueRef::from(v)),
98+
ToSqlOutput::AppendDefault => ToSqlOutput::AppendDefault,
6999
})
70100
}
71101
}
@@ -211,6 +241,13 @@ impl ToSql for std::time::Duration {
211241
}
212242
}
213243

244+
impl ToSql for AppendDefault {
245+
#[inline]
246+
fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
247+
Ok(ToSqlOutput::AppendDefault)
248+
}
249+
}
250+
214251
#[cfg(test)]
215252
mod test {
216253
use super::ToSql;

0 commit comments

Comments
 (0)