Initial commit: duck derive macros

This commit is contained in:
2025-07-17 22:53:27 -07:00
commit 40cdd9a4c2
18 changed files with 4333 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target
*.ignore

1965
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

79
Cargo.toml Normal file
View File

@ -0,0 +1,79 @@
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.package]
edition = "2024"
version = "0.0.1"
[workspace.lints.rust]
unused_import_braces = "deny"
unit_bindings = "deny"
single_use_lifetimes = "deny"
non_ascii_idents = "deny"
macro_use_extern_crate = "deny"
elided_lifetimes_in_paths = "deny"
absolute_paths_not_starting_with_crate = "deny"
explicit_outlives_requirements = "warn"
unused_crate_dependencies = "warn"
redundant_lifetimes = "warn"
missing_docs = "warn"
[workspace.lints.clippy]
upper_case_acronyms = "deny"
needless_return = "allow"
new_without_default = "allow"
tabs_in_doc_comments = "allow"
dbg_macro = "deny"
allow_attributes = "deny"
create_dir = "deny"
filetype_is_file = "deny"
integer_division = "allow"
lossy_float_literal = "deny"
map_err_ignore = "deny"
mutex_atomic = "deny"
needless_raw_strings = "deny"
str_to_string = "deny"
string_add = "deny"
string_to_string = "deny"
use_debug = "allow"
verbose_file_reads = "deny"
large_types_passed_by_value = "deny"
wildcard_dependencies = "deny"
negative_feature_names = "deny"
redundant_feature_names = "deny"
multiple_crate_versions = "allow"
missing_safety_doc = "warn"
identity_op = "allow"
print_stderr = "deny"
print_stdout = "deny"
comparison_chain = "allow"
unimplemented = "deny"
unwrap_used = "warn"
expect_used = "warn"
#
# MARK: dependencies
#
[workspace.dependencies]
libduck-derive = { path = "crates/libduck-derive" }
libduck = { path = "crates/libduck" }
uuid = { version = "1.16.0", features = ["serde", "v4", "v5"] }
url = { version = "2.5.4", features = ["serde"] }
chrono = { version = "0.4.40", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
strum = { version = "0.27", features = ["derive"] }
itertools = "0.14.0"
utoipa = "5.4.0"
proc-macro2 = "1.0.95"
syn = "2.0.101"
quote = "1.0.40"
paste = "1.0.15"
static_assertions = "1.1.0"
darling = "0.21.0"

15
README.md Normal file
View File

@ -0,0 +1,15 @@
`libduck` provides quality-of-life features for [duckdb-rs](https://github.com/duckdb/duckdb-rs). See doc comments in `lib.rs` for details.
## TODO:
- `CAST(? AS JSON)` hack docs, feature, and example
- features (utoipa, url, etc)
- tests
- simple struct, all types
- u8/blob
- json
- option json and blob (Option must be external)
- nested structs
- string enums
- arc json, arc blob (repeated nesting)
- docstring examples (from tests)

View File

@ -0,0 +1,16 @@
[package]
name = "libduck-derive"
version = { workspace = true }
edition = { workspace = true }
[lints]
workspace = true
[lib]
proc-macro = true
[dependencies]
syn = { workspace = true }
quote = { workspace = true }
proc-macro2 = { workspace = true }
darling = { workspace = true }

View File

@ -0,0 +1,64 @@
use quote::{ToTokens, quote};
use syn::Fields;
use crate::util::{DuckFieldMode, parse_attrs};
pub fn duck_value(name: &proc_macro2::Ident, fields: &Fields) -> proc_macro2::TokenStream {
let mut columns = Vec::new();
for f in fields {
let ident = &f.ident;
let ty = &f.ty;
let ident_str = ident.clone().to_token_stream().to_string();
let attrs = match parse_attrs(&f.attrs, ty) {
Err(x) => return x,
Ok(x) => x,
};
match attrs.mode {
DuckFieldMode::Json => {
if let Some(inty) = attrs.optional_inner {
columns.push(quote! {
(
#ident_str.to_owned(),
<
Option<::libduck::special::DuckJson<#inty>>
as ::libduck::DuckValue
>::duck_type()
)
});
} else {
columns.push(quote! {
(
#ident_str.to_owned(),
<
::libduck::special::DuckJson<#ty>
as ::libduck::DuckValue
>::duck_type()
)
});
}
}
DuckFieldMode::Normal => {
columns.push(quote! {
(
#ident_str.to_owned(),
<#ty as ::libduck::DuckValue>::duck_type()
)
});
}
}
}
return quote! {
impl ::libduck::DuckValue for #name {
fn duck_type() -> ::libduck::duckdb::types::Type {
::libduck::duckdb::types::Type::Struct(
vec![#(#columns),*]
)
}
}
};
}

View File

@ -0,0 +1,99 @@
use quote::{quote, ToTokens};
use syn::Fields;
use crate::util::{parse_attrs, DuckFieldMode};
pub fn from_duck(name: &proc_macro2::Ident, fields: &Fields) -> proc_macro2::TokenStream {
let mut lines = Vec::new();
let mut idents = Vec::new();
for f in fields {
let ident = &f.ident;
let ty = &f.ty;
let ident_str = ident.clone().to_token_stream().to_string();
let attrs = match parse_attrs(&f.attrs, ty) {
Err(x) => return x,
Ok(x) => x,
};
match attrs.mode {
DuckFieldMode::Json => {
if let Some(inty) = attrs.optional_inner {
lines.push(quote! {
path_to_field.push(#ident_str.to_owned());
let #ident = {
let value = stx.get(&#ident_str.to_owned()).cloned();
<
Option<::libduck::special::DuckJson<#inty>>
as ::libduck::FromDuck
>::from_duck_with_path(value, &path_to_field)?.map(|x| x.0)
};
path_to_field.pop();
});
} else {
lines.push(quote! {
path_to_field.push(#ident_str.to_owned());
let #ident = {
let value = stx.get(&#ident_str.to_owned()).cloned();
<
::libduck::special::DuckJson<#ty>
as ::libduck::FromDuck
>::from_duck_with_path(value, &path_to_field)?.0
};
path_to_field.pop();
});
}
}
DuckFieldMode::Normal => {
lines.push(quote! {
path_to_field.push(#ident_str.to_owned());
let #ident = {
// TODO: no clone
let value = stx.get(&#ident_str.to_owned()).cloned();
<#ty as ::libduck::FromDuck>::from_duck_with_path(value, &path_to_field)?
};
path_to_field.pop();
});
}
}
idents.push(ident);
}
return quote! {
impl ::libduck::FromDuck for #name {
fn from_duck_with_path(value: Option<::libduck::duckdb::types::Value>, path_to_field: &[String]) -> Result<Self, ::libduck::FromDuckError> {
let mut path_to_field: Vec<_> = path_to_field.into();
match value {
Some(::libduck::duckdb::types::Value::Struct(stx)) => {
#(#lines)*
return Ok(Self {
#(#idents),*
});
}
Some(x) => {
return Err(::libduck::FromDuckError::InvalidType {
path_to_field,
expected: <Self as ::libduck::DuckValue>::duck_type(),
got: ::libduck::_infer_value_type(&x),
});
}
None => {
return Err(::libduck::FromDuckError::InvalidType {
path_to_field,
expected: <Self as ::libduck::DuckValue>::duck_type(),
got: None,
});
}
}
}
}
};
}

View File

@ -0,0 +1,240 @@
//! Procedural macros for automatically deriving duck traits.
//! Do not use this crate directly, always import it using `libduck`'s re-export.
//!
//! This crate provides derive macros that implement [`FromDuck`], and [`ToDuck`], and [`DuckValue`].
//!
//! See [`FromDuck`] for detailed docs.
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
mod duck_value;
mod from_duck;
mod to_duck;
mod util;
/// This macro derives [`FromDuck`] for structs and for enums with no fields.
/// - [`FromDuck`] requires [`DuckValue`], and you'll likely want to derive that too.
/// - Be careful with binary data. [`Vec<u8>`] maps to `TINYINT[]` in duckdb, while [`DuckBlob`] maps to `BLOB`.
///
///
/// # Struct example
///
/// ```ignore
/// #[derive(Debug, Clone, DuckValue, FromDuck, ToDuck)]
/// pub struct User {
/// // These fields may not be null
/// pub uuid: Uuid,
/// pub name: String,
/// pub created_at: chrono::DateTime<Utc>,
///
/// // This field is optional, and may be null
/// pub bio: Option<String>,
///
/// // This field is stored as a json string
/// #[duck(json)]
/// pub extra: HashMap<String, serde_json::Value>
///
/// // This field is stored as a `MAP(TEXT, TEXT)`
/// pub tags: HashMap<String, String>
///
/// // This field is a binary blob.
/// //
/// // The `Arc` has no effect on the type of this field
/// // in duckdb, and is transparently (de)serialized.
/// pub avatar: Option<Arc<DuckBlob>>
/// }
/// ```
///
///
/// # Enum example
///
/// [`FromDuck`], [`ToDuck`], and [`DuckValue`] can only be derived for enums whose variants have no fields.
/// They are stored as TEXT in duckdb, and we use the enum's implementation of [`ToString`] and [`std::str::FromStr`]
/// to convert variants to/from strings.
///
/// You may use [strum](https://docs.rs/strum/latest/strum) to derive [`ToString`] and [`std::str::FromStr`]
/// for string enums like this.
///
/// ```ignore
/// #[derive(DuckValue, FromDuck, ToDuck)]
/// pub enum State {
/// A,
/// B,
/// C
/// }
/// ``
#[proc_macro_derive(FromDuck, attributes(duck))]
pub fn derive_fromduck(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = input.ident.clone();
match &input.data {
syn::Data::Struct(data) => return from_duck::from_duck(&name, &data.fields).into(),
syn::Data::Enum(data) => {
if let Some(f) = data
.variants
.iter()
.map(|x| x.fields.iter().next())
.find(|x| x.is_some())
.flatten()
{
let error = syn::Error::new_spanned(
f,
"FromDuck can only be derived for enums with no fields",
);
return error.to_compile_error().into();
}
let name_str = name.to_string();
return quote! {
impl ::libduck::FromDuck for #name {
fn from_duck_with_path(
value: Option<::libduck::duckdb::types::Value>,
path_to_field: &[String]
) -> Result<Self, ::libduck::FromDuckError> {
use std::str::FromStr;
let v = match value {
Some(::libduck::duckdb::types::Value::Text(s)) => match <#name as FromStr>::from_str(&s) {
Ok(x) => x,
Err(error) => {
return Err(::libduck::FromDuckError::ParseError {
path_to_field: path_to_field.into(),
string: s.into(),
msg: Some(format!("invalid {}", #name_str)),
parent: Some(Box::new(error))
});
}
},
Some(x) => {
return Err(::libduck::FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: <Self as ::libduck::DuckValue>::duck_type(),
got: ::libduck::_infer_value_type(&x),
});
}
None => {
return Err(::libduck::FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: <Self as ::libduck::DuckValue>::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
}
.into();
}
_ => {
let error = syn::Error::new_spanned(
&input,
"FromDuck can only be derived for structs and enums",
);
return error.to_compile_error().into();
}
};
}
/// This macro derives [`ToDuck`] for structs and for enums with no fields.
/// - [`ToDuck`] requires [`DuckValue`], and you'll likely want to derive that too.
///
/// This macro works exactly like [`FromDuck`].
/// Refer to that macro for detailed docs.
#[proc_macro_derive(ToDuck, attributes(duck))]
pub fn derive_toduck(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = input.ident.clone();
match &input.data {
syn::Data::Struct(data) => return to_duck::to_duck(&name, &data.fields).into(),
syn::Data::Enum(data) => {
if let Some(f) = data
.variants
.iter()
.map(|x| x.fields.iter().next())
.find(|x| x.is_some())
.flatten()
{
let error = syn::Error::new_spanned(
f,
"ToDuck can only be derived for enums with no fields",
);
return error.to_compile_error().into();
}
return quote! {
impl ::libduck::ToDuck for #name {
fn to_duck_with_path(
&self,
path_to_field: &[String]
) -> Result<::libduck::duckdb::types::Value, ::libduck::ToDuckError> {
Ok(::libduck::duckdb::types::Value::Text(<Self as ToString>::to_string(self)))
}
}
}
.into();
}
_ => {
let error =
syn::Error::new_spanned(&input, "ToDuck can only be derived for structs and enums");
return error.to_compile_error().into();
}
};
}
/// This macro derives [`DuckValue`] for structs and for enums with no fields.
///
/// Refer to [`FromDuck`] for detailed docs.
#[proc_macro_derive(DuckValue, attributes(duck))]
pub fn derive_duckvalue(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = input.ident.clone();
match &input.data {
syn::Data::Struct(data) => return duck_value::duck_value(&name, &data.fields).into(),
syn::Data::Enum(data) => {
if let Some(f) = data
.variants
.iter()
.map(|x| x.fields.iter().next())
.find(|x| x.is_some())
.flatten()
{
let error = syn::Error::new_spanned(
f,
"DuckValue can only be derived for enums with no fields",
);
return error.to_compile_error().into();
}
return quote! {
impl ::libduck::DuckValue for #name {
fn duck_type() -> ::libduck::duckdb::types::Type {
::libduck::duckdb::types::Type::Text
}
}
}
.into();
}
_ => {
let error = syn::Error::new_spanned(
&input,
"DuckValue can only be derived for structs and enums",
);
return error.to_compile_error().into();
}
};
}

View File

@ -0,0 +1,81 @@
use quote::{ToTokens, quote};
use syn::Fields;
use crate::util::{DuckFieldMode, parse_attrs};
pub fn to_duck(name: &proc_macro2::Ident, fields: &Fields) -> proc_macro2::TokenStream {
let mut lines = Vec::new();
for f in fields {
let ident = &f.ident;
let ty = &f.ty;
let ident_str = ident.clone().to_token_stream().to_string();
let attrs = match parse_attrs(&f.attrs, ty) {
Err(x) => return x,
Ok(x) => x,
};
match attrs.mode {
DuckFieldMode::Json => {
// Note how we use refs here to prevent cloning
if let Some(inty) = attrs.optional_inner {
lines.push(quote! {
{
path_to_field.push(#ident_str.to_owned());
let val = self.#ident.as_ref().map(::libduck::special::DuckJson);
let #ident = <
Option<::libduck::special::DuckJson<&#inty>>
as ::libduck::ToDuck
>::to_duck_with_path(&val, &path_to_field)?;
fields.push((#ident_str.to_owned(), #ident));
path_to_field.pop();
}
});
} else {
lines.push(quote! {
{
path_to_field.push(#ident_str.to_owned());
let val = ::libduck::special::DuckJson(&self.#ident);
let #ident = <
::libduck::special::DuckJson<&#ty>
as ::libduck::ToDuck
>::to_duck_with_path(&val, &path_to_field)?;
fields.push((#ident_str.to_owned(), #ident));
path_to_field.pop();
}
});
}
}
DuckFieldMode::Normal => {
lines.push(quote! {
{
path_to_field.push(#ident_str.to_owned());
let #ident = <#ty as ::libduck::ToDuck>::to_duck_with_path(&self.#ident, &path_to_field)?;
fields.push((#ident_str.to_owned(), #ident));
path_to_field.pop();
}
});
}
}
}
return quote! {
impl ::libduck::ToDuck for #name {
fn to_duck_with_path(&self, path_to_field: &[String]) -> Result<::libduck::duckdb::types::Value, ::libduck::ToDuckError> {
let mut path_to_field: Vec<_> = path_to_field.into();
let mut fields = Vec::new();
#(#lines);*
let x = ::libduck::duckdb::types::OrderedMap::from(fields);
return Ok(::libduck::duckdb::types::Value::Struct(x));
}
}
};
}

View File

@ -0,0 +1,128 @@
use darling::FromMeta;
use quote::ToTokens;
use syn::{AttrStyle, Attribute, MacroDelimiter, Meta, MetaList, Path, Type};
pub enum DuckFieldMode {
Normal,
Json,
}
pub struct DuckAttrs {
pub mode: DuckFieldMode,
/// If some, this type is optional,
/// and the inner type is inside.
///
/// We use this to strip the first option from fields
/// annotated with `#[duck(json)]`, since the option needs
/// to be placed _before_ `DuckJson`.
pub optional_inner: Option<Type>,
}
/// Given tokens for `Option<T>`, return `T`.
/// Returns `None` if this is not an `Option`.
pub fn option_inner(ty: &Type) -> Option<&Type> {
let syn::Type::Path(ty) = ty else { return None };
if ty.qself.is_some() {
return None;
}
let ty = &ty.path;
#[expect(clippy::unwrap_used)] // We check length
if ty.segments.is_empty() || ty.segments.last().unwrap().ident != "Option" {
return None;
}
if !(ty.segments.len() == 1
|| (ty.segments.len() == 3
&& ["core", "std"].contains(&ty.segments[0].ident.to_string().as_str())
&& ty.segments[1].ident == "option"))
{
return None;
}
#[expect(clippy::unwrap_used)] // We checked length
let last_segment = ty.segments.last().unwrap();
let syn::PathArguments::AngleBracketed(generics) = &last_segment.arguments else {
return None;
};
if generics.args.len() != 1 {
return None;
}
let syn::GenericArgument::Type(inner_type) = &generics.args[0] else {
return None;
};
Some(inner_type)
}
/// Parse the `#[duck]` attribute
pub fn parse_attrs(
attrs: &Vec<Attribute>,
ty: &Type,
) -> Result<DuckAttrs, proc_macro2::TokenStream> {
#[derive(Debug, Default, FromMeta, PartialEq, Eq)]
#[darling(derive_syn_parse)]
struct RawDuckAttr {
#[darling(default)]
json: bool,
}
impl RawDuckAttr {
fn merge(&mut self, rhs: Self) {
self.json |= rhs.json;
}
}
let mut rda = RawDuckAttr::default();
for a in attrs {
match a {
Attribute {
style: AttrStyle::Outer,
meta:
Meta::List(MetaList {
delimiter: MacroDelimiter::Paren(_),
tokens,
path: Path {
leading_colon: None,
segments,
},
}),
..
} => {
// Not our token
if segments.to_token_stream().to_string() != "duck" {
continue;
}
let attr: RawDuckAttr = match syn::parse(tokens.clone().into()) {
Ok(x) => x,
Err(err) => {
let error = syn::Error::new_spanned(a, format!("{err}"));
return Err(error.to_compile_error());
}
};
rda.merge(attr);
}
_ => continue,
}
}
let optional_inner = option_inner(ty).cloned();
let mode = match rda.json {
true => DuckFieldMode::Json,
false => DuckFieldMode::Normal,
};
return Ok(DuckAttrs {
mode,
optional_inner,
});
}

30
crates/libduck/Cargo.toml Normal file
View File

@ -0,0 +1,30 @@
[package]
name = "libduck"
version = { workspace = true }
edition = { workspace = true }
[lints]
workspace = true
[dependencies]
libduck-derive = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
itertools = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }
utoipa = { workspace = true }
# duckdb is only used by this crate,
# and re-exported everywhere else.
# duckdb 1.2.1 is broken, https://github.com/duckdb/duckdb-rs/issues/467
# 1.2.2 should fix those issues, but until then we must pull from git.
# duckdb = { version = "1.2.1", features = ["bundled", "parquet"] }
[dependencies.duckdb]
git = "https://github.com/duckdb/duckdb-rs.git"
rev = "6ffcc70b4f1f67e19f3789b206cc22f4b8811468"
features = ["bundled", "parquet", "chrono", "json", "serde_json", "url", "uuid"]

View File

@ -0,0 +1,392 @@
use duckdb::types::{OrderedMap, Value};
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
collections::HashMap,
fmt::{Debug, Display},
hash::Hash,
ops::Deref,
rc::Rc,
sync::Arc,
};
use utoipa::ToSchema;
use crate::{DuckValue, Type, _infer_value_type};
use super::{
from_duck::{FromDuck, FromDuckError},
to_duck::{ToDuck, ToDuckError},
};
// TODO: Duration
// MARK: Cow<T>
impl<T: DuckValue + Clone> DuckValue for Cow<'_, T> {
#[inline]
fn duck_type() -> Type {
T::duck_type()
}
}
impl<T: FromDuck + Clone> FromDuck for Cow<'_, T> {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
Ok(Cow::Owned(T::from_duck_with_path(val, path_to_field)?))
}
}
impl<T: ToDuck + DuckValue + Clone> ToDuck for Cow<'_, T> {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
self.deref().to_duck_with_path(_path_to_field)
}
}
// MARK: Arc<T>
impl<T: DuckValue> DuckValue for Arc<T> {
#[inline]
fn duck_type() -> Type {
T::duck_type()
}
}
impl<T: FromDuck> FromDuck for Arc<T> {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
Ok(Arc::new(T::from_duck_with_path(val, path_to_field)?))
}
}
impl<T: ToDuck + DuckValue> ToDuck for Arc<T> {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
self.deref().to_duck_with_path(_path_to_field)
}
}
// MARK: Rc<T>
impl<T: DuckValue> DuckValue for Rc<T> {
#[inline]
fn duck_type() -> Type {
T::duck_type()
}
}
impl<T: FromDuck> FromDuck for Rc<T> {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
Ok(Rc::new(T::from_duck_with_path(val, path_to_field)?))
}
}
impl<T: ToDuck + DuckValue> ToDuck for Rc<T> {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
self.deref().to_duck_with_path(_path_to_field)
}
}
// MARK: Box<T>
impl<T: DuckValue> DuckValue for Box<T> {
#[inline]
fn duck_type() -> Type {
T::duck_type()
}
}
impl<T: FromDuck> FromDuck for Box<T> {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
Ok(Box::new(T::from_duck_with_path(val, path_to_field)?))
}
}
impl<T: ToDuck + DuckValue> ToDuck for Box<T> {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
self.deref().to_duck_with_path(_path_to_field)
}
}
// MARK: Option<T>
impl<T: DuckValue> DuckValue for Option<T> {
#[inline]
fn duck_type() -> Type {
T::duck_type()
}
}
impl<T: FromDuck> FromDuck for Option<T> {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
match val {
None | Some(Value::Null) => return Ok(None),
x => return Ok(Some(T::from_duck_with_path(x, path_to_field)?)),
};
}
}
impl<T: ToDuck + DuckValue> ToDuck for Option<T> {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
match self {
Some(x) => return x.to_duck_with_path(_path_to_field),
None => return Ok(Value::Null),
};
}
}
// MARK: Vec<T>
impl<T: DuckValue> DuckValue for Vec<T> {
#[inline]
fn duck_type() -> Type {
Type::List(Box::new(T::duck_type()))
}
}
impl<T: FromDuck> FromDuck for Vec<T> {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let mut path: Vec<_> = path_to_field.into();
match val {
Some(Value::List(x)) => {
let mut out = Vec::new();
for v in x {
path.push(format!("{}", out.len()));
out.push(T::from_duck_with_path(Some(v), &path)?);
path.pop();
}
return Ok(out);
}
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
}
}
impl<T: ToDuck> ToDuck for Vec<T> {
#[inline]
fn to_duck_with_path(&self, path_to_field: &[String]) -> Result<Value, ToDuckError> {
let mut path: Vec<_> = path_to_field.into();
let mut out = Vec::with_capacity(self.len());
for x in self {
path.push(format!("{}", out.len()));
out.push(x.to_duck_with_path(&path)?);
path.pop();
}
return Ok(Value::List(out));
}
}
// MARK: DuckBlob
/// Binary data stored in DuckDB as a BLOB type.
///
/// `DuckBlob` wraps a `Vec<u8>` and maps to DuckDB's BLOB type.
/// A plain `Vec<u8>` maps to `TINYINT[]` instead.
#[derive(Clone, Serialize, Deserialize, ToSchema)]
#[serde(transparent)]
#[schema(value_type = Vec<u8>)]
pub struct DuckBlob(pub Vec<u8>);
impl Debug for DuckBlob {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DuckBlob")
.field("len", &self.0.len())
.finish()
}
}
impl DuckBlob {
/// Create a new [`DuckBlob`] containing the given vector
#[inline]
pub fn new(buffer: Vec<u8>) -> Self {
Self(buffer)
}
/// Take the vector out of this [`DuckBlob`]
#[inline]
pub fn into_inner(self) -> Vec<u8> {
self.0
}
}
impl From<Vec<u8>> for DuckBlob {
fn from(value: Vec<u8>) -> Self {
Self(value)
}
}
impl From<DuckBlob> for Vec<u8> {
fn from(value: DuckBlob) -> Self {
value.0
}
}
impl Deref for DuckBlob {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DuckValue for DuckBlob {
#[inline]
fn duck_type() -> Type {
Type::Blob
}
}
impl FromDuck for DuckBlob {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
match val {
Some(Value::Blob(x)) => Ok(DuckBlob(x)),
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
}
}
}
impl ToDuck for DuckBlob {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
// TODO: no clone?
return Ok(Value::Blob(self.0.clone()));
}
}
// MARK: HashMap<K, V>
impl<K: DuckValue + Eq + Hash + Display, V: DuckValue> DuckValue for HashMap<K, V> {
#[inline]
fn duck_type() -> Type {
Type::Map(Box::new(K::duck_type()), Box::new(V::duck_type()))
}
}
impl<K: FromDuck + Eq + Hash + Display, V: FromDuck> FromDuck for HashMap<K, V> {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let mut path: Vec<_> = path_to_field.into();
let mut out = HashMap::new();
match val {
Some(Value::Map(x)) => {
// TODO: do not clone
for (k, v) in x.iter() {
path.push(format!("KEY_{}", out.len()));
let k = K::from_duck_with_path(Some(k.clone()), &path)?;
path.pop();
path.push(format!("{k}"));
let v = V::from_duck_with_path(Some(v.clone()), &path)?;
path.pop();
out.insert(k, v);
}
}
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(out);
}
}
impl<K: ToDuck + Eq + Hash + Display, V: ToDuck> ToDuck for HashMap<K, V> {
#[inline]
fn to_duck_with_path(&self, path_to_field: &[String]) -> Result<Value, ToDuckError> {
let mut path: Vec<_> = path_to_field.into();
let mut out = Vec::with_capacity(self.len());
for (k, v) in self.iter() {
path.push(format!("KEY_{}", out.len()));
let kv = k.to_duck_with_path(&path)?;
path.pop();
path.push(format!("{k}"));
let vv = v.to_duck_with_path(&path)?;
path.pop();
out.push((kv, vv))
}
let out = OrderedMap::from(out);
return Ok(Value::Map(out));
}
}

