use std::{
collections::HashMap,
sync::{Arc, LazyLock, Mutex, OnceLock, PoisonError},
};
use toasty::{
schema::{app, mapping},
stmt::{IntoExpr, IntoInsert, Path},
};
use toasty_core::{driver::Capability, stmt::PathRoot};
use topcoat::context::Cx;
use crate::{DeclarationErrorKind, Text};
pub(crate) type ModelId = app::ModelId;
#[derive(Clone)]
pub(crate) struct AppSchema(Arc<toasty_core::Schema>);
impl AppSchema {
pub(crate) fn of_db(db: &toasty::Db) -> Self {
Self(db.schema().clone())
}
pub(crate) fn of_model<M: toasty::schema::Model>() -> Result<Self, String> {
type Compiled = Arc<OnceLock<Result<AppSchema, String>>>;
static COMPILED: LazyLock<Mutex<HashMap<ModelId, Compiled>>> =
LazyLock::new(Mutex::default);
let entry = COMPILED
.lock()
.unwrap_or_else(PoisonError::into_inner)
.entry(M::id())
.or_default()
.clone();
entry.get_or_init(Self::compile::<M>).clone()
}
fn compile<M: toasty::schema::Model>() -> Result<Self, String> {
let mut models = app::ModelSet::new();
M::register(&mut models);
let builder = toasty_core::schema::Builder::new();
let mut refusals = Vec::new();
for driver in [
&Capability::POSTGRESQL,
&Capability::SQLITE,
&Capability::MYSQL,
&Capability::DYNAMODB,
] {
let app = app::Schema::from_macro(models.clone()).map_err(|error| error.to_string())?;
match builder.build(app, driver) {
Ok(schema) => return Ok(Self(Arc::new(schema))),
Err(refused) => refusals.push(format!("{}: {refused}", driver.driver_name)),
}
}
Err(refusals.join("; "))
}
pub(crate) fn of(cx: &Cx) -> Option<Self> {
topcoat::context::try_context::<toasty::Db>(cx).map(Self::of_db)
}
pub(crate) fn inner(&self) -> &toasty_core::Schema {
&self.0
}
pub(crate) fn registers(&self, name: &str) -> bool {
self.0
.app
.models()
.any(|model| model.name().upper_camel_case() == name)
}
}
#[derive(Debug, Clone)]
pub(crate) struct ModelPath(toasty_core::stmt::Path);
impl ModelPath {
pub(crate) fn of<M, T>(path: &Path<M, T>) -> Self {
Self(path.clone().into())
}
pub(crate) fn steps(&self) -> &[usize] {
self.0.projection.as_slice()
}
pub(crate) fn is_embedded(&self) -> bool {
self.steps().len() > 1 || matches!(self.0.root, PathRoot::Variant { .. })
}
pub(crate) fn root_step(&self) -> Option<usize> {
match &self.0.root {
PathRoot::Variant { parent, .. } => Self((**parent).clone()).root_step(),
_ => self.steps().first().copied(),
}
}
pub(crate) fn eq<T: IntoExpr<T>>(&self, value: T) -> toasty::stmt::Expr<bool> {
let rhs: toasty_core::stmt::Expr = value.into_expr().into();
toasty::stmt::Expr::from_untyped(toasty_core::stmt::Expr::eq(
self.0.clone().into_stmt(),
rhs,
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FieldInfo {
pub(crate) index: usize,
pub(crate) name: String,
pub(crate) label: Text,
pub(crate) nullable: bool,
pub(crate) unique: bool,
pub(crate) unique_with: Vec<Vec<String>>,
pub(crate) relation: bool,
}
pub(crate) fn fields<M: toasty::schema::Model>() -> Vec<FieldInfo> {
let model = M::schema();
let root = model.as_root_unwrap();
root.fields
.iter()
.filter_map(|field| field_info(field, root))
.collect()
}
pub(crate) fn is_via<M: toasty::schema::Model>(name: &str) -> bool {
M::schema().as_root().is_some_and(|root| {
root.fields.iter().any(|field| {
field.name.app.as_deref() == Some(name) && matches!(field.ty, app::FieldTy::Via(_))
})
})
}
pub(crate) fn is_list<M: toasty::schema::Model>(name: &str) -> bool {
M::schema().as_root().is_some_and(|root| {
root.fields.iter().any(|field| {
field.name.app.as_deref() == Some(name)
&& matches!(&field.ty, app::FieldTy::Primitive(p) if p.ty.is_list())
})
})
}
pub(crate) fn is_inverse<M: toasty::schema::Model>(name: &str) -> bool {
M::schema().as_root().is_some_and(|root| {
root.fields.iter().any(|field| {
field.name.app.as_deref() == Some(name)
&& matches!(field.ty, app::FieldTy::Has(_) | app::FieldTy::Via(_))
})
})
}
pub(crate) fn root_field<M: toasty::schema::Model>(path: &ModelPath) -> Option<String> {
let index = path.root_step()?;
M::schema().as_root()?.fields.get(index)?.name.app.clone()
}
pub(crate) fn field<M: toasty::schema::Model>(
path: &ModelPath,
) -> Result<FieldInfo, DeclarationErrorKind> {
let model = M::schema();
let root = model.as_root_unwrap();
let index = single_segment(path)?;
let field = root.fields.get(index).unwrap_or_else(|| {
panic!(
"field index {index} out of bounds for {}",
std::any::type_name::<M>()
)
});
Ok(field_info(field, root).expect("a model root's fields are named"))
}
fn field_info(field: &app::Field, root: &app::ModelRoot) -> Option<FieldInfo> {
let name = field.name.app.as_deref()?;
Some(FieldInfo {
index: field.id.index,
name: name.to_string(),
label: Text::field(belongs_to::label_name(field, root)),
nullable: field.nullable(),
unique: root.indices.iter().any(|index| {
index.unique && !index.primary_key && index.fields.iter().any(|f| f.field == field.id)
}),
unique_with: root
.indices
.iter()
.filter(|index| {
index.unique
&& !index.primary_key
&& index.fields.iter().any(|f| f.field == field.id)
})
.map(|index| {
index
.fields
.iter()
.filter(|f| f.field != field.id)
.filter_map(|f| root.fields.get(f.field.index)?.name.app.clone())
.collect()
})
.collect(),
relation: field.is_relation(),
})
}
pub(crate) fn single_segment(path: &ModelPath) -> Result<usize, DeclarationErrorKind> {
match path.steps() {
[index] => Ok(*index),
steps => Err(DeclarationErrorKind::TraversalLens { steps: steps.len() }),
}
}
pub(crate) fn prefilled_fields<M: toasty::schema::Model>() -> Vec<bool> {
let insert = <M::Create as Default>::default().into_insert();
let toasty_core::stmt::Expr::Stmt(statement) = toasty_core::stmt::Expr::from(insert) else {
return Vec::new();
};
statement
.stmt
.as_insert()
.and_then(|insert| insert.source.body.as_values())
.and_then(|values| values.rows.last())
.and_then(|row| row.as_record())
.map(|record| {
let model = M::schema();
(record.fields.iter().zip(model.fields()))
.map(|(expr, field)| !expr.is_value_null() || field.is_versionable())
.collect()
})
.unwrap_or_default()
}
#[derive(Debug, Clone)]
pub(crate) struct LeafField {
pub(crate) name: String,
pub(crate) label: crate::Text,
pub(crate) nullable: bool,
pub(crate) unique: bool,
pub(crate) unique_with: Vec<Vec<String>>,
}
impl From<FieldInfo> for LeafField {
fn from(field: FieldInfo) -> Self {
Self {
label: field.label,
name: field.name,
nullable: field.nullable,
unique: field.unique,
unique_with: field.unique_with,
}
}
}
pub(crate) fn single_field<M: toasty::schema::Model>(
path: &ModelPath,
) -> Result<LeafField, DeclarationErrorKind> {
field::<M>(path).map(LeafField::from)
}
pub(crate) fn embedded_leaf(
AppSchema(schema): &AppSchema,
path: &ModelPath,
model: &'static str,
) -> Result<LeafField, DeclarationErrorKind> {
walk_embedded(schema, &path.0).ok_or_else(|| DeclarationErrorKind::UnresolvedLens {
model,
steps: path.steps().to_vec(),
})
}
fn walk_embedded(
schema: &toasty_core::Schema,
path: &toasty_core::stmt::Path,
) -> Option<LeafField> {
match &path.root {
PathRoot::Model(id) => {
let root = schema.app.get_model(*id)?.as_root()?;
let model = schema.mapping.models.get(&root.id)?;
descend(
schema,
&root.fields,
&model.fields,
path.projection.as_slice(),
)
}
PathRoot::Variant { parent, variant_id } => {
let root = schema
.app
.get_model(parent.root.as_model_unwrap())?
.as_root()?;
let model = schema.mapping.models.get(&root.id)?;
let parent_steps = parent.projection.as_slice();
let app_field = app_field_at(schema, &root.fields, parent_steps)?;
let Some(app::Model::EmbeddedEnum(e)) = app_embedded(schema, app_field) else {
return None;
};
let MappingField::Enum(me) = mapping_field_at(&model.fields, parent_steps)? else {
return None;
};
let variant = me.variants.get(variant_id.index)?;
let payloads: Vec<_> = e.variant_fields(variant_id.index).iter().collect();
descend(
schema,
&payloads,
&variant.fields,
path.projection.as_slice(),
)
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct EnumShape {
pub(crate) discriminant: String,
pub(crate) variants: Vec<(String, Text)>,
}
pub(crate) fn embedded_enum(schema: &AppSchema, path: &ModelPath) -> Option<EnumShape> {
let AppSchema(schema) = schema;
let PathRoot::Model(id) = path.0.root else {
return None;
};
let root = schema.app.get_model(id)?.as_root()?;
let mapping = schema.mapping.models.get(&root.id)?;
let steps = path.steps();
let app_field = app_field_at(schema, &root.fields, steps)?;
let (app::Model::EmbeddedEnum(e), MappingField::Enum(me)) = (
app_embedded(schema, app_field)?,
mapping_field_at(&mapping.fields, steps)?,
) else {
return None;
};
let discriminant = column_of(schema, &MappingField::Primitive(me.discriminant.clone()))?.name;
let variants = e
.variants
.iter()
.map(|v| {
super::value_text(&v.discriminant).map(|value| {
(
value,
Text::option(&e.name.snake_case(), &v.name.snake_case()),
)
})
})
.collect::<Option<Vec<_>>>()?;
Some(EnumShape {
discriminant,
variants,
})
}
type MappingField = mapping::Field;
fn descend<F>(
schema: &toasty_core::Schema,
app_fields: &[F],
mapping_fields: &[MappingField],
steps: &[usize],
) -> Option<LeafField>
where
F: std::borrow::Borrow<app::Field>,
{
let (first, rest) = steps.split_first()?;
let app_field: &app::Field = app_fields.get(*first)?.borrow();
let mapping_field = mapping_fields.get(*first)?;
if rest.is_empty() || is_document(app_field) {
return column_of(schema, mapping_field);
}
let app::FieldTy::Embedded(embedded) = &app_field.ty else {
return None;
};
match schema.app.get_model(embedded.target)? {
app::Model::EmbeddedStruct(e) => {
let MappingField::Struct(ms) = mapping_field else {
return None;
};
descend(schema, &e.fields, &ms.fields, rest)
}
app::Model::EmbeddedEnum(e) => {
let MappingField::Enum(me) = mapping_field else {
return None;
};
let (variant_index, tail) = rest.split_first()?;
let variant = me.variants.get(*variant_index)?;
let payloads: Vec<_> = e.variant_fields(*variant_index).iter().collect();
descend(schema, &payloads, &variant.fields, tail)
}
_ => None,
}
}
fn column_of(schema: &toasty_core::Schema, field: &MappingField) -> Option<LeafField> {
let MappingField::Primitive(p) = field else {
return None;
};
let column = &schema.db.tables[p.column.table.0].columns[p.column.index];
Some(LeafField {
name: column.name.clone(),
label: crate::Text::field(&column.name),
nullable: true,
unique: false,
unique_with: Vec::new(),
})
}
fn is_document(field: &app::Field) -> bool {
matches!(
&field.ty,
app::FieldTy::Primitive(p) if matches!(p.ty, toasty::stmt::Type::Model(_))
)
}
fn app_embedded<'a>(schema: &'a toasty_core::Schema, field: &app::Field) -> Option<&'a app::Model> {
let app::FieldTy::Embedded(embedded) = &field.ty else {
return None;
};
schema.app.get_model(embedded.target)
}
mod belongs_to;
mod lookup;
mod reach;
pub(crate) use belongs_to::{BelongsTo, belongs_to, belongs_tos};
use lookup::{app_field_at, mapping_field_at};
pub(crate) use reach::{included_models, read_relations};
#[cfg(test)]
mod tests;