Initial commit: duck derive macros
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target
|
||||
*.ignore
|
1965
Cargo.lock
generated
Normal file
1965
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
79
Cargo.toml
Normal file
79
Cargo.toml
Normal 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
15
README.md
Normal 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)
|
16
crates/libduck-derive/Cargo.toml
Normal file
16
crates/libduck-derive/Cargo.toml
Normal 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 }
|
64
crates/libduck-derive/src/duck_value.rs
Normal file
64
crates/libduck-derive/src/duck_value.rs
Normal 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),*]
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
99
crates/libduck-derive/src/from_duck.rs
Normal file
99
crates/libduck-derive/src/from_duck.rs
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
240
crates/libduck-derive/src/lib.rs
Normal file
240
crates/libduck-derive/src/lib.rs
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
81
crates/libduck-derive/src/to_duck.rs
Normal file
81
crates/libduck-derive/src/to_duck.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
128
crates/libduck-derive/src/util.rs
Normal file
128
crates/libduck-derive/src/util.rs
Normal 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
30
crates/libduck/Cargo.toml
Normal 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"]
|
392
crates/libduck/src/containers.rs
Normal file
392
crates/libduck/src/containers.rs
Normal 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));
|
||||
}
|
||||
}
|
159
crates/libduck/src/from_duck.rs
Normal file
159
crates/libduck/src/from_duck.rs
Normal 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
133
crates/libduck/src/lib.rs
Normal 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()?,
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
729
crates/libduck/src/primitives.rs
Normal file
729
crates/libduck/src/primitives.rs
Normal 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))
|
||||
}
|
||||
}
|
87
crates/libduck/src/special.rs
Normal file
87
crates/libduck/src/special.rs
Normal 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));
|
||||
}
|
||||
}
|
113
crates/libduck/src/to_duck.rs
Normal file
113
crates/libduck/src/to_duck.rs
Normal 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
1
rustfmt.toml
Normal file
@ -0,0 +1 @@
|
||||
hard_tabs = true
|
Reference in New Issue
Block a user