View File

@ -0,0 +1,159 @@
use duckdb::types::{Type, Value};
use itertools::Itertools;
use std::{
error::Error,
fmt::{Debug, Display},
};
use super::DuckValue;
/// An error we can encounter when
/// converting a [`duckdb::types::Value`] to a struct.
pub enum FromDuckError {
/// We encountered a duckdb object with a type
/// that cannot be converted in the requested Rust object
InvalidType {
/// Where this field is (see [`FromDuck::from_duck_with_path`])
path_to_field: Vec<String>,
/// The type we expected to find
/// (other types may be valid)
expected: Type,
/// The type we got.
/// This is [`None`] if we weren't able to infer it.
got: Option<Type>,
},
/// We had to decode a string to convert a duckdb object to a Rust type,
/// but encountered an error while decoding that string.
///
/// for example, this error will be returned when
/// a TEXT field contains an invalid [`url::Url`].
ParseError {
/// Where this field is (see [`FromDuck::from_duck_with_path`])
path_to_field: Vec<String>,
/// The string we tried to deserialize
string: String,
/// A message with a quick overview of the error
msg: Option<String>,
/// The error we encountered
parent: Option<Box<dyn Error>>,
},
}
impl std::error::Error for FromDuckError {
fn cause(&self) -> Option<&dyn Error> {
match self {
Self::ParseError { parent, .. } => parent.as_ref().map(|v| &**v),
_ => None,
}
}
}
impl Debug for FromDuckError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<Self as Display>::fmt(self, f)
}
}
impl Display for FromDuckError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidType {
path_to_field,
expected,
got,
} => match got {
None => {
write!(
f,
"unexpected type at {} (expected {expected})",
path_to_field.iter().join(".")
)
}
Some(got) => {
write!(
f,
"unexpected type at {} (expected {expected}, got {got:?})",
path_to_field.iter().join(".")
)
}
},
Self::ParseError {
path_to_field,
string,
msg: Some(msg),
parent: _parent,
} => {
write!(
f,
"could not parse string at {}: {msg} (while parsing {string:?})",
path_to_field.iter().join(".")
)
}
Self::ParseError {
path_to_field,
string,
msg: None,
parent: _parent,
} => {
write!(
f,
"could not parse string at {} (while parsing {string:?})",
path_to_field.iter().join(".")
)
}
}
}
}
//
// MARK: FromDuck
//
/// Trait for types that can be decoded from DuckDB values.
/// This trait provides a type-safe way to convert [`duckdb::types::Value`] instances into Rust types.
/// It can be automatically derived using `#[derive(FromDuck)]`.
///
/// [`FromDuck`] is very similar to [`duckdb::types::FromSql`], with the following differences
/// - [`duckdb::types::FromSql`] operates on references and arrow types, while [`FromDuck`] operates on owned values
/// - [`FromDuck`] provides much better error reporting
/// - [`FromDuck`] may be derived
pub trait FromDuck: Sized + DuckValue {
/// Try to decode this type from a duck value. \
/// **In nearly all cases, you don't want to call this function.** \
/// Call [`FromDuck::from_duck`] instead.
///
/// ## `path_to_field`
/// `path_to_field` is an path to the field name we're decoding,
/// and allows us to generate helpful errors.
///
/// For example, if we're decoding the field "url" in the following schema:
/// ```notrust
/// STRUCT (
/// page STRUCT (
/// url VARCHAR
/// )
/// )
/// ```
///
/// `path_to_field` will be `["page", "url"]`
///
/// This array should be empty if you're decoding a "root" struct
/// (which will usually be the case when decoding rows returned by a query)
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError>;
/// Try to decode this type from a duck value.
fn from_duck(val: Value) -> Result<Self, FromDuckError> {
return Self::from_duck_with_path(Some(val), &[]);
}
}

