From 15e5b5d00d9baeb2a9de5b27fa416541441cb8b9 Mon Sep 17 00:00:00 2001 From: Mathias Lafeldt Date: Wed, 19 Nov 2025 12:16:40 +0100 Subject: [PATCH 1/6] Handle invalid DuckDB type more gracefully --- crates/duckdb/src/core/logical_type.rs | 20 +++++++++++++++++++- crates/duckdb/src/vtab/arrow.rs | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/duckdb/src/core/logical_type.rs b/crates/duckdb/src/core/logical_type.rs index 8e6b5ce1..cb9d46a3 100644 --- a/crates/duckdb/src/core/logical_type.rs +++ b/crates/duckdb/src/core/logical_type.rs @@ -10,6 +10,8 @@ use crate::ffi::*; #[repr(u32)] #[derive(Debug, PartialEq, Eq)] pub enum LogicalTypeId { + /// Invalid + Invalid = DUCKDB_TYPE_DUCKDB_TYPE_INVALID, /// Boolean Boolean = DUCKDB_TYPE_DUCKDB_TYPE_BOOLEAN, /// Tinyint @@ -74,6 +76,7 @@ impl From for LogicalTypeId { /// Convert from u32 to LogicalTypeId fn from(value: u32) -> Self { match value { + DUCKDB_TYPE_DUCKDB_TYPE_INVALID => Self::Invalid, DUCKDB_TYPE_DUCKDB_TYPE_BOOLEAN => Self::Boolean, DUCKDB_TYPE_DUCKDB_TYPE_TINYINT => Self::Tinyint, DUCKDB_TYPE_DUCKDB_TYPE_SMALLINT => Self::Smallint, @@ -119,6 +122,7 @@ impl Debug for LogicalTypeHandle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { let id = self.id(); match id { + LogicalTypeId::Invalid => write!(f, "Invalid"), LogicalTypeId::Struct => { write!(f, "struct<")?; for i in 0..self.num_children() { @@ -129,7 +133,7 @@ impl Debug for LogicalTypeHandle { } write!(f, ">") } - _ => write!(f, "{:?}", self.id()), + _ => write!(f, "{:?}", id), } } } @@ -360,4 +364,18 @@ mod test { assert_eq!(typ.child_name(1), "world"); assert_eq!(typ.child(1).id(), LogicalTypeId::Integer); } + + #[test] + fn test_invalid_type() { + use crate::ffi::{duckdb_create_logical_type, DUCKDB_TYPE_DUCKDB_TYPE_INVALID}; + + // Create an invalid logical type (what DuckDB returns in certain error cases) + let invalid_type = + unsafe { LogicalTypeHandle::new(duckdb_create_logical_type(DUCKDB_TYPE_DUCKDB_TYPE_INVALID)) }; + + assert_eq!(invalid_type.id(), LogicalTypeId::Invalid); + + let debug_str = format!("{invalid_type:?}"); + assert_eq!(debug_str, "Invalid"); + } } diff --git a/crates/duckdb/src/vtab/arrow.rs b/crates/duckdb/src/vtab/arrow.rs index dfc14f95..a7e03b80 100644 --- a/crates/duckdb/src/vtab/arrow.rs +++ b/crates/duckdb/src/vtab/arrow.rs @@ -263,6 +263,7 @@ pub fn flat_vector_to_arrow_array( ) -> Result, Box> { let type_id = vector.logical_type().id(); match type_id { + LogicalTypeId::Invalid => Err("Cannot convert invalid logical type to arrow array".into()), LogicalTypeId::Integer => { let data = vector.as_slice_with_len::(len); From 657821f3da9046bb2216618063cf1a25415eee4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Ercolanelli?= Date: Wed, 19 Nov 2025 17:43:09 +0100 Subject: [PATCH 2/6] Add missing LogicalTypeId --- crates/duckdb/src/core/logical_type.rs | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/duckdb/src/core/logical_type.rs b/crates/duckdb/src/core/logical_type.rs index 8e6b5ce1..5178755b 100644 --- a/crates/duckdb/src/core/logical_type.rs +++ b/crates/duckdb/src/core/logical_type.rs @@ -10,6 +10,8 @@ use crate::ffi::*; #[repr(u32)] #[derive(Debug, PartialEq, Eq)] pub enum LogicalTypeId { + /// Invalid + Invalid = DUCKDB_TYPE_DUCKDB_TYPE_INVALID, /// Boolean Boolean = DUCKDB_TYPE_DUCKDB_TYPE_BOOLEAN, /// Tinyint @@ -66,14 +68,35 @@ pub enum LogicalTypeId { Uuid = DUCKDB_TYPE_DUCKDB_TYPE_UUID, /// Union Union = DUCKDB_TYPE_DUCKDB_TYPE_UNION, + /// Bit + Bit = DUCKDB_TYPE_DUCKDB_TYPE_BIT, + /// Time TZ + TimeTZ = DUCKDB_TYPE_DUCKDB_TYPE_TIME_TZ, /// Timestamp TZ TimestampTZ = DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_TZ, + /// Unsigned Hugeint + UHugeint = DUCKDB_TYPE_DUCKDB_TYPE_UHUGEINT, + /// Array + Array = DUCKDB_TYPE_DUCKDB_TYPE_ARRAY, + /// Any + Any = DUCKDB_TYPE_DUCKDB_TYPE_ANY, + /// Bignum + Bignum = DUCKDB_TYPE_DUCKDB_TYPE_BIGNUM, + /// SqlNull + SqlNull = DUCKDB_TYPE_DUCKDB_TYPE_SQLNULL, + /// String Literal + StringLiteral = DUCKDB_TYPE_DUCKDB_TYPE_STRING_LITERAL, + /// Integer Literal + IntegerLiteral = DUCKDB_TYPE_DUCKDB_TYPE_INTEGER_LITERAL, + /// Time NS + TimeNs = DUCKDB_TYPE_DUCKDB_TYPE_TIME_NS, } impl From for LogicalTypeId { /// Convert from u32 to LogicalTypeId fn from(value: u32) -> Self { match value { + DUCKDB_TYPE_DUCKDB_TYPE_INVALID => Self::Invalid, DUCKDB_TYPE_DUCKDB_TYPE_BOOLEAN => Self::Boolean, DUCKDB_TYPE_DUCKDB_TYPE_TINYINT => Self::Tinyint, DUCKDB_TYPE_DUCKDB_TYPE_SMALLINT => Self::Smallint, @@ -102,7 +125,17 @@ impl From for LogicalTypeId { DUCKDB_TYPE_DUCKDB_TYPE_MAP => Self::Map, DUCKDB_TYPE_DUCKDB_TYPE_UUID => Self::Uuid, DUCKDB_TYPE_DUCKDB_TYPE_UNION => Self::Union, + DUCKDB_TYPE_DUCKDB_TYPE_BIT => Self::Bit, + DUCKDB_TYPE_DUCKDB_TYPE_TIME_TZ => Self::TimeTZ, DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_TZ => Self::TimestampTZ, + DUCKDB_TYPE_DUCKDB_TYPE_UHUGEINT => Self::UHugeint, + DUCKDB_TYPE_DUCKDB_TYPE_ARRAY => Self::Array, + DUCKDB_TYPE_DUCKDB_TYPE_ANY => Self::Any, + DUCKDB_TYPE_DUCKDB_TYPE_BIGNUM => Self::Bignum, + DUCKDB_TYPE_DUCKDB_TYPE_SQLNULL => Self::SqlNull, + DUCKDB_TYPE_DUCKDB_TYPE_STRING_LITERAL => Self::StringLiteral, + DUCKDB_TYPE_DUCKDB_TYPE_INTEGER_LITERAL => Self::IntegerLiteral, + DUCKDB_TYPE_DUCKDB_TYPE_TIME_NS => Self::TimeNs, _ => panic!(), } } @@ -258,6 +291,7 @@ impl LogicalTypeHandle { LogicalTypeId::Struct => unsafe { duckdb_struct_type_child_count(self.ptr) as usize }, LogicalTypeId::Union => unsafe { duckdb_union_type_member_count(self.ptr) as usize }, LogicalTypeId::List => 1, + LogicalTypeId::Array => 1, _ => 0, } } From 9545437a77fe259547831851c375c2e2b37e0542 Mon Sep 17 00:00:00 2001 From: Mathias Lafeldt Date: Thu, 20 Nov 2025 10:57:09 +0100 Subject: [PATCH 3/6] Return Invalid instead of panicking for unknown types --- crates/duckdb/src/core/logical_type.rs | 5 ++- crates/duckdb/src/vtab/arrow.rs | 52 ++++++++++---------------- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/crates/duckdb/src/core/logical_type.rs b/crates/duckdb/src/core/logical_type.rs index 138b4f45..a979e4b2 100644 --- a/crates/duckdb/src/core/logical_type.rs +++ b/crates/duckdb/src/core/logical_type.rs @@ -9,6 +9,7 @@ use crate::ffi::*; /// #[repr(u32)] #[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum LogicalTypeId { /// Invalid Invalid = DUCKDB_TYPE_DUCKDB_TYPE_INVALID, @@ -136,7 +137,9 @@ impl From for LogicalTypeId { DUCKDB_TYPE_DUCKDB_TYPE_STRING_LITERAL => Self::StringLiteral, DUCKDB_TYPE_DUCKDB_TYPE_INTEGER_LITERAL => Self::IntegerLiteral, DUCKDB_TYPE_DUCKDB_TYPE_TIME_NS => Self::TimeNs, - _ => panic!(), + // Return Invalid for unknown types to handle forward compatibility + // when DuckDB adds new types in future versions + _ => Self::Invalid, } } } diff --git a/crates/duckdb/src/vtab/arrow.rs b/crates/duckdb/src/vtab/arrow.rs index a7e03b80..52e93d90 100644 --- a/crates/duckdb/src/vtab/arrow.rs +++ b/crates/duckdb/src/vtab/arrow.rs @@ -25,9 +25,7 @@ use arrow::{ record_batch::RecordBatch, }; -use libduckdb_sys::{ - duckdb_date, duckdb_hugeint, duckdb_interval, duckdb_string_t, duckdb_time, duckdb_timestamp, duckdb_vector, -}; +use libduckdb_sys::{duckdb_date, duckdb_string_t, duckdb_time, duckdb_timestamp, duckdb_vector}; use num::{cast::AsPrimitive, ToPrimitive}; /// A pointer to the Arrow record batch for the table function. @@ -462,35 +460,25 @@ pub fn flat_vector_to_arrow_array( Ok(Arc::new(structs)) } - LogicalTypeId::Struct => { - todo!() - } - LogicalTypeId::Decimal => { - todo!() - } - LogicalTypeId::Map => { - todo!() - } - LogicalTypeId::List => { - todo!() - } - LogicalTypeId::Union => { - todo!() - } - LogicalTypeId::Interval => { - let _data = vector.as_slice_with_len::(len); - todo!() - } - LogicalTypeId::Hugeint => { - let _data = vector.as_slice_with_len::(len); - todo!() - } - LogicalTypeId::Enum => { - todo!() - } - LogicalTypeId::Uuid => { - todo!() - } + LogicalTypeId::Interval => todo!(), + LogicalTypeId::Hugeint => todo!(), + LogicalTypeId::Decimal => todo!(), + LogicalTypeId::Enum => todo!(), + LogicalTypeId::List => todo!(), + LogicalTypeId::Struct => todo!(), + LogicalTypeId::Map => todo!(), + LogicalTypeId::Array => todo!(), + LogicalTypeId::Uuid => todo!(), + LogicalTypeId::Union => todo!(), + LogicalTypeId::Bit => todo!(), + LogicalTypeId::TimeTZ => todo!(), + LogicalTypeId::UHugeint => todo!(), + LogicalTypeId::Any => todo!(), + LogicalTypeId::Bignum => todo!(), + LogicalTypeId::SqlNull => todo!(), + LogicalTypeId::StringLiteral => todo!(), + LogicalTypeId::IntegerLiteral => todo!(), + LogicalTypeId::TimeNs => todo!(), } } From f58bd97f65129630d82fe303206183e0b68e8e38 Mon Sep 17 00:00:00 2001 From: Mathias Lafeldt Date: Thu, 20 Nov 2025 14:56:46 +0100 Subject: [PATCH 4/6] Introduce LogicalTypeId::Unsupported --- crates/duckdb/src/core/logical_type.rs | 20 +++++++++++++++----- crates/duckdb/src/vtab/arrow.rs | 6 ++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/duckdb/src/core/logical_type.rs b/crates/duckdb/src/core/logical_type.rs index a979e4b2..0060336f 100644 --- a/crates/duckdb/src/core/logical_type.rs +++ b/crates/duckdb/src/core/logical_type.rs @@ -91,6 +91,8 @@ pub enum LogicalTypeId { IntegerLiteral = DUCKDB_TYPE_DUCKDB_TYPE_INTEGER_LITERAL, /// Time NS TimeNs = DUCKDB_TYPE_DUCKDB_TYPE_TIME_NS, + /// DuckDB returned a type that this wrapper does not yet recognize + Unsupported = u32::MAX, } impl From for LogicalTypeId { @@ -137,9 +139,8 @@ impl From for LogicalTypeId { DUCKDB_TYPE_DUCKDB_TYPE_STRING_LITERAL => Self::StringLiteral, DUCKDB_TYPE_DUCKDB_TYPE_INTEGER_LITERAL => Self::IntegerLiteral, DUCKDB_TYPE_DUCKDB_TYPE_TIME_NS => Self::TimeNs, - // Return Invalid for unknown types to handle forward compatibility - // when DuckDB adds new types in future versions - _ => Self::Invalid, + // Unknown / forward compatible types + _ => Self::Unsupported, } } } @@ -285,8 +286,12 @@ impl LogicalTypeHandle { /// Logical type ID pub fn id(&self) -> LogicalTypeId { - let duckdb_type_id = unsafe { duckdb_get_type_id(self.ptr) }; - duckdb_type_id.into() + self.raw_id().into() + } + + /// Raw logical type id returned by DuckDB C API + pub fn raw_id(&self) -> u32 { + unsafe { duckdb_get_type_id(self.ptr) } } /// Logical type children num @@ -412,4 +417,9 @@ mod test { let debug_str = format!("{invalid_type:?}"); assert_eq!(debug_str, "Invalid"); } + + #[test] + fn test_unknown_type() { + assert_eq!(LogicalTypeId::from(999_999), LogicalTypeId::Unsupported); + } } diff --git a/crates/duckdb/src/vtab/arrow.rs b/crates/duckdb/src/vtab/arrow.rs index 52e93d90..10df8a85 100644 --- a/crates/duckdb/src/vtab/arrow.rs +++ b/crates/duckdb/src/vtab/arrow.rs @@ -259,9 +259,11 @@ pub fn flat_vector_to_arrow_array( vector: &mut FlatVector, len: usize, ) -> Result, Box> { - let type_id = vector.logical_type().id(); + let raw_type_id = vector.logical_type().raw_id(); + let type_id = LogicalTypeId::from(raw_type_id); match type_id { - LogicalTypeId::Invalid => Err("Cannot convert invalid logical type to arrow array".into()), + LogicalTypeId::Invalid => Err("Cannot convert invalid logical type returned by DuckDB".into()), + LogicalTypeId::Unsupported => Err(format!("Unsupported DuckDB logical type ID {raw_type_id}").into()), LogicalTypeId::Integer => { let data = vector.as_slice_with_len::(len); From 7b6fe672b5307af755868574ab3ac44e80150887 Mon Sep 17 00:00:00 2001 From: Mathias Lafeldt Date: Thu, 20 Nov 2025 14:57:41 +0100 Subject: [PATCH 5/6] Enable array child inspection --- crates/duckdb/src/core/logical_type.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/duckdb/src/core/logical_type.rs b/crates/duckdb/src/core/logical_type.rs index 0060336f..19341d0d 100644 --- a/crates/duckdb/src/core/logical_type.rs +++ b/crates/duckdb/src/core/logical_type.rs @@ -327,7 +327,8 @@ impl LogicalTypeHandle { match self.id() { LogicalTypeId::Struct => duckdb_struct_type_child_type(self.ptr, idx as u64), LogicalTypeId::Union => duckdb_union_type_member_type(self.ptr, idx as u64), - _ => panic!("not a struct or union"), + LogicalTypeId::Array => duckdb_array_type_child_type(self.ptr), + _ => panic!("not a struct, union, or array"), } }; unsafe { Self::new(c_logical_type) } @@ -362,26 +363,36 @@ mod test { #[test] fn test_struct() { - let fields = &[("hello", LogicalTypeHandle::from(crate::core::LogicalTypeId::Boolean))]; + let fields = &[("hello", LogicalTypeHandle::from(LogicalTypeId::Boolean))]; let typ = LogicalTypeHandle::struct_type(fields); assert_eq!(typ.num_children(), 1); assert_eq!(typ.child_name(0), "hello"); - assert_eq!(typ.child(0).id(), crate::core::LogicalTypeId::Boolean); + assert_eq!(typ.child(0).id(), LogicalTypeId::Boolean); + } + + #[test] + fn test_array() { + let child = LogicalTypeHandle::from(LogicalTypeId::Integer); + let array = LogicalTypeHandle::array(&child, 4); + + assert_eq!(array.id(), LogicalTypeId::Array); + assert_eq!(array.num_children(), 1); + assert_eq!(array.child(0).id(), LogicalTypeId::Integer); } #[test] fn test_decimal() { let typ = LogicalTypeHandle::decimal(10, 2); - assert_eq!(typ.id(), crate::core::LogicalTypeId::Decimal); + assert_eq!(typ.id(), LogicalTypeId::Decimal); assert_eq!(typ.decimal_width(), 10); assert_eq!(typ.decimal_scale(), 2); } #[test] fn test_decimal_methods() { - let typ = LogicalTypeHandle::from(crate::core::LogicalTypeId::Varchar); + let typ = LogicalTypeHandle::from(LogicalTypeId::Varchar); assert_eq!(typ.decimal_width(), 0); assert_eq!(typ.decimal_scale(), 0); From 78e50c4a2905aee9942258bea1854acad7d2edcc Mon Sep 17 00:00:00 2001 From: Mathias Lafeldt Date: Thu, 20 Nov 2025 15:40:24 +0100 Subject: [PATCH 6/6] Add try_id helper --- crates/duckdb/src/core/logical_type.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/duckdb/src/core/logical_type.rs b/crates/duckdb/src/core/logical_type.rs index 19341d0d..34d74acd 100644 --- a/crates/duckdb/src/core/logical_type.rs +++ b/crates/duckdb/src/core/logical_type.rs @@ -157,6 +157,7 @@ impl Debug for LogicalTypeHandle { let id = self.id(); match id { LogicalTypeId::Invalid => write!(f, "Invalid"), + LogicalTypeId::Unsupported => write!(f, "Unsupported({})", self.raw_id()), LogicalTypeId::Struct => { write!(f, "struct<")?; for i in 0..self.num_children() { @@ -289,6 +290,19 @@ impl LogicalTypeHandle { self.raw_id().into() } + /// Logical type ID, with forward-compatibility awareness. + /// + /// Returns `Ok(LogicalTypeId)` for all known ids (including `Invalid`), and + /// `Err(raw_id)` when DuckDB returns an id this wrapper does not yet + /// recognize. + pub fn try_id(&self) -> Result { + let raw = self.raw_id(); + match LogicalTypeId::from(raw) { + LogicalTypeId::Unsupported => Err(raw), + id => Ok(id), + } + } + /// Raw logical type id returned by DuckDB C API pub fn raw_id(&self) -> u32 { unsafe { duckdb_get_type_id(self.ptr) } @@ -313,6 +327,7 @@ impl LogicalTypeHandle { let child_name_ptr = match self.id() { LogicalTypeId::Struct => duckdb_struct_type_child_name(self.ptr, idx as u64), LogicalTypeId::Union => duckdb_union_type_member_name(self.ptr, idx as u64), + LogicalTypeId::Unsupported => panic!("unsupported logical type {}", self.raw_id()), _ => panic!("not a struct or union"), }; let c_str = CString::from_raw(child_name_ptr); @@ -328,6 +343,7 @@ impl LogicalTypeHandle { LogicalTypeId::Struct => duckdb_struct_type_child_type(self.ptr, idx as u64), LogicalTypeId::Union => duckdb_union_type_member_type(self.ptr, idx as u64), LogicalTypeId::Array => duckdb_array_type_child_type(self.ptr), + LogicalTypeId::Unsupported => panic!("unsupported logical type {}", self.raw_id()), _ => panic!("not a struct, union, or array"), } }; @@ -424,6 +440,8 @@ mod test { unsafe { LogicalTypeHandle::new(duckdb_create_logical_type(DUCKDB_TYPE_DUCKDB_TYPE_INVALID)) }; assert_eq!(invalid_type.id(), LogicalTypeId::Invalid); + assert_eq!(invalid_type.try_id().unwrap(), LogicalTypeId::Invalid); + assert_eq!(invalid_type.raw_id(), DUCKDB_TYPE_DUCKDB_TYPE_INVALID); let debug_str = format!("{invalid_type:?}"); assert_eq!(debug_str, "Invalid");