mod table;
use super::{Result, app, db, mapping};
use crate::schema::mapping::TableToModel;
use crate::schema::{Mapping, Schema, Table, TableId};
use crate::{driver, stmt};
use indexmap::IndexMap;
use std::collections::HashSet;
#[derive(Debug)]
pub struct Builder {
table_name_prefix: Option<String>,
}
struct BuildSchema<'a> {
builder: &'a Builder,
db: &'a driver::Capability,
table_lookup: IndexMap<String, TableId>,
tables: Vec<Table>,
mapping: Mapping,
}
impl Builder {
pub fn new() -> Self {
Self {
table_name_prefix: None,
}
}
pub fn table_name_prefix(&mut self, prefix: &str) -> &mut Self {
self.table_name_prefix = Some(prefix.to_string());
self
}
pub fn build(&self, mut app: app::Schema, db: &driver::Capability) -> Result<Schema> {
let mut builder = BuildSchema {
builder: self,
db,
table_lookup: IndexMap::new(),
tables: vec![],
mapping: Mapping {
models: IndexMap::new(),
document_columns: IndexMap::new(),
},
};
verify_document_types(&app)?;
for model in app.models.values_mut() {
model.verify(db)?;
builder.build_model_constraints(model)?;
}
for model in app.models() {
let app::Model::Root(model) = model else {
continue;
};
let table = builder.build_table_stub_for_model(model);
builder.mapping.models.insert(
model.id,
mapping::Model {
id: model.id,
table,
columns: vec![],
fields: vec![], model_to_table: stmt::ExprRecord::default(),
table_to_model: TableToModel::default(),
default_returning: stmt::Expr::null(),
},
);
}
builder.build_tables_from_models(&app, db)?;
builder.index_document_columns(&app);
let schema = Schema {
app,
db: db::Schema {
tables: builder.tables,
},
mapping: builder.mapping,
};
schema.verify()?;
Ok(schema)
}
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}
impl BuildSchema<'_> {
fn index_document_columns(&mut self, app: &app::Schema) {
fn collect_field(
app: &app::Schema,
field: &app::Field,
mapped: &mapping::Field,
out: &mut IndexMap<db::ColumnId, stmt::Type>,
) {
match (&field.ty, mapped) {
(app::FieldTy::Primitive(primitive), mapping::Field::Primitive(p))
if document_embed_id(&primitive.ty).is_some() =>
{
out.insert(p.column, primitive.ty.clone());
}
(app::FieldTy::Embedded(_), mapping::Field::Struct(s)) => {
for (field, mapped) in app.fields(s.id).iter().zip(&s.fields) {
collect_field(app, field, mapped, out);
}
}
(app::FieldTy::Embedded(embedded), mapping::Field::Enum(e)) => {
let app::Model::EmbeddedEnum(embedded_enum) = app.model(embedded.target) else {
panic!("enum field mapping on a non-enum embed")
};
for (index, variant) in e.variants.iter().enumerate() {
for (field, mapped) in
embedded_enum.variant_fields(index).zip(&variant.fields)
{
collect_field(app, field, mapped, out);
}
}
}
_ => {}
}
}
let mut out = IndexMap::new();
for model_mapping in self.mapping.models.values() {
for (field, mapped) in app
.fields(model_mapping.id)
.iter()
.zip(&model_mapping.fields)
{
collect_field(app, field, mapped, &mut out);
}
}
self.mapping.document_columns = out;
}
fn build_model_constraints(&self, model: &mut app::Model) -> Result<()> {
let model_name = model.name().to_string();
let mut bool_key_fields: HashSet<app::FieldId> = HashSet::new();
if let app::Model::Root(root) = &*model
&& !self.db.bool_key_type
{
let index_key_fields: HashSet<app::FieldId> = root
.indices
.iter()
.flat_map(|idx| idx.fields.iter().map(|f| f.field))
.collect();
for f in &root.fields {
if (f.primary_key || index_key_fields.contains(&f.id))
&& matches!(&f.ty, app::FieldTy::Primitive(p) if matches!(p.ty, stmt::Type::Bool))
{
bool_key_fields.insert(f.id);
}
}
}
let fields = match model {
app::Model::Root(root) => &mut root.fields[..],
app::Model::EmbeddedStruct(embedded) => &mut embedded.fields[..],
app::Model::EmbeddedEnum(_) => return Ok(()),
};
for field in fields.iter_mut() {
if let app::FieldTy::Primitive(primitive) = &mut field.ty {
if bool_key_fields.contains(&field.id) {
primitive.storage_ty = Some(db::Type::Integer(1));
}
let field_name = || {
field.name.app.as_deref().unwrap_or_else(|| {
panic!(
"model `{model_name}` field has no app-level name; \
expected every primitive field to carry one"
)
})
};
let is_document = document_embed_id(&primitive.ty).is_some();
if is_document {
if !self.db.document_collections {
return Err(crate::Error::unsupported_feature(format!(
"model `{model_name}` field `{}` uses `#[document]` storage, \
but this backend does not yet support `#[document]` fields.",
field_name()
)));
}
} else if matches!(&primitive.ty, stmt::Type::List(_)) && !self.db.vec_scalar {
return Err(crate::Error::unsupported_feature(format!(
"model `{model_name}` field `{}` is a `Vec<T>` collection, \
but this backend does not yet support `Vec<scalar>` model fields.",
field_name()
)));
}
let storage_ty = db::Type::from_app(
&primitive.ty,
primitive.storage_ty.as_ref(),
&self.db.storage_types,
)?;
if let db::Type::VarChar(size) = storage_ty {
field
.constraints
.push(app::Constraint::length_less_than(size));
}
}
}
Ok(())
}
}
fn document_unsupported_leaf(ty: &stmt::Type) -> Option<&'static str> {
match ty {
#[cfg(feature = "jiff")]
stmt::Type::Zoned => Some("Zoned"),
stmt::Type::Bytes => Some("Vec<u8>"),
stmt::Type::List(elem) => document_unsupported_leaf(elem),
_ => None,
}
}
fn document_embed_id(ty: &stmt::Type) -> Option<app::ModelId> {
match ty {
stmt::Type::Model(id) => Some(*id),
stmt::Type::List(elem) => match &**elem {
stmt::Type::Model(id) => Some(*id),
_ => None,
},
_ => None,
}
}
fn verify_document_types(app: &app::Schema) -> Result<()> {
for model in app.models.values() {
let fields = match model {
app::Model::Root(root) => &root.fields,
app::Model::EmbeddedStruct(embedded) => &embedded.fields,
app::Model::EmbeddedEnum(_) => continue,
};
for field in fields {
if let app::FieldTy::Primitive(primitive) = &field.ty
&& let Some(embed_id) = document_embed_id(&primitive.ty)
{
verify_document_embed(app, embed_id)?;
}
}
}
Ok(())
}
fn verify_document_embed(app: &app::Schema, embed_id: app::ModelId) -> Result<()> {
let app::Model::EmbeddedStruct(embedded) = app.model(embed_id) else {
return Err(crate::Error::unsupported_feature(
"#[document] elements must be `#[derive(Embed)]` structs",
));
};
for field in &embedded.fields {
let Some(name) = field.name.app.as_deref() else {
return Err(crate::Error::unsupported_feature(format!(
"embedded struct `{}` has an unnamed field; #[document] storage \
requires named fields",
embedded.name
)));
};
if field.name.storage.is_some() {
return Err(crate::Error::unsupported_feature(format!(
"embedded struct `{}` field `{name}` has a `#[column]` rename, \
which is not supported inside a #[document] field",
embedded.name
)));
}
match &field.ty {
app::FieldTy::Primitive(primitive) => {
if let Some(nested) = document_embed_id(&primitive.ty) {
verify_document_embed(app, nested)?;
} else if let Some(bad) = document_unsupported_leaf(&primitive.ty) {
return Err(crate::Error::unsupported_feature(format!(
"embedded struct `{}` field `{name}` stores `{bad}` inside a \
`#[document]`, which JSON document storage cannot represent.",
embedded.name
)));
}
}
app::FieldTy::Embedded(embedded_field) => {
verify_document_embed(app, embedded_field.target)?;
}
app::FieldTy::BelongsTo(_) | app::FieldTy::Has(_) | app::FieldTy::Via(_) => {
return Err(crate::Error::unsupported_feature(format!(
"embedded struct `{}` field `{name}` is a relation, which is \
not supported inside a #[document] field",
embedded.name
)));
}
}
}
Ok(())
}