133
crates/libduck/src/lib.rs Normal file
View File

@ -0,0 +1,133 @@
//! [`libduck`] provides quality-of-life improvements on top of [duckdb-rs](https://github.com/duckdb/duckdb-rs).
//!
//! This crate re-exports [`duckdb`]. This makes sure we only have one version of [`duckdb`] in the source tree.
//!
//! # Features
//!
//! - [`FromDuck`] allows conversion from a [`duckdb::types::Value`] to an arbitrary Rust struct
//! - [`ToDuck`] allows conversion from an arbitrary Rust struct to a [`duckdb::types::Value`]
//! - [`DuckValue`] is a dependency for both [`FromDuck`] and [`ToDuck`].
//!
//! All the traits above can be derived see the `#[derive]` macro doc-comments for details.
//!
//!
//! # Caveats
//!
//! ## `Vec<u8>` is not a binary blob
//! When deriving [`FromDuck`] or [`ToDuck`], `Vec<u8>`s become `TINYINT[]`. \
//! If you want a proper binary blob (which you almost certainly do), use [`DuckBlob`] instead.
//!
//! ## [`ToDuck`] panics
//! A few code paths in [duckdb-rs](https://github.com/duckdb/duckdb-rs) are not implemented, and panic when we try to call them.
//! This makes the [`ToDuck`] trait difficult to use directly. Structs and arrays are among the values that panic!
//!
//! This may be hacked around using `CAST(? AS JSON)`. See [`ToDuck`] for details.
use duckdb::types::{Type, Value};
use itertools::Itertools;
pub use duckdb;
pub use libduck_derive::{DuckValue, FromDuck, ToDuck};
mod from_duck;
pub use from_duck::*;
mod to_duck;
pub use to_duck::*;
mod containers;
pub use containers::DuckBlob;
mod primitives;
pub mod special;
/// Something that can be converted into a DuckDB value.
pub trait DuckValue {
/// Get the duckdb type this object deserializes from.
/// This is only used to produce helpful errors.
fn duck_type() -> Type;
}
/// Try to infer a [`Value`]'s duckdb [`Type`].
/// Returns [`None`] if the value's type cannot be inferred.
///
/// This is similar to [`Value::data_type()`], and is only used to generate helpful errors.
///
/// You should never need to use this function manually.
/// It would be private if it wasn't inside the code generated by `#[derive(FromDuck)]`
#[inline]
pub fn _infer_value_type(value: &Value) -> Option<Type> {
return Some(match value {
Value::Null => Type::Null,
Value::Boolean(_) => Type::Boolean,
Value::TinyInt(_) => Type::TinyInt,
Value::SmallInt(_) => Type::SmallInt,
Value::Int(_) => Type::Int,
Value::BigInt(_) => Type::BigInt,
Value::HugeInt(_) => Type::HugeInt,
Value::UTinyInt(_) => Type::UTinyInt,
Value::USmallInt(_) => Type::USmallInt,
Value::UInt(_) => Type::UInt,
Value::UBigInt(_) => Type::UBigInt,
Value::Float(_) => Type::Float,
Value::Double(_) => Type::Double,
Value::Decimal(_) => Type::Decimal,
Value::Timestamp(_, _) => Type::Timestamp,
Value::Text(_) => Type::Text,
Value::Blob(_) => Type::Blob,
Value::Date32(_) => Type::Date32,
Value::Time64(..) => Type::Time64,
Value::Interval { .. } => Type::Interval,
Value::Enum(..) => Type::Enum,
Value::Union(_) => Type::Union,
Value::List(x) => {
// Edge case: x is null (also true for map)
if !x.iter().map(_infer_value_type).all_equal() {
return None;
};
let value = x.first()?;
let vt = _infer_value_type(value)?;
Type::List(Box::new(vt))
}
Value::Map(x) => {
if !x.keys().map(_infer_value_type).all_equal() {
return None;
};
if !x.values().map(_infer_value_type).all_equal() {
return None;
};
let key = x.keys().next()?;
let kt = _infer_value_type(key)?;
let value = x.values().next()?;
let vt = _infer_value_type(value)?;
Type::Map(Box::new(kt), Box::new(vt))
}
Value::Struct(x) => {
let mut out = Vec::new();
for (k, v) in x.iter() {
out.push((k.into(), _infer_value_type(v)?));
}
Type::Struct(out)
}
Value::Array(x) => {
if !x.iter().map(_infer_value_type).all_equal() {
return None;
};
Type::Array(
Box::new(_infer_value_type(x.first()?)?),
x.len().try_into().ok()?,
)
}
});
}

