fix: bump the bundled DuckDB engine to 1.5.5

The 1.5.5 duckdb crate no longer hands back a 96-bit `rust_decimal`, so a
DECIMAL wider than that renders instead of panicking inside an `extern "C"`
frame — which, being unable to unwind, aborted the whole worker process and
left the job running as a zombie. `SELECT
'1234567890123456789012345678.9012345678'::DECIMAL(38, 10)` was enough.

Adapting to the crate's API: `Value` is now `#[non_exhaustive]` and gained
`UHugeInt` and `Geometry`, and `rust_decimal` became an optional feature that
the `decimal`/`numeric` argument path still needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-08-07 07:47:16 +00:00
co-authored by Claude Opus 5
parent 0459dda1bf
commit f7b634b2ee
4 changed files with 101 additions and 869 deletions
+1 -1
View File
@@ -1 +1 @@
181fa0c206d7f84a289b4396a7f7764bc815d284
65949a067fb43a556dc93bd648e808cc5cbfa933
File diff suppressed because it is too large Load Diff
@@ -5,7 +5,9 @@ edition = "2024"
[dependencies]
chrono = "0.4.41"
duckdb = { version = "1.10502.0", features = ["bundled"] }
# `rust_decimal` is optional upstream, and off it there is no way to turn the
# JSON number behind a `decimal`/`numeric` job argument into a DuckDB DECIMAL.
duckdb = { version = "1.10505.0", features = ["bundled", "rust_decimal"] }
regex = "1"
rust_decimal = "1.37.2"
serde = { version = "1.0", features = ["derive"] }
@@ -639,6 +639,7 @@ fn duckdb_value_to_json_value(
duckdb::types::Value::Int(i) => serde_json::Value::Number(i.into()),
duckdb::types::Value::BigInt(i) => serde_json::Value::Number(i.into()),
duckdb::types::Value::HugeInt(i) => serde_json::Value::String(i.to_string()),
duckdb::types::Value::UHugeInt(u) => serde_json::Value::String(u.to_string()),
duckdb::types::Value::UTinyInt(u) => serde_json::Value::Number(u.into()),
duckdb::types::Value::USmallInt(u) => serde_json::Value::Number(u.into()),
duckdb::types::Value::UInt(u) => serde_json::Value::Number(u.into()),
@@ -660,11 +661,14 @@ fn duckdb_value_to_json_value(
.map_err(|e| format!("Error parsing JSON text: {}", e.to_string()))?
}
duckdb::types::Value::Text(s) => serde_json::Value::String(s),
duckdb::types::Value::Blob(b) => serde_json::Value::Array(
b.into_iter()
.map(|byte| serde_json::Value::Number(byte.into()))
.collect(),
),
// GEOMETRY surfaces as WKB bytes; render it like any other byte string.
duckdb::types::Value::Blob(b) | duckdb::types::Value::Geometry(b) => {
serde_json::Value::Array(
b.into_iter()
.map(|byte| serde_json::Value::Number(byte.into()))
.collect(),
)
}
duckdb::types::Value::Date32(d) => {
match chrono::DateTime::from_timestamp(i64::from(d) * 86_400, 0) {
Some(dt) => serde_json::Value::String(dt.date_naive().to_string()),
@@ -710,6 +714,15 @@ fn duckdb_value_to_json_value(
.collect::<Result<serde_json::Map<_, _>, _>>()?,
),
duckdb::types::Value::Union(value) => serde_json::Value::String(format!("{:?}", *value)),
// `Value` is `#[non_exhaustive]`: a newer engine can hand back a variant this
// build has never seen. Name it instead of emitting a plausible-looking
// rendering that silently misrepresents the column.
other => {
return Err(format!(
"Unsupported DuckDB value for this build: {:?}",
other
))
}
};
Ok(json_value)
}
@@ -812,6 +825,39 @@ mod temporal_json_tests {
assert_eq!(json_of(4), serde_json::json!("10:30:00"));
}
// DuckDB DECIMAL runs to 38 digits. Rendering one through a 96-bit decimal
// type aborts the whole worker rather than erroring — the panic escapes an
// `extern "C"` frame, which cannot unwind — so every width has to survive
// this conversion.
#[test]
fn wide_decimals_render_without_losing_precision() {
let conn = duckdb::Connection::open_in_memory().unwrap();
let mut stmt = conn
.prepare(
"SELECT (-1.05)::DECIMAL(4, 2) AS neg,
(1.50)::DECIMAL(4, 2) AS trailing_zero,
'1234567890123456789012345678.9012345678'::DECIMAL(38, 10) AS wide,
'-9999999999999999999999999999.9999999999'::DECIMAL(38, 10) AS wide_neg",
)
.unwrap();
let mut rows = stmt.query([]).unwrap();
let row = rows.next().unwrap().unwrap();
let json_of = |i: usize| {
let v: duckdb::types::Value = row.get(i).unwrap();
duckdb_value_to_json_value(v, &None).unwrap()
};
assert_eq!(json_of(0), serde_json::json!("-1.05"));
assert_eq!(json_of(1), serde_json::json!("1.50"));
assert_eq!(
json_of(2),
serde_json::json!("1234567890123456789012345678.9012345678")
);
assert_eq!(
json_of(3),
serde_json::json!("-9999999999999999999999999999.9999999999")
);
}
// The data-test sample probe shape emitted by
// `windmill-parser::sql_materialize::build_data_test_checks`: one scan
// yielding the violating-row count plus a bounded `to_json` sample of the
@@ -1174,7 +1220,8 @@ fn json_value_to_duckdb_value(
"double" | "float8" => duckdb::types::Value::Double(v),
"decimal" | "numeric" => duckdb::types::Value::Decimal(
Decimal::from_f64(v)
.ok_or_else(|| "Could not convert f64 to Decimal".to_string())?,
.ok_or_else(|| "Could not convert f64 to Decimal".to_string())?
.into(),
),
_ => duckdb::types::Value::Double(v), // default fallback
}