use crate::{attrs, field_value, fields, Mode};
use heck::ToSnakeCase;
use proc_macro2::{Span, TokenStream as Tokens};
use quote::{format_ident, quote, quote_spanned};
use std::collections::HashSet;
use syn::{spanned::Spanned, Data, DeriveInput, Fields, LitStr};
#[derive(Default)]
struct Options {
name: Option<String>,
primary_key: bool,
auto_increment: bool,
unique: bool,
index: bool,
skip: bool,
references: Option<(String, String)>,
on_delete: Option<String>,
}
fn identifier(value: &str, span: Span) -> syn::Result<()> {
let mut chars = value.chars();
if !chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|| !chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Err(syn::Error::new(span,"SQL identifiers must contain ASCII letters, digits or underscores and start with a letter or underscore"));
}
Ok(())
}
fn options(attributes: &[syn::Attribute], container: bool) -> syn::Result<Options> {
let mut result = Options::default();
let mut seen = HashSet::new();
for attr in attributes.iter().filter(|a| a.path().is_ident("sql")) {
attr.parse_nested_meta(|meta| {
let name = meta
.path
.get_ident()
.ok_or_else(|| meta.error("expected a SQL attribute name"))?
.to_string();
if !seen.insert(name.clone()) {
return Err(meta.error("duplicate SQL attribute"));
}
if container {
if name != "table" {
return Err(meta.error("SQL model attributes support table = \"...\""));
}
let value = meta.value()?.parse::<LitStr>()?;
identifier(&value.value(), value.span())?;
result.name = Some(value.value());
return Ok(());
}
match name.as_str() {
"rename" => {
let value = meta.value()?.parse::<LitStr>()?;
identifier(&value.value(), value.span())?;
result.name = Some(value.value());
}
"primary_key" => result.primary_key = true,
"auto_increment" => result.auto_increment = true,
"unique" => result.unique = true,
"index" => result.index = true,
"skip" => result.skip = true,
"references" => {
let value = meta.value()?.parse::<LitStr>()?;
let text = value.value();
let Some((table, column)) = text.split_once('.') else {
return Err(meta.error("references requires table.column"));
};
identifier(table, value.span())?;
identifier(column, value.span())?;
result.references = Some((table.into(), column.into()));
}
"on_delete" => {
let value = meta.value()?.parse::<LitStr>()?;
if !matches!(
value.value().as_str(),
"no_action" | "restrict" | "cascade" | "set_null"
) {
return Err(meta
.error("on_delete supports no_action, restrict, cascade or set_null"));
}
result.on_delete = Some(value.value());
}
_ => return Err(meta.error("unsupported SQL field attribute")),
}
Ok(())
})?;
}
if result.auto_increment && !result.primary_key {
return Err(syn::Error::new(
Span::call_site(),
"auto_increment requires primary_key",
));
}
if result.on_delete.is_some() && result.references.is_none() {
return Err(syn::Error::new(
Span::call_site(),
"on_delete requires references",
));
}
if result.skip
&& (result.primary_key
|| result.auto_increment
|| result.unique
|| result.index
|| result.references.is_some()
|| result.name.is_some())
{
return Err(syn::Error::new(
Span::call_site(),
"skipped SQL fields cannot declare database column attributes",
));
}
Ok(result)
}
pub(super) fn expand(input: DeriveInput) -> syn::Result<Tokens> {
if !input.generics.params.is_empty() {
return Err(syn::Error::new(
input.generics.span(),
"SqlModel requires a concrete owned struct",
));
}
let Data::Struct(data) = &input.data else {
return Err(syn::Error::new(
input.span(),
"SqlModel requires a struct with named fields",
));
};
if !matches!(data.fields, Fields::Named(_)) {
return Err(syn::Error::new(
data.fields.span(),
"SqlModel requires named fields",
));
}
let model_options = options(&input.attrs, true)?;
let name = &input.ident;
let table = model_options
.name
.unwrap_or_else(|| name.to_string().trim_start_matches("r#").to_snake_case());
identifier(&table, name.span())?;
let rd = match proc_macro_crate::crate_name("rydantic")
.map_err(|e| syn::Error::new(input.span(), e))?
{
proc_macro_crate::FoundCrate::Itself => quote!(::rydantic),
proc_macro_crate::FoundCrate::Name(name) => {
let id = format_ident!("{name}");
quote!(::#id)
}
};
let container = attrs::container(&input.attrs)?;
let fields = fields(&data.fields, None, &container)?;
let mut columns = Vec::new();
let mut reads = Vec::new();
let mut bindings = Vec::new();
let mut included = Vec::new();
let mut checks = Vec::new();
let mut names = HashSet::new();
let mut primary = None;
for (field, original) in fields.iter().zip(data.fields.iter()) {
let options = options(&original.attrs, false)?;
let ident = &field.ident;
let ty = &field.ty;
if options.skip {
let location = &field.input;
let value = field_value(
field,
quote!(::core::option::Option::<#rd::Input<'_>>::None),
&Mode {
rd: &rd,
borrow: false,
},
);
reads.push(quote!(#ident:{
let mut ctx=#rd::Context::new();
let value=ctx.at(#location,|__rydantic_ctx|{#value});
ctx.finish(value).map_err(|error|#rd::sqlx::Error::Decode(::std::boxed::Box::new(error)))?
}));
continue;
}
let column = options
.name
.unwrap_or_else(|| ident.to_string().trim_start_matches("r#").to_owned());
identifier(&column, ident.span())?;
if !names.insert(column.to_ascii_lowercase()) {
return Err(syn::Error::new(ident.span(), "duplicate SQL column name"));
}
let index = columns.len();
let pk = options.primary_key;
let auto = options.auto_increment;
let unique = options.unique;
let indexed = options.index;
if options.on_delete.as_deref() == Some("set_null") {
checks.push(quote_spanned!(ty.span()=>
const _: () = assert!(<#ty as #rd::sql::SqlValue>::NULLABLE && !#pk,
"on_delete = set_null requires a nullable non-primary-key column");
));
}
if pk {
if primary.is_some() {
return Err(syn::Error::new(
ident.span(),
"SqlModel supports one primary key",
));
}
primary = Some((ident, ty));
}
let references = if let Some((table, column)) = options.references {
let delete = format_ident!(
"{}",
match options.on_delete.as_deref().unwrap_or("no_action") {
"cascade" => "Cascade",
"restrict" => "Restrict",
"set_null" => "SetNull",
_ => "NoAction",
}
);
quote!(::core::option::Option::Some(#rd::sql::Reference{table:#table,column:#column,on_delete:#rd::sql::OnDelete::#delete}))
} else {
quote!(::core::option::Option::None)
};
columns.push(quote!(#rd::sql::Column{name:#column,sql_type:<#ty as #rd::sql::SqlValue>::SQL_TYPE,nullable:<#ty as #rd::sql::SqlValue>::NULLABLE && !#pk,primary_key:#pk,auto_increment:#auto,unique:#unique,index:#indexed,references:#references}));
reads.push(quote!(#ident:<#ty as #rd::sql::SqlValue>::read(row,#column).map_err(|error|#rd::sqlx::Error::ColumnDecode{index:#column.to_owned(),source: ::std::boxed::Box::new(error)})?));
let location = &field.input;
bindings.push(quote!(#index=><#ty as #rd::sql::SqlValue>::bind(&self.#ident,query).map_err(|error|error.at(#location))));
let include = if auto {
quote!(!<#ty as #rd::sql::AutoKey>::needs_generation(&self.#ident))
} else {
quote!(true)
};
included.push(quote!(#index=>#include));
}
let Some((key, key_ty)) = primary else {
return Err(syn::Error::new(
name.span(),
"SqlModel requires one #[sql(primary_key)] field",
));
};
Ok(quote! {
#(#checks)*
impl #rd::SqlModel for #name {
type Key=<#key_ty as #rd::sql::Key>::Value;
const TABLE:&'static str=#table;
const COLUMNS:&'static [#rd::sql::Column]=&[#(#columns),*];
fn key(&self)->::core::option::Option<&Self::Key>{<#key_ty as #rd::sql::Key>::value(&self.#key)}
fn decode_row(row:&#rd::sqlx::sqlite::SqliteRow)->::core::result::Result<Self,#rd::sqlx::Error>{Ok(Self{#(#reads),*})}
fn bind_column(&self,index:usize,query:&mut #rd::sqlx::QueryBuilder<#rd::sqlx::Sqlite>)->::core::result::Result<(),#rd::SqlError>{match index{#(#bindings,)* _=>unreachable!("generated SQL column index")}}
fn include_insert_column(&self,index:usize)->bool{match index{#(#included,)* _=>false}}
}
impl<'__rydantic_row> #rd::sqlx::FromRow<'__rydantic_row,#rd::sqlx::sqlite::SqliteRow> for #name {
fn from_row(row:&'__rydantic_row #rd::sqlx::sqlite::SqliteRow)->::core::result::Result<Self,#rd::sqlx::Error>{
let value=<Self as #rd::SqlModel>::decode_row(row)?;
<Self as #rd::Model>::validate(&value).map_err(|error|#rd::sqlx::Error::Decode(::std::boxed::Box::new(error)))?;
Ok(value)
}
}
})
}