View File

@ -0,0 +1,729 @@
//! This module implements [`FromDuck`], [`ToDuck`], and [`DuckValue`] for all primitive types.
use chrono::{DateTime, TimeZone, Utc};
use duckdb::types::{TimeUnit, Type, Value};
use url::Url;
use uuid::Uuid;
use super::{
from_duck::{FromDuck, FromDuckError},
to_duck::{ToDuck, ToDuckError},
};
use crate::{DuckValue, _infer_value_type};
// MARK: DateTime<Utc>
impl DuckValue for DateTime<Utc> {
#[inline]
fn duck_type() -> Type {
Type::Timestamp
}
}
impl FromDuck for DateTime<Utc> {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let timestamp = match val {
Some(Value::Timestamp(time_unit, time_value)) => {
let value = time_value;
match time_unit {
TimeUnit::Second => Utc.timestamp_opt(value, 0).unwrap(),
TimeUnit::Millisecond => Utc.timestamp_millis_opt(value).unwrap(),
TimeUnit::Microsecond => Utc.timestamp_micros(value).unwrap(),
TimeUnit::Nanosecond => Utc.timestamp_nanos(value),
}
}
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(timestamp);
}
}
impl ToDuck for DateTime<Utc> {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
let micros = self.timestamp_micros();
Ok(Value::Timestamp(TimeUnit::Microsecond, micros))
}
}
// MARK: Uuid
impl DuckValue for Uuid {
#[inline]
fn duck_type() -> Type {
Type::Text
}
}
impl FromDuck for Uuid {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let uuid = match val {
Some(Value::Text(uuid_string)) => match uuid_string.parse() {
Ok(x) => x,
Err(e) => {
return Err(FromDuckError::ParseError {
path_to_field: path_to_field.into(),
string: uuid_string,
msg: Some("invalid uuid".into()),
parent: Some(Box::new(e)),
});
}
},
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(uuid);
}
}
impl ToDuck for Uuid {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::Text(self.to_string()))
}
}
// MARK: Url
impl DuckValue for Url {
#[inline]
fn duck_type() -> Type {
Type::Text
}
}
impl FromDuck for Url {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let url = match val {
Some(Value::Text(url_string)) => match url_string.parse() {
Ok(x) => x,
Err(e) => {
return Err(FromDuckError::ParseError {
path_to_field: path_to_field.into(),
string: url_string,
msg: Some("invalid url".into()),
parent: Some(Box::new(e)),
});
}
},
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(url);
}
}
impl ToDuck for Url {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::Text(self.to_string()))
}
}
// MARK: bool
impl DuckValue for bool {
#[inline]
fn duck_type() -> Type {
Type::Boolean
}
}
impl FromDuck for bool {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
match val {
Some(Value::Boolean(x)) => return Ok(x),
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
}
}
impl ToDuck for bool {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::Boolean(*self))
}
}
// MARK: String
impl DuckValue for String {
#[inline]
fn duck_type() -> Type {
Type::Text
}
}
impl FromDuck for String {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let string = match val {
Some(Value::Text(s)) => s,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(string);
}
}
impl ToDuck for String {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::Text(self.clone()))
}
}
// MARK: f64
impl DuckValue for f64 {
#[inline]
fn duck_type() -> Type {
Type::Double
}
}
impl FromDuck for f64 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::Double(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for f64 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::Double(*self))
}
}
// MARK: f32
impl DuckValue for f32 {
#[inline]
fn duck_type() -> Type {
Type::Float
}
}
impl FromDuck for f32 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::Float(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for f32 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::Float(*self))
}
}
// MARK: i32
impl DuckValue for i32 {
#[inline]
fn duck_type() -> Type {
Type::Int
}
}
impl FromDuck for i32 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::Int(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for i32 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::Int(*self))
}
}
// MARK: i64
impl DuckValue for i64 {
#[inline]
fn duck_type() -> Type {
Type::BigInt
}
}
impl FromDuck for i64 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::BigInt(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for i64 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::BigInt(*self))
}
}
// MARK: i16
impl DuckValue for i16 {
#[inline]
fn duck_type() -> Type {
Type::SmallInt
}
}
impl FromDuck for i16 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::SmallInt(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for i16 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::SmallInt(*self))
}
}
// MARK: i8
impl DuckValue for i8 {
#[inline]
fn duck_type() -> Type {
Type::TinyInt
}
}
impl FromDuck for i8 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::TinyInt(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for i8 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::TinyInt(*self))
}
}
// MARK: u32
impl DuckValue for u32 {
#[inline]
fn duck_type() -> Type {
Type::UInt
}
}
impl FromDuck for u32 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::UInt(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for u32 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::UInt(*self))
}
}
// MARK: u64
impl DuckValue for u64 {
#[inline]
fn duck_type() -> Type {
Type::UBigInt
}
}
impl FromDuck for u64 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::UBigInt(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for u64 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::UBigInt(*self))
}
}
// MARK: u16
impl DuckValue for u16 {
#[inline]
fn duck_type() -> Type {
Type::USmallInt
}
}
impl FromDuck for u16 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::USmallInt(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for u16 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::USmallInt(*self))
}
}
// MARK: u8
impl DuckValue for u8 {
#[inline]
fn duck_type() -> Type {
Type::UTinyInt
}
}
impl FromDuck for u8 {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let v = match val {
Some(Value::UTinyInt(x)) => x,
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
return Ok(v);
}
}
impl ToDuck for u8 {
#[inline]
fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result<Value, ToDuckError> {
Ok(Value::UTinyInt(*self))
}
}

