cido-macros 0.2.0

Macros for generating code that enables easier interfacing with cido
Documentation
use crate::parse::deps::*;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};

pub const BLOCK_RANGE_COL: &str = "__block_number_range";
pub const BLOCK_NUMBER_COL: &str = "__block_number";

impl crate::codegen::CodegenWrapper<super::TransformerStruct> {
  pub(crate) fn impl_persistent(&self) -> syn::Result<TokenStream> {
    let from_row = self.from_row();
    let db_table = self.impl_db_table()?;
    Ok(quote! {
      #from_row
      #db_table
    })
  }

  pub(crate) fn impl_db_table(&self) -> syn::Result<TokenStream> {
    let db_ty_ident = self.db_name();
    let max_table_rows = self.struct_opts.db_opts.partition_max_rows;
    let max_table_bytes = self.struct_opts.db_opts.partition_max_bytes;

    let sql_table_ident = self.sql_table_name();
    let sql_id_name = &*self.id_field().sql_config.name;

    let cidomap = &self.struct_opts.cidomap;

    let mut fields = vec![];

    let kind = if self.is_entity() {
      let check_not_empty = format!("NOT isempty({BLOCK_RANGE_COL})");
      fields.push(quote!(::cido::__internal::ColDescription {
        name: #BLOCK_RANGE_COL,
        type_fn: <<<#cidomap as ::cido::__internal::Cidomap>::Network as ::cido::__internal::Network>::BlockNumberRange as ::cido::__internal::sqlx::Type<::cido::__internal::sqlx::Postgres>>::type_info,
        constraints: &[
          ::cido::__internal::ColConstraint::NotNull,
          ::cido::__internal::ColConstraint::Check { check: #check_not_empty },
        ],
        is_static: false,
        indexed: false,
      }));
      quote!(::cido::__internal::RecordType::Entity)
    } else {
      fields.push(quote!(::cido::__internal::ColDescription {
        name: #BLOCK_NUMBER_COL,
        type_fn: <<<#cidomap as ::cido::__internal::Cidomap>::Network as ::cido::__internal::Network>::BlockNumber as ::cido::__internal::sqlx::Type<::cido::__internal::sqlx::Postgres>>::type_info,
        constraints: &[::cido::__internal::ColConstraint::NotNull],
        is_static: false,
        indexed: false,
      }));
      quote!(::cido::__internal::RecordType::Event)
    };

    let id_field_idx = self.id_offset + 1;

    for f in self.fields.iter() {
      let name = &*f.sql_config.name;
      let ty = f.db_type();
      let mut constraints = quote!();
      if !f.sql_config.nullable {
        quote!(::cido::__internal::ColConstraint::NotNull,).to_tokens(&mut constraints)
      }
      let type_fn = quote!(<#ty as ::cido::__internal::sqlx::Type<::cido::__internal::sqlx::Postgres>>::type_info);
      let is_static = f.is_static;
      let indexed = f.indexed;
      fields.push(quote!(
        ::cido::__internal::ColDescription {
          name: #name,
          type_fn: #type_fn,
          constraints: &[#constraints],
          is_static: #is_static,
          indexed: #indexed,
        }
      ));
    }

    let table_constraints = {
      let list = match self.struct_type {
        super::StructType::Entity => {
          let method = format!(
            "using gist({sql_id_name} with =, {} with &&)",
            BLOCK_RANGE_COL
          );
          let gist_name = format!("exclude_{sql_id_name}_{BLOCK_RANGE_COL}");
          let check_block_number_range_open = format!("upper({BLOCK_RANGE_COL}) is null");
          let check_block_number_range_open_name = format!("{BLOCK_RANGE_COL}_is_open");
          let check_block_number_range_closed = format!("upper({BLOCK_RANGE_COL}) is not null");
          let check_block_number_range_closed_name = format!("{BLOCK_RANGE_COL}_is_closed");
          let check_lower_is_not_null = format!("lower({BLOCK_RANGE_COL}) is not null");
          let check_lower_is_not_null_name = format!("lower_{BLOCK_RANGE_COL}_is_not_null");

          quote! {
            ::cido::__internal::TableConstraint::new(
              ::cido::__internal::TableConstraintType::Exclude{method: #method},
              ::core::option::Option::Some(#gist_name),
            )
            .apply_to_non_current(),
            ::cido::__internal::TableConstraint::new(
              ::cido::__internal::TableConstraintType::PrimaryKey{columns: &[#sql_id_name]},
              ::core::option::Option::Some("pk"),
            )
            .apply_to_current(),
            ::cido::__internal::TableConstraint::new(
              ::cido::__internal::TableConstraintType::Check{check: #check_block_number_range_open},
              ::core::option::Option::Some(#check_block_number_range_open_name),
            )
            .apply_to_current(),
            ::cido::__internal::TableConstraint::new(
              ::cido::__internal::TableConstraintType::Check{check: #check_block_number_range_closed},
              ::core::option::Option::Some(#check_block_number_range_closed_name),
            )
            .apply_to_non_current(),
            ::cido::__internal::TableConstraint::new(
              ::cido::__internal::TableConstraintType::Check{check: #check_lower_is_not_null},
              ::core::option::Option::Some(#check_lower_is_not_null_name),
            )
          }
        }
        super::StructType::Event => quote! {
          ::cido::__internal::TableConstraint::new(
            ::cido::__internal::TableConstraintType::PrimaryKey{columns: &[#sql_id_name]}, ::core::option::Option::None,
          ),
        },
      };
      quote!(&[#list])
    };

    let mut tuple_bindings = vec![];
    for (i, f) in self.fields.iter().enumerate() {
      let f_name = &f.db_config.name;
      let clone = (i < self.fields.len() - 1).then(|| quote!(.clone()));
      let conversion = f.db_config.field_to_db.conversion(f_name);
      let transform = if f.self_type() == f.db_type() || f.entity_or_event.is_some() {
        quote! {|s|&s.#f_name}
      } else {
        quote! {
          |s| {
            let #f_name = &s.#f_name;
            #conversion
          }
        }
      };
      tuple_bindings.push(quote! {
        query.push_bind(::cido::__internal::PgBindIter::from(iter #clone.map(#transform)));
      });
    }

    Ok(quote! {
      impl #db_ty_ident {
        fn __static_table_resolver() -> &'static ::std::sync::OnceLock::<::cido::__internal::TableResolver<#db_ty_ident>> {
          static __TABLE_RESOLVER: ::std::sync::OnceLock::<::cido::__internal::TableResolver<#db_ty_ident>> = ::std::sync::OnceLock::new();
          &__TABLE_RESOLVER
        }

        fn __table_descriptor_inner() -> &'static ::cido::__internal::TableDescriptor::<#db_ty_ident> {
          static FIELDS: &[::cido::__internal::ColDescription] = &[#(#fields),*];
          static TABLE_DESCRIPTOR: ::cido::__internal::TableDescriptor::<#db_ty_ident> = ::cido::__internal::TableDescriptor::<#db_ty_ident> {
            base_table_name: #sql_table_ident,
            fields: FIELDS,
            id_field: &FIELDS[#id_field_idx],
            event_order_field: &FIELDS[0],
            constraints: #table_constraints,
            record_kind: #kind,
            max_table_rows: #max_table_rows,
            max_table_bytes: #max_table_bytes,
            table_resolver: #db_ty_ident::__static_table_resolver,
          };
          &TABLE_DESCRIPTOR
        }

        fn __bind_array_inner<'q>(
          iter: impl Iterator<Item = &'q Self> + Clone + Send + 'q,
          query: &mut ::cido::__internal::sqlx::query_builder::Separated<'_, 'q, ::cido::__internal::sqlx::Postgres, &'static str>,
        ) {
          #(#tuple_bindings)*
        }
      }
    })
  }

  #[allow(clippy::wrong_self_convention)]
  fn from_row(&self) -> TokenStream {
    let db_ty_ident = self.struct_name();
    let resolve_fields = self.fields.iter().map(|f| {
      let field_name = &f.name;
      let sql_name = &*f.sql_config.name;
      let db_type = f.db_type();
      let db_conversion = f.db_config.db_to_field.conversion(field_name);
      // TODO: maybe special case id field here so that it is required.
      quote! {
        if let Ok(#field_name) = row.try_get::<#db_type, _>(#sql_name) {
          __this__.#field_name = #db_conversion;
        }
      }
    });
    quote! {
      impl<'r, R> ::cido::__internal::sqlx::FromRow<'r, R> for #db_ty_ident
      where R: ::cido::__internal::sqlx::Row<Database = ::cido::__internal::sqlx::Postgres>,
        for<'a> &'a str: ::cido::__internal::sqlx::ColumnIndex<R>,
      {
        #[inline]
        fn from_row(row: &'r R) -> ::core::result::Result<Self, ::cido::__internal::sqlx::Error> {
          use ::cido::__internal::sqlx::Row;
          let mut __this__ = <Self as ::core::default::Default>::default();
          let mut f = || -> ::core::result::Result::<(), ::cido::prelude::CidomapError> {
            #(#resolve_fields)*
            ::core::result::Result::Ok(())
          };
          match f() {
            ::core::result::Result::Ok(()) => ::core::result::Result::Ok(__this__),
            // ::core::result::Result::Err(::cido::__internal::CidomapError::DatabaseError(e)) => ::core::result::Result::Err(e),
            ::core::result::Result::Err(e) => ::core::result::Result::Err(::cido::__internal::sqlx::Error::Decode(::std::boxed::Box::new(e) as _)),
          }
        }
      }
    }
  }
}