use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields, Ident, LitStr, Type};
#[proc_macro_derive(IgnisModel, attributes(ignis))]
pub fn derive_ignis_model(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match impl_ignis_model(&input) {
Ok(tokens) => tokens.into(),
Err(err) => {
let error = format!("Failed to derive IgnisModel: {}", err);
quote! {
compile_error!(#error);
}
.into()
}
}
}
#[proc_macro_derive(Insertable)]
pub fn derive_insertable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match impl_insertable(&input) {
Ok(tokens) => tokens.into(),
Err(err) => {
let error = format!("Failed to derive Insertable: {}", err);
quote! {
compile_error!(#error);
}
.into()
}
}
}
#[proc_macro_derive(Updatable)]
pub fn derive_updatable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match impl_updatable(&input) {
Ok(tokens) => tokens.into(),
Err(err) => {
let error = format!("Failed to derive Updatable: {}", err);
quote! {
compile_error!(#error);
}
.into()
}
}
}
#[proc_macro_derive(Deletable)]
pub fn derive_deletable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match impl_deletable(&input) {
Ok(tokens) => tokens.into(),
Err(err) => {
let error = format!("Failed to derive Deletable: {}", err);
quote! {
compile_error!(#error);
}
.into()
}
}
}
#[proc_macro_derive(PgComposite, attributes(pg_composite))]
pub fn derive_pg_composite(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match impl_pg_composite(&input) {
Ok(tokens) => tokens.into(),
Err(err) => {
let error = format!("Failed to derive PgComposite: {}", err);
quote! {
compile_error!(#error);
}
.into()
}
}
}
fn extract_ignis_attrs(attrs: &[Attribute]) -> anyhow::Result<IgnisConfig> {
let mut table_name = None;
let mut primary_keys = Vec::new();
let mut schema = None;
for attr in attrs {
if attr.path().is_ident("ignis") {
attr.parse_nested_meta(|meta| {
let ident = meta
.path
.get_ident()
.ok_or_else(|| meta.error("Expected identifier"))?;
match ident.to_string().as_str() {
"table" => {
let value = meta.value()?.parse::<LitStr>()?;
table_name = Some(value.value());
Ok(())
}
"pk" => {
let value = meta.value()?.parse::<LitStr>()?;
let keys: Vec<String> = value
.value()
.split(',')
.map(|s| s.trim().to_string())
.collect();
primary_keys = keys;
Ok(())
}
"schema" => {
let value = meta.value()?.parse::<LitStr>()?;
schema = Some(value.value());
Ok(())
}
_ => Err(meta.error("Unknown ignis attribute")),
}
})?;
}
}
let table_name =
table_name.ok_or_else(|| anyhow::anyhow!("Missing #[ignis(table = \"...\")]"))?;
if primary_keys.is_empty() {
return Err(anyhow::anyhow!("Missing #[ignis(pk = \"...\")]"));
}
Ok(IgnisConfig {
table_name,
primary_keys,
schema,
})
}
#[derive(Debug)]
struct IgnisConfig {
table_name: String,
primary_keys: Vec<String>,
schema: Option<String>,
}
fn generate_insert_fields(fields: &Fields) -> anyhow::Result<proc_macro2::TokenStream> {
let mut field_bindings = Vec::new();
for field in fields.iter() {
let field_name = field
.ident
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Unnamed fields not supported"))?;
field_bindings.push(quote! {
let value = ::ignis_db::BindValue::from(self.#field_name.clone());
builder = builder.value(stringify!(#field_name), value.to_parameter_binding())
.map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
});
}
Ok(quote! {
let mut builder = ::ignis_db::macros::parameter_binding::SafeInsertBuilder::new(Self::FULL_TABLE_NAME)
.map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
#(#field_bindings)*
builder.execute(executor).await
})
}
fn generate_update_fields(
fields: &Fields,
primary_keys: &[String],
) -> anyhow::Result<proc_macro2::TokenStream> {
let mut set_bindings = Vec::new();
let primary_key_set: std::collections::HashSet<_> = primary_keys.iter().collect();
for field in fields.iter() {
let field_name = field
.ident
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Unnamed fields not supported"))?;
let field_name_str = field_name.to_string();
if !primary_key_set.contains(&field_name_str) {
set_bindings.push(quote! {
let value = ::ignis_db::BindValue::from(self.#field_name.clone());
builder = builder.set(stringify!(#field_name), value.to_parameter_binding())
.map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
});
}
}
Ok(quote! {
let mut builder = ::ignis_db::macros::parameter_binding::SafeUpdateBuilder::new(Self::FULL_TABLE_NAME)
.map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
#(#set_bindings)*
let pk_values = self.get_composite_key_values()
.map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
for (key, value) in pk_values {
builder = builder.r#where(key, value.to_parameter_binding())
.map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
}
builder.execute(executor).await
})
}
fn generate_delete_fields() -> proc_macro2::TokenStream {
quote! {
let mut builder = ::ignis_db::macros::parameter_binding::SafeDeleteBuilder::new(Self::FULL_TABLE_NAME)
.map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
let pk_values = self.get_composite_key_values()
.map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
for (key, value) in pk_values {
builder = builder.r#where(key, value.to_parameter_binding())
.map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
}
builder.execute(executor).await
}
}
fn generate_composite_key_extraction(
primary_keys: &[String],
fields: &Fields,
) -> anyhow::Result<proc_macro2::TokenStream> {
let mut key_extractions = Vec::new();
for pk_name in primary_keys {
let pk_ident = syn::Ident::new(pk_name, proc_macro2::Span::call_site());
let pk_field = fields
.iter()
.find(|field| field.ident.as_ref().map(|i| i.to_string()) == Some(pk_name.to_string()))
.ok_or_else(|| {
anyhow::anyhow!("Primary key field '{}' not found in struct", pk_name)
})?;
let conversion = match &pk_field.ty {
Type::Path(type_path) => {
let type_name = type_path
.path
.segments
.last()
.ok_or_else(|| anyhow::anyhow!("Invalid type path"))?
.ident
.to_string();
match type_name.as_str() {
"i32" => quote! { (#pk_name, ::ignis_db::BindValue::Int4(self.#pk_ident)) },
"i64" => quote! { (#pk_name, ::ignis_db::BindValue::Int8(self.#pk_ident)) },
"String" => {
quote! { (#pk_name, ::ignis_db::BindValue::Text(self.#pk_ident.clone())) }
}
"bool" => quote! { (#pk_name, ::ignis_db::BindValue::Bool(self.#pk_ident)) },
"Uuid" => quote! { (#pk_name, ::ignis_db::BindValue::Uuid(self.#pk_ident)) },
"DateTime" => {
quote! { (#pk_name, ::ignis_db::BindValue::Timestamptz(self.#pk_ident)) }
}
"Decimal" => {
quote! { (#pk_name, ::ignis_db::BindValue::Numeric(self.#pk_ident)) }
}
"Value" => {
quote! { (#pk_name, ::ignis_db::BindValue::Jsonb(self.#pk_ident.clone())) }
}
_ => {
quote! { (#pk_name, ::ignis_db::BindValue::from(self.#pk_ident.clone())) }
}
}
}
_ => {
return Err(anyhow::anyhow!(
"Unsupported primary key type for field {}",
pk_name
))
}
};
key_extractions.push(conversion);
}
Ok(quote! {
Ok(vec![#(#key_extractions),*])
})
}
fn impl_ignis_model(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
let name = &input.ident;
let config = extract_ignis_attrs(&input.attrs)?;
let fields = match &input.data {
Data::Struct(data) => &data.fields,
_ => {
return Err(anyhow::anyhow!(
"IgnisModel can only be derived for structs"
))
}
};
let table_name = &config.table_name;
let primary_keys = &config.primary_keys;
let schema = config.schema.as_ref();
let full_table_name = if let Some(schema_name) = schema {
format!("{}.{}", schema_name, table_name)
} else {
table_name.clone()
};
let pk_string = primary_keys.join(", ");
let (pk_extraction, composite_extraction) = if primary_keys.len() == 1 {
let pk_name = &primary_keys[0];
let pk_ident = syn::Ident::new(pk_name, proc_macro2::Span::call_site());
let pk_field = fields
.iter()
.find(|field| field.ident.as_ref().map(|i| i.to_string()) == Some(pk_name.to_string()))
.ok_or_else(|| {
anyhow::anyhow!("Primary key field '{}' not found in struct", pk_name)
})?;
let single_extraction = match &pk_field.ty {
Type::Path(type_path) => {
let type_name = type_path
.path
.segments
.last()
.ok_or_else(|| anyhow::anyhow!("Invalid type path"))?
.ident
.to_string();
match type_name.as_str() {
"i32" => quote! { Ok(::ignis_db::BindValue::Int4(self.#pk_ident)) },
"i64" => quote! { Ok(::ignis_db::BindValue::Int8(self.#pk_ident)) },
"String" => quote! { Ok(::ignis_db::BindValue::Text(self.#pk_ident.clone())) },
"bool" => quote! { Ok(::ignis_db::BindValue::Bool(self.#pk_ident)) },
"Uuid" => quote! { Ok(::ignis_db::BindValue::Uuid(self.#pk_ident)) },
"DateTime" => quote! { Ok(::ignis_db::BindValue::Timestamptz(self.#pk_ident)) },
"Decimal" => quote! { Ok(::ignis_db::BindValue::Numeric(self.#pk_ident)) },
"Value" => quote! { Ok(::ignis_db::BindValue::Jsonb(self.#pk_ident.clone())) },
_ => quote! { Ok(::ignis_db::BindValue::from(self.#pk_ident.clone())) },
}
}
_ => return Err(anyhow::anyhow!("Unsupported primary key type")),
};
let composite = quote! {
Ok(vec![(#pk_name, self.get_primary_key_value()?)])
};
(single_extraction, composite)
} else {
let composite_extraction = generate_composite_key_extraction(primary_keys, fields)?;
let single = quote! {
Err(anyhow::anyhow!("Cannot use get_primary_key_value() with composite key. Use get_composite_key_values() instead."))
};
(single, composite_extraction)
};
let expanded = if let Some(schema_name) = schema {
quote! {
impl ::ignis_db::IgnisModel for #name {
const TABLE: &'static str = #table_name;
const PK: &'static str = #pk_string;
const SCHEMA: Option<&'static str> = Some(#schema_name);
const FULL_TABLE_NAME: &'static str = #full_table_name;
fn get_primary_key_value(&self) -> anyhow::Result<::ignis_db::BindValue> {
#pk_extraction
}
fn get_composite_key_values(&self) -> anyhow::Result<Vec<(&'static str, ::ignis_db::BindValue)>> {
#composite_extraction
}
}
}
} else {
quote! {
impl ::ignis_db::IgnisModel for #name {
const TABLE: &'static str = #table_name;
const PK: &'static str = #pk_string;
const SCHEMA: Option<&'static str> = None;
const FULL_TABLE_NAME: &'static str = #full_table_name;
fn get_primary_key_value(&self) -> anyhow::Result<::ignis_db::BindValue> {
#pk_extraction
}
fn get_composite_key_values(&self) -> anyhow::Result<Vec<(&'static str, ::ignis_db::BindValue)>> {
#composite_extraction
}
}
}
};
Ok(expanded)
}
fn impl_insertable(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
let name = &input.ident;
let fields = match &input.data {
Data::Struct(data) => &data.fields,
_ => {
return Err(anyhow::anyhow!(
"Insertable can only be derived for structs"
))
}
};
let insert_impl = generate_insert_fields(fields)?;
let expanded = quote! {
impl ::ignis_db::Insertable for #name {
async fn insert<'e, E>(&self, executor: E) -> sqlx::Result<u64>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
#insert_impl
}
}
};
Ok(expanded)
}
fn impl_updatable(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
let name = &input.ident;
let config = extract_ignis_attrs(&input.attrs)?;
let fields = match &input.data {
Data::Struct(data) => &data.fields,
_ => return Err(anyhow::anyhow!("Updatable can only be derived for structs")),
};
let update_impl = generate_update_fields(fields, &config.primary_keys)?;
let expanded = quote! {
impl ::ignis_db::Updatable for #name {
async fn update<'e, E>(&self, executor: E) -> sqlx::Result<u64>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
#update_impl
}
}
};
Ok(expanded)
}
fn impl_deletable(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
let name = &input.ident;
let delete_impl = generate_delete_fields();
let expanded = quote! {
impl ::ignis_db::Deletable for #name {
async fn delete<'e, E>(&self, executor: E) -> sqlx::Result<u64>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
#delete_impl
}
}
};
Ok(expanded)
}
fn impl_pg_composite(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
let struct_name = &input.ident;
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
let composite_config = parse_pg_composite_attributes(&input.attrs)?;
let type_name = composite_config
.type_name
.unwrap_or_else(|| struct_name.to_string().to_lowercase());
let fields_info = analyze_composite_fields(&input.data)?;
let sqlx_type_impl = generate_sqlx_type_impl(
struct_name,
&type_name,
&impl_generics,
&type_generics,
where_clause,
);
let sqlx_encode_impl = generate_sqlx_encode_impl(
struct_name,
&fields_info,
&impl_generics,
&type_generics,
where_clause,
);
let sqlx_decode_impl = generate_sqlx_decode_impl(
struct_name,
&fields_info,
&impl_generics,
&type_generics,
where_clause,
);
let ddl_functions = generate_ddl_functions(
struct_name,
&type_name,
&fields_info,
&impl_generics,
&type_generics,
where_clause,
);
Ok(quote! {
#sqlx_type_impl
#sqlx_encode_impl
#sqlx_decode_impl
#ddl_functions
})
}
#[derive(Default)]
struct PgCompositeConfig {
type_name: Option<String>,
schema: Option<String>,
}
fn parse_pg_composite_attributes(attrs: &[Attribute]) -> anyhow::Result<PgCompositeConfig> {
let mut config = PgCompositeConfig::default();
for attr in attrs {
if attr.path().is_ident("pg_composite") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
config.type_name = Some(lit.value());
Ok(())
} else if meta.path.is_ident("schema") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
config.schema = Some(lit.value());
Ok(())
} else {
Err(meta.error("unsupported pg_composite attribute"))
}
})?;
}
}
Ok(config)
}
enum CompositeFieldsInfo {
Named(Vec<CompositeNamedField>),
Unnamed(Vec<CompositeUnnamedField>),
Unit,
}
struct CompositeNamedField {
name: Ident,
#[allow(dead_code)]
ty: Type, sql_type: String,
}
struct CompositeUnnamedField {
#[allow(dead_code)]
ty: Type, sql_type: String,
index: usize,
}
fn analyze_composite_fields(data: &Data) -> anyhow::Result<CompositeFieldsInfo> {
match data {
Data::Struct(data_struct) => {
match &data_struct.fields {
Fields::Named(fields) => {
let mut named_fields = Vec::new();
for field in &fields.named {
let field_name = field.ident.as_ref().unwrap().clone();
let field_type = field.ty.clone();
let sql_type = rust_type_to_pg_type(&field_type);
validate_type_compatibility(&field_type, &sql_type)?;
named_fields.push(CompositeNamedField {
name: field_name,
ty: field_type,
sql_type,
});
}
Ok(CompositeFieldsInfo::Named(named_fields))
}
Fields::Unnamed(fields) => {
let mut unnamed_fields = Vec::new();
for (index, field) in fields.unnamed.iter().enumerate() {
let field_type = field.ty.clone();
let sql_type = rust_type_to_pg_type(&field_type);
validate_type_compatibility(&field_type, &sql_type)?;
unnamed_fields.push(CompositeUnnamedField {
ty: field_type,
sql_type,
index,
});
}
Ok(CompositeFieldsInfo::Unnamed(unnamed_fields))
}
Fields::Unit => Ok(CompositeFieldsInfo::Unit),
}
}
_ => Err(anyhow::anyhow!(
"PgComposite can only be derived for structs"
)),
}
}
fn validate_type_compatibility(rust_type: &Type, sql_type: &str) -> anyhow::Result<()> {
match rust_type {
Type::Path(type_path) => {
let path = &type_path.path;
if let Some(segment) = path.segments.last() {
let type_name = segment.ident.to_string();
let expected_sql_types: &[&str] = match type_name.as_str() {
"i16" => &["SMALLINT"],
"i32" => &["INTEGER", "INT", "INT4"],
"i64" => &["BIGINT", "INT8"],
"f32" => &["REAL", "FLOAT4"],
"f64" => &["DOUBLE PRECISION", "FLOAT8"],
"bool" => &["BOOLEAN", "BOOL"],
"String" => &["TEXT", "VARCHAR"],
"Uuid" => &["UUID"],
"Decimal" => &["NUMERIC", "DECIMAL"],
"Option" => {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
{
return validate_type_compatibility(inner_type, sql_type);
}
}
return Ok(()); }
_ => {
return Ok(());
}
};
if !expected_sql_types
.iter()
.any(|&expected| expected.eq_ignore_ascii_case(sql_type))
{
return Err(anyhow::anyhow!(
"Type compatibility warning: Rust type '{}' mapped to SQL type '{}', expected one of: {}",
type_name,
sql_type,
expected_sql_types.join(", ")
));
}
}
}
_ => {
return Ok(());
}
}
Ok(())
}
#[allow(dead_code)]
fn is_option_type(ty: &Type) -> bool {
match ty {
Type::Path(type_path) => {
let path = &type_path.path;
if let Some(segment) = path.segments.last() {
segment.ident == "Option"
} else {
false
}
}
_ => false,
}
}
fn rust_type_to_pg_type(ty: &Type) -> String {
match ty {
Type::Path(type_path) => {
let path = &type_path.path;
if let Some(segment) = path.segments.last() {
match segment.ident.to_string().as_str() {
"i16" => "SMALLINT".to_string(),
"i32" => "INTEGER".to_string(),
"i64" => "BIGINT".to_string(),
"f32" => "REAL".to_string(),
"f64" => "DOUBLE PRECISION".to_string(),
"bool" => "BOOLEAN".to_string(),
"String" => "TEXT".to_string(),
"Uuid" => "UUID".to_string(),
"Decimal" => "NUMERIC".to_string(),
"Option" => {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
{
return rust_type_to_pg_type(inner_type);
}
}
"TEXT".to_string()
}
_ => "TEXT".to_string(), }
} else {
"TEXT".to_string()
}
}
_ => "TEXT".to_string(),
}
}
fn generate_sqlx_type_impl(
struct_name: &Ident,
type_name: &str,
impl_generics: &syn::ImplGenerics,
type_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
) -> proc_macro2::TokenStream {
quote! {
impl #impl_generics sqlx::Type<sqlx::Postgres> for #struct_name #type_generics #where_clause {
fn type_info() -> sqlx::postgres::PgTypeInfo {
sqlx::postgres::PgTypeInfo::with_name(#type_name)
}
}
}
}
fn generate_sqlx_encode_impl(
struct_name: &Ident,
fields_info: &CompositeFieldsInfo,
impl_generics: &syn::ImplGenerics,
type_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
) -> proc_macro2::TokenStream {
let encode_body = match fields_info {
CompositeFieldsInfo::Named(fields) => {
let field_encodings: Vec<_> = fields
.iter()
.map(|field| {
let field_name = &field.name;
quote! { encoder.encode(&self.#field_name); }
})
.collect();
quote! {
let mut encoder = sqlx::postgres::types::PgRecordEncoder::new(buf);
#(#field_encodings)*
encoder.finish();
}
}
CompositeFieldsInfo::Unnamed(fields) => {
let field_encodings: Vec<_> = fields
.iter()
.map(|field| {
let index = syn::Index::from(field.index);
quote! { encoder.encode(&self.#index); }
})
.collect();
quote! {
let mut encoder = sqlx::postgres::types::PgRecordEncoder::new(buf);
#(#field_encodings)*
encoder.finish();
}
}
CompositeFieldsInfo::Unit => {
quote! {
}
}
};
quote! {
impl #impl_generics sqlx::Encode<'_, sqlx::Postgres> for #struct_name #type_generics #where_clause {
fn encode_by_ref(&self, buf: &mut sqlx::postgres::PgArgumentBuffer) -> sqlx::encode::IsNull {
#encode_body
sqlx::encode::IsNull::No
}
}
}
}
fn generate_sqlx_decode_impl(
struct_name: &Ident,
fields_info: &CompositeFieldsInfo,
impl_generics: &syn::ImplGenerics,
type_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
) -> proc_macro2::TokenStream {
let decode_body = match fields_info {
CompositeFieldsInfo::Named(fields) => {
let field_decodings: Vec<_> = fields
.iter()
.map(|field| {
let field_name = &field.name;
quote! { #field_name: decoder.try_decode()?, }
})
.collect();
quote! {
let mut decoder = sqlx::postgres::types::PgRecordDecoder::new(value)?;
Ok(Self {
#(#field_decodings)*
})
}
}
CompositeFieldsInfo::Unnamed(fields) => {
let field_decodings: Vec<_> = fields
.iter()
.map(|_| {
quote! { decoder.try_decode()?, }
})
.collect();
quote! {
let mut decoder = sqlx::postgres::types::PgRecordDecoder::new(value)?;
Ok(Self(#(#field_decodings)*))
}
}
CompositeFieldsInfo::Unit => {
quote! {
Ok(Self)
}
}
};
quote! {
impl #impl_generics sqlx::Decode<'_, sqlx::Postgres> for #struct_name #type_generics #where_clause {
fn decode(value: sqlx::postgres::PgValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
#decode_body
}
}
}
}
fn generate_ddl_functions(
struct_name: &Ident,
type_name: &str,
fields_info: &CompositeFieldsInfo,
impl_generics: &syn::ImplGenerics,
type_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
) -> proc_macro2::TokenStream {
let field_definitions = match fields_info {
CompositeFieldsInfo::Named(fields) => {
let definitions: Vec<_> = fields
.iter()
.map(|field| {
let field_name = field.name.to_string();
let sql_type = &field.sql_type;
format!(" \"{}\" {}", field_name, sql_type)
})
.collect();
definitions.join(",\n")
}
CompositeFieldsInfo::Unnamed(fields) => {
let definitions: Vec<_> = fields
.iter()
.enumerate()
.map(|(index, field)| {
let field_name = format!("field_{}", index);
let sql_type = &field.sql_type;
format!(" \"{}\" {}", field_name, sql_type)
})
.collect();
definitions.join(",\n")
}
CompositeFieldsInfo::Unit => String::new(),
};
quote! {
impl #impl_generics #struct_name #type_generics #where_clause {
pub fn generate_create_ddl() -> String {
format!(
"CREATE TYPE \"{}\" AS (\n{}\n);",
#type_name,
#field_definitions
)
}
pub fn generate_drop_ddl() -> String {
format!("DROP TYPE IF EXISTS \"{}\" CASCADE;", #type_name)
}
pub fn pg_type_name() -> &'static str {
#type_name
}
}
}
}