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}