Skip to main content

toasty_core/schema/
builder.rs

1mod table;
2
3use super::{Result, app, db, mapping};
4use crate::schema::mapping::TableToModel;
5use crate::schema::{Mapping, Schema, Table, TableId};
6use crate::{driver, stmt};
7use indexmap::IndexMap;
8
9/// Constructs a [`Schema`] from an app-level schema and driver capabilities.
10///
11/// The builder generates the database-level schema (tables, columns, indices)
12/// and the mapping layer that connects app fields to database columns. Call
13/// [`build`](Builder::build) to produce the final, validated [`Schema`].
14///
15/// # Examples
16///
17/// ```ignore
18/// use toasty_core::schema::Builder;
19///
20/// let schema = Builder::new()
21///     .table_name_prefix("myapp_")
22///     .build(app_schema, &capability)
23///     .expect("valid schema");
24/// ```
25#[derive(Debug)]
26pub struct Builder {
27    /// If set, prefix all table names with this string.
28    table_name_prefix: Option<String>,
29}
30
31/// Used to track state during the build process.
32struct BuildSchema<'a> {
33    /// Build options.
34    builder: &'a Builder,
35
36    db: &'a driver::Capability,
37
38    /// Maps table names to identifiers. The identifiers are reserved before the
39    /// table objects are actually created.
40    table_lookup: IndexMap<String, TableId>,
41
42    /// Tables as they are built.
43    tables: Vec<Table>,
44
45    /// App-level to db-level schema mapping.
46    mapping: Mapping,
47}
48
49impl Builder {
50    /// Creates a new `Builder` with default settings.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use toasty_core::schema::Builder;
56    ///
57    /// let builder = Builder::new();
58    /// ```
59    pub fn new() -> Self {
60        Self {
61            table_name_prefix: None,
62        }
63    }
64
65    /// Sets a prefix that will be prepended to all generated table names.
66    ///
67    /// This is useful for multi-tenant setups or avoiding name collisions.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use toasty_core::schema::Builder;
73    ///
74    /// let mut builder = Builder::new();
75    /// builder.table_name_prefix("myapp_");
76    /// ```
77    pub fn table_name_prefix(&mut self, prefix: &str) -> &mut Self {
78        self.table_name_prefix = Some(prefix.to_string());
79        self
80    }
81
82    /// Builds the complete [`Schema`] from the given app schema and driver
83    /// capabilities.
84    ///
85    /// This method:
86    /// 1. Verifies each model against the driver's capabilities
87    /// 2. Generates field-level constraints (e.g., `VARCHAR` length limits)
88    /// 3. Creates database tables, columns, and indices
89    /// 4. Builds the bidirectional mapping between models and tables
90    /// 5. Validates the resulting schema
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if the schema is invalid (e.g., duplicate index names,
95    /// unsupported types, missing references).
96    ///
97    /// # Examples
98    ///
99    /// ```ignore
100    /// use toasty_core::schema::Builder;
101    ///
102    /// let schema = Builder::new()
103    ///     .build(app_schema, &capability)?;
104    /// ```
105    pub fn build(&self, mut app: app::Schema, db: &driver::Capability) -> Result<Schema> {
106        let mut builder = BuildSchema {
107            builder: self,
108            db,
109            table_lookup: IndexMap::new(),
110            tables: vec![],
111            mapping: Mapping {
112                models: IndexMap::new(),
113            },
114        };
115
116        for model in app.models.values_mut() {
117            // Initial verification pass to ensure all models are valid based on the
118            // specified driver capability.
119            model.verify(db)?;
120
121            // Generate any additional field-level constraints to satisfy the
122            // target database.
123            builder.build_model_constraints(model)?;
124        }
125
126        // Find all models that specified a table name, ensure a table is
127        // created for that model, and link the model with the table.
128        // Skip embedded models as they don't have their own tables.
129        for model in app.models() {
130            // Skip embedded models - they are flattened into parent tables
131            let app::Model::Root(model) = model else {
132                continue;
133            };
134
135            let table = builder.build_table_stub_for_model(model);
136
137            // Create a mapping shell for the model (fields will be built during mapping phase)
138            builder.mapping.models.insert(
139                model.id,
140                mapping::Model {
141                    id: model.id,
142                    table,
143                    columns: vec![],
144                    fields: vec![], // Will be populated during mapping phase
145                    model_to_table: stmt::ExprRecord::default(),
146                    table_to_model: TableToModel::default(),
147                    default_returning: stmt::Expr::null(),
148                },
149            );
150        }
151
152        builder.build_tables_from_models(&app, db)?;
153
154        let schema = Schema {
155            app,
156            db: db::Schema {
157                tables: builder.tables,
158            },
159            mapping: builder.mapping,
160        };
161
162        // Verify the schema structure
163        schema.verify()?;
164
165        Ok(schema)
166    }
167}
168
169impl Default for Builder {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175impl BuildSchema<'_> {
176    fn build_model_constraints(&self, model: &mut app::Model) -> Result<()> {
177        let model_name = model.name().to_string();
178        let fields = match model {
179            app::Model::Root(root) => &mut root.fields[..],
180            app::Model::EmbeddedStruct(embedded) => &mut embedded.fields[..],
181            app::Model::EmbeddedEnum(_) => return Ok(()),
182        };
183        for field in fields.iter_mut() {
184            if let app::FieldTy::Primitive(primitive) = &mut field.ty {
185                if matches!(primitive.ty, stmt::Type::List(_)) && !self.db.vec_scalar {
186                    let field_name = field.name.app.as_deref().unwrap_or_else(|| {
187                        panic!(
188                            "model `{model_name}` field has no app-level name; \
189                             expected every primitive field to carry one"
190                        )
191                    });
192                    return Err(crate::Error::unsupported_feature(format!(
193                        "model `{model_name}` field `{field_name}` is a `Vec<T>` collection, \
194                         but this backend does not yet support `Vec<scalar>` model fields."
195                    )));
196                }
197
198                let storage_ty = db::Type::from_app(
199                    &primitive.ty,
200                    primitive.storage_ty.as_ref(),
201                    &self.db.storage_types,
202                )?;
203
204                if let db::Type::VarChar(size) = storage_ty {
205                    field
206                        .constraints
207                        .push(app::Constraint::length_less_than(size));
208                }
209            }
210        }
211
212        Ok(())
213    }
214}