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                },
148            );
149        }
150
151        builder.build_tables_from_models(&app, db)?;
152
153        let schema = Schema {
154            app,
155            db: db::Schema {
156                tables: builder.tables,
157            },
158            mapping: builder.mapping,
159        };
160
161        // Verify the schema structure
162        schema.verify()?;
163
164        Ok(schema)
165    }
166}
167
168impl Default for Builder {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl BuildSchema<'_> {
175    fn build_model_constraints(&self, model: &mut app::Model) -> Result<()> {
176        let fields = match model {
177            app::Model::Root(root) => &mut root.fields[..],
178            app::Model::EmbeddedStruct(embedded) => &mut embedded.fields[..],
179            app::Model::EmbeddedEnum(_) => return Ok(()),
180        };
181        for field in fields.iter_mut() {
182            if let app::FieldTy::Primitive(primitive) = &mut field.ty {
183                let storage_ty = db::Type::from_app(
184                    &primitive.ty,
185                    primitive.storage_ty.as_ref(),
186                    &self.db.storage_types,
187                )?;
188
189                if let db::Type::VarChar(size) = storage_ty {
190                    field
191                        .constraints
192                        .push(app::Constraint::length_less_than(size));
193                }
194            }
195        }
196
197        Ok(())
198    }
199}