View File

@ -0,0 +1,87 @@
//! This module provides a few utilities only used inside macros.
//! You shouldn't ever never use any code in here manually.
use duckdb::types::{Type, Value};
use serde::{de::DeserializeOwned, Serialize};
use std::fmt::Debug;
use utoipa::ToSchema;
use super::{
from_duck::{FromDuck, FromDuckError},
to_duck::{ToDuck, ToDuckError},
};
use crate::{DuckValue, _infer_value_type};
// MARK: DuckJson
/// A JSON object stored as text in DuckDB with automatic deserialization.
#[derive(Debug, Clone, ToSchema)]
#[schema(value_type = T)]
pub struct DuckJson<T: Debug + Clone>(pub T);
impl<T: Debug + Clone> DuckValue for DuckJson<T> {
#[inline]
fn duck_type() -> Type {
Type::Text
}
}
impl<T: Debug + Clone + DeserializeOwned> FromDuck for DuckJson<T> {
#[inline]
fn from_duck_with_path(
val: Option<Value>,
path_to_field: &[String],
) -> Result<Self, FromDuckError> {
let string = match val {
Some(Value::Text(s)) => s.clone(),
Some(x) => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: _infer_value_type(&x),
});
}
None => {
return Err(FromDuckError::InvalidType {
path_to_field: path_to_field.into(),
expected: Self::duck_type(),
got: None,
});
}
};
let value: T = match serde_json::from_str(&string) {
Ok(x) => x,
Err(error) => {
return Err(FromDuckError::ParseError {
path_to_field: path_to_field.into(),
string,
msg: Some("could not deserialize json".into()),
parent: Some(Box::new(error)),
});
}
};
return Ok(Self(value));
}
}
impl<T: Debug + Clone + Serialize> ToDuck for DuckJson<T> {
#[inline]
fn to_duck_with_path(&self, path_to_field: &[String]) -> Result<Value, ToDuckError> {
let string = match serde_json::to_string(&self.0) {
Ok(x) => x,
Err(error) => {
return Err(ToDuckError::EncodeError {
path_to_field: path_to_field.into(),
msg: Some("could not serialize json".into()),
parent: Some(Box::new(error)),
});
}
};
return Ok(Value::Text(string));
}
}

View File

@ -0,0 +1,113 @@
use duckdb::types::Value;
use itertools::Itertools;
use std::{
error::Error,
fmt::{Debug, Display},
};
use super::DuckValue;
/// A error we can encounter when converting a Rust type to a [`duckdb::types::Value`].
pub enum ToDuckError {
/// We encountered an error when encoding a Rust type as a string.
EncodeError {
/// Where this field is (see [`FromDuck::from_duck_with_path`])
path_to_field: Vec<String>,
/// A message with a quick overview of the error
msg: Option<String>,
/// The error we encountered
parent: Option<Box<dyn Error>>,
},
}
impl std::error::Error for ToDuckError {
fn cause(&self) -> Option<&dyn Error> {
match self {
Self::EncodeError { parent, .. } => parent.as_ref().map(|v| &**v),
}
}
}
impl Debug for ToDuckError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<Self as Display>::fmt(self, f)
}
}
impl Display for ToDuckError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EncodeError {
path_to_field,
msg: Some(msg),
parent: _parent,
} => {
write!(
f,
"could not parse string at {}: {msg}",
path_to_field.iter().join(".")
)
}
Self::EncodeError {
path_to_field,
msg: None,
parent: _parent,
} => {
write!(
f,
"could not encode string at {}",
path_to_field.iter().join(".")
)
}
}
}
}
/// # WARNING
/// Many parts of the duck sdk are not implemented, and will panic when we try to use them.
/// They may be inserted by serializing them to a JSON string and decoding with `CAST(? AS JSON)` in duckdb.
///
/// This does not affect [`crate::FromDuck`], and only interferes when calling [`ToDuck::to_duck`].
///
///
/// # Docs
/// Trait for types that can be converted to DuckDB values.
/// This trait provides a type-safe way to convert Rust types to [`duckdb::types::Value`]s.
/// It can be automatically derived using `#[derive(FromDuck)]`.
///
/// [`ToDuck`] is very similar to [`duckdb::types::FromSql`], with the following differences
/// - [`duckdb::types::FromSql`] operates on references and arrow types, while [`ToDuck`] operates on owned values
/// - [`ToDuck`] provides much better error reporting
/// - [`ToDuck`] may be derived
pub trait ToDuck: Sized + DuckValue {
/// Try to encode this type as a duck value. \
/// **In nearly all cases, you don't want to call this function.** \
/// Call [`ToDuck::to_duck`] instead.
///
/// ## `path_to_field`
/// `path_to_field` is an path to the field name we're decoding,
/// and allows us to generate helpful errors.
///
/// For example, if we're decoding the field "url" in the following schema:
/// ```notrust
/// STRUCT (
/// page STRUCT (
/// url VARCHAR
/// )
/// )
/// ```
///
/// `path_to_field` will be `["page", "url"]`
///
/// This array should be empty if you're decoding a "root" struct
/// (which will usually be the case when decoding rows returned by a query)
fn to_duck_with_path(&self, path_to_field: &[String]) -> Result<Value, ToDuckError>;
/// Try to encode this type as a duck value.
fn to_duck(&self) -> Result<Value, ToDuckError> {
return self.to_duck_with_path(&[]);
}
}

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
hard_tabs = true