1use super::{Schema, TopologicalSort, entity::index_table_ref};
2use crate::{ConnectionTrait, DbBackend, DbErr, EntityTrait, Statement};
3use sea_query::{
4 ForeignKeyCreateStatement, Index, IndexCreateStatement, IntoIden, TableAlterStatement,
5 TableCreateStatement, TableName, TableRef, extension::postgres::TypeCreateStatement,
6};
7
8pub struct SchemaBuilder {
10 helper: Schema,
11 entities: Vec<EntitySchemaInfo>,
12}
13
14pub struct EntitySchemaInfo {
16 table: TableCreateStatement,
17 enums: Vec<TypeCreateStatement>,
18 indexes: Vec<IndexCreateStatement>,
19 schema_name: Option<String>,
22}
23
24impl std::fmt::Debug for SchemaBuilder {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "SchemaBuilder {{")?;
27 write!(f, " entities: [")?;
28 for (i, entity) in self.entities.iter().enumerate() {
29 if i > 0 {
30 write!(f, ", ")?;
31 }
32 entity.debug_print(f, &self.helper.backend)?;
33 }
34 write!(f, " ]")?;
35 write!(f, " }}")
36 }
37}
38
39impl std::fmt::Debug for EntitySchemaInfo {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 self.debug_print(f, &DbBackend::Sqlite)
42 }
43}
44
45impl SchemaBuilder {
46 pub fn new(schema: Schema) -> Self {
48 Self {
49 helper: schema,
50 entities: Default::default(),
51 }
52 }
53
54 pub fn register<E: EntityTrait>(mut self, entity: E) -> Self {
56 let entity = EntitySchemaInfo::new(entity, &self.helper);
57 if !self
58 .entities
59 .iter()
60 .any(|e| e.table.get_table_name() == entity.table.get_table_name())
61 {
62 self.entities.push(entity);
63 }
64 self
65 }
66
67 #[cfg(feature = "entity-registry")]
68 pub(crate) fn helper(&self) -> &Schema {
69 &self.helper
70 }
71
72 #[cfg(feature = "entity-registry")]
73 pub(crate) fn register_entity(&mut self, entity: EntitySchemaInfo) {
74 self.entities.push(entity);
75 }
76
77 #[cfg(feature = "schema-sync")]
89 #[cfg_attr(docsrs, doc(cfg(feature = "schema-sync")))]
90 pub fn sync<C>(self, db: &C) -> Result<(), DbErr>
91 where
92 C: ConnectionTrait + sea_schema::Connection,
93 {
94 let _existing = match db.get_database_backend() {
95 #[cfg(feature = "sqlx-mysql")]
96 DbBackend::MySql => {
97 use sea_schema::{mysql::discovery::SchemaDiscovery, probe::SchemaProbe};
98
99 let current_schema: String = db
100 .query_one(
101 sea_query::SelectStatement::new()
102 .expr(sea_schema::mysql::MySql::get_current_schema()),
103 )?
104 .ok_or_else(|| DbErr::RecordNotFound("Can't get current schema".into()))?
105 .try_get_by_index(0)?;
106
107 let mut target_schemas = std::collections::BTreeSet::new();
109 for entity in &self.entities {
110 let schema = entity.schema_name.as_deref().unwrap_or(¤t_schema);
111 target_schemas.insert(schema.to_string());
112 }
113
114 let mut tables_by_schema = std::collections::HashMap::new();
115 for schema_name in &target_schemas {
116 let schema_discovery = SchemaDiscovery::new_no_exec(schema_name);
117 let schema = schema_discovery
118 .discover_with(db)
119 .map_err(|err| DbErr::Query(crate::RuntimeErr::SqlxError(err.into())))?;
120
121 tables_by_schema.insert(
122 schema_name.clone(),
123 schema.tables.iter().map(|table| table.write()).collect(),
124 );
125 }
126
127 DiscoveredSchema {
128 current_schema,
129 tables_by_schema,
130 enums_by_schema: Default::default(),
131 }
132 }
133 #[cfg(feature = "sqlx-postgres")]
134 DbBackend::Postgres => {
135 use sea_schema::{postgres::discovery::SchemaDiscovery, probe::SchemaProbe};
136
137 let current_schema: String = db
138 .query_one(
139 sea_query::SelectStatement::new()
140 .expr(sea_schema::postgres::Postgres::get_current_schema()),
141 )?
142 .ok_or_else(|| DbErr::RecordNotFound("Can't get current schema".into()))?
143 .try_get_by_index(0)?;
144
145 let mut target_schemas = std::collections::BTreeSet::new();
147 for entity in &self.entities {
148 let schema = entity.schema_name.as_deref().unwrap_or(¤t_schema);
149 target_schemas.insert(schema.to_string());
150 }
151
152 let mut tables_by_schema = std::collections::HashMap::new();
153 let mut enums_by_schema = std::collections::HashMap::new();
154 for schema_name in &target_schemas {
155 let schema_discovery = SchemaDiscovery::new_no_exec(schema_name);
156 let schema = schema_discovery
157 .discover_with(db)
158 .map_err(|err| DbErr::Query(crate::RuntimeErr::SqlxError(err.into())))?;
159
160 tables_by_schema.insert(
161 schema_name.clone(),
162 schema.tables.iter().map(|table| table.write()).collect(),
163 );
164 enums_by_schema.insert(
165 schema_name.clone(),
166 schema.enums.iter().map(|def| def.write()).collect(),
167 );
168 }
169
170 DiscoveredSchema {
171 current_schema,
172 tables_by_schema,
173 enums_by_schema,
174 }
175 }
176 #[cfg(feature = "sqlx-sqlite")]
177 DbBackend::Sqlite => {
178 use sea_schema::sqlite::{SqliteDiscoveryError, discovery::SchemaDiscovery};
179 let schema = SchemaDiscovery::discover_with(db)
180 .map_err(|err| {
181 DbErr::Query(match err {
182 SqliteDiscoveryError::SqlxError(err) => {
183 crate::RuntimeErr::SqlxError(err.into())
184 }
185 _ => crate::RuntimeErr::Internal(format!("{err:?}")),
186 })
187 })?
188 .merge_indexes_into_table();
189 let mut tables_by_schema = std::collections::HashMap::new();
190 tables_by_schema.insert(
191 String::new(),
192 schema.tables.iter().map(|table| table.write()).collect(),
193 );
194 DiscoveredSchema {
195 current_schema: String::new(),
196 tables_by_schema,
197 enums_by_schema: Default::default(),
198 }
199 }
200 #[cfg(feature = "rusqlite")]
201 DbBackend::Sqlite => {
202 use sea_schema::sqlite::{SqliteDiscoveryError, discovery::SchemaDiscovery};
203 let schema = SchemaDiscovery::discover_with(db)
204 .map_err(|err| {
205 DbErr::Query(match err {
206 SqliteDiscoveryError::RusqliteError(err) => {
207 crate::RuntimeErr::Rusqlite(err.into())
208 }
209 _ => crate::RuntimeErr::Internal(format!("{err:?}")),
210 })
211 })?
212 .merge_indexes_into_table();
213 let mut tables_by_schema = std::collections::HashMap::new();
214 tables_by_schema.insert(
215 String::new(),
216 schema.tables.iter().map(|table| table.write()).collect(),
217 );
218 DiscoveredSchema {
219 current_schema: String::new(),
220 tables_by_schema,
221 enums_by_schema: Default::default(),
222 }
223 }
224 #[allow(unreachable_patterns)]
225 other => {
226 return Err(DbErr::BackendNotSupported {
227 db: other.as_str(),
228 ctx: "SchemaBuilder::sync",
229 });
230 }
231 };
232
233 #[allow(unreachable_code)]
234 let mut created_enums: Vec<Statement> = Default::default();
235
236 #[allow(unreachable_code)]
237 for table_name in self.sorted_tables() {
238 if let Some(entity) = self
239 .entities
240 .iter()
241 .find(|entity| table_name == get_table_name(entity.table.get_table_name()))
242 {
243 entity.sync(db, &_existing, &mut created_enums)?;
244 }
245 }
246
247 Ok(())
248 }
249
250 pub fn apply<C: ConnectionTrait>(self, db: &C) -> Result<(), DbErr> {
254 let mut created_enums: Vec<Statement> = Default::default();
255
256 for table_name in self.sorted_tables() {
257 if let Some(entity) = self
258 .entities
259 .iter()
260 .find(|entity| table_name == get_table_name(entity.table.get_table_name()))
261 {
262 entity.apply(db, &mut created_enums)?;
263 }
264 }
265
266 Ok(())
267 }
268
269 #[allow(dead_code)]
274 #[cfg(all(feature = "schema-sync", feature = "sqlx-sqlite"))]
275 fn _assert_sync_future_is_send(self, db: &crate::DatabaseConnection) {
276 fn assert_send<T: Send>(_: &T) {}
277 assert_send(&self.sync(db));
278 }
279
280 fn sorted_tables(&self) -> Vec<TableName> {
281 let mut sorter = TopologicalSort::<TableName>::new();
282
283 for entity in self.entities.iter() {
284 let table_name = get_table_name(entity.table.get_table_name());
285 sorter.insert(table_name);
286 }
287 for entity in self.entities.iter() {
288 let self_table = get_table_name(entity.table.get_table_name());
289 for fk in entity.table.get_foreign_key_create_stmts().iter() {
290 let fk = fk.get_foreign_key();
291 let ref_table = get_table_name(fk.get_ref_table());
292 if self_table != ref_table {
293 sorter.add_dependency(ref_table, self_table.clone());
295 }
296 }
297 }
298 let mut sorted = Vec::new();
299 while let Some(i) = sorter.pop() {
300 sorted.push(i);
301 }
302 if sorted.len() != self.entities.len() {
303 for entity in self.entities.iter() {
305 let table_name = get_table_name(entity.table.get_table_name());
306 if !sorted.contains(&table_name) {
307 sorted.push(table_name);
308 }
309 }
310 }
311
312 sorted
313 }
314}
315
316struct DiscoveredSchema {
317 current_schema: String,
319 tables_by_schema: std::collections::HashMap<String, Vec<TableCreateStatement>>,
321 enums_by_schema: std::collections::HashMap<String, Vec<TypeCreateStatement>>,
323}
324
325impl DiscoveredSchema {
326 fn find_table(
335 &self,
336 entity_schema: Option<&str>,
337 entity_table_name: &TableName,
338 ) -> Option<&TableCreateStatement> {
339 let schema = entity_schema.unwrap_or(&self.current_schema);
340 let schema_tables = self.tables_by_schema.get(schema)?;
341 let bare_entity_name = TableName(None, entity_table_name.1.clone());
344 schema_tables
345 .iter()
346 .find(|tbl| get_table_name(tbl.get_table_name()) == bare_entity_name)
347 }
348
349 fn find_enums(&self, entity_schema: Option<&str>) -> &[TypeCreateStatement] {
350 let schema = entity_schema.unwrap_or(&self.current_schema);
351 self.enums_by_schema
352 .get(schema)
353 .map(|v| v.as_slice())
354 .unwrap_or(&[])
355 }
356}
357
358impl EntitySchemaInfo {
359 pub fn new<E: EntityTrait>(entity: E, helper: &Schema) -> Self {
361 Self {
362 table: helper.create_table_from_entity(entity),
363 enums: helper.create_enum_from_entity(entity),
364 indexes: helper.create_index_from_entity(entity),
365 schema_name: entity.schema_name().map(|s| s.to_string()),
366 }
367 }
368
369 fn apply<C: ConnectionTrait>(
370 &self,
371 db: &C,
372 created_enums: &mut Vec<Statement>,
373 ) -> Result<(), DbErr> {
374 for stmt in self.enums.iter() {
375 let new_stmt = db.get_database_backend().build(stmt);
376 if !created_enums.iter().any(|s| s == &new_stmt) {
377 db.execute(stmt)?;
378 created_enums.push(new_stmt);
379 }
380 }
381 db.execute(&self.table)?;
382 for stmt in self.indexes.iter() {
383 db.execute(stmt)?;
384 }
385 Ok(())
386 }
387
388 #[allow(dead_code)]
390 fn sync<C: ConnectionTrait>(
391 &self,
392 db: &C,
393 existing: &DiscoveredSchema,
394 created_enums: &mut Vec<Statement>,
395 ) -> Result<(), DbErr> {
396 let db_backend = db.get_database_backend();
397
398 let existing_enums = existing.find_enums(self.schema_name.as_deref());
400 for stmt in self.enums.iter() {
401 let mut has_enum = false;
402 let new_stmt = db_backend.build(stmt);
403 for existing_enum in existing_enums {
404 if db_backend.build(existing_enum) == new_stmt {
405 has_enum = true;
406 break;
408 }
409 }
410 if !has_enum && !created_enums.iter().any(|s| s == &new_stmt) {
411 db.execute(stmt)?;
412 created_enums.push(new_stmt);
413 }
414 }
415 let table_name = get_table_name(self.table.get_table_name());
416 let existing_table = existing.find_table(self.schema_name.as_deref(), &table_name);
418 if let Some(existing_table) = existing_table {
419 for column_def in self.table.get_columns() {
420 let existing_column = existing_table
421 .get_columns()
422 .iter()
423 .find(|c| c.get_column_name() == column_def.get_column_name());
424 let Some(existing_column) = existing_column else {
425 let mut renamed_from = "";
426 if let Some(comment) = &column_def.get_column_spec().comment
427 && let Some((_, suffix)) = comment.rsplit_once("renamed_from \"")
428 && let Some((prefix, _)) = suffix.split_once('"')
429 {
430 renamed_from = prefix;
431 }
432 if renamed_from.is_empty() {
433 db.execute(
434 TableAlterStatement::new()
435 .table(self.table.get_table_name().expect("Checked above").clone())
436 .add_column(column_def.to_owned()),
437 )?;
438 } else {
439 db.execute(
440 TableAlterStatement::new()
441 .table(self.table.get_table_name().expect("Checked above").clone())
442 .rename_column(
443 renamed_from.to_owned(),
444 column_def.get_column_name(),
445 ),
446 )?;
447 }
448 continue;
449 };
450 if let (Some(desired), Some(current)) = (
453 column_def.get_column_type(),
454 existing_column.get_column_type(),
455 ) {
456 let backend = db.get_database_backend();
457 let desired_sql = render_column_type(backend, desired);
458 let current_sql = render_column_type(backend, current);
459 if desired_sql != current_sql {
460 tracing::warn!(
461 "schema sync: column `{}`.`{}` is `{}` in the database but the entity \
462 defines `{}`; sync is non-destructive and will not alter it (apply the \
463 change with a migration)",
464 table_name.1.to_string(),
465 column_def.get_column_name(),
466 current_sql,
467 desired_sql,
468 );
469 }
470 }
471 }
472 if db.get_database_backend() != DbBackend::Sqlite {
473 for foreign_key in self.table.get_foreign_key_create_stmts().iter() {
474 let mut key_exists = false;
475 for existing_key in existing_table.get_foreign_key_create_stmts().iter() {
476 if compare_foreign_key(foreign_key, existing_key) {
477 key_exists = true;
478 break;
479 }
480 }
481 if !key_exists {
482 db.execute(foreign_key)?;
483 }
484 }
485 }
486 } else {
487 db.execute(&self.table)?;
488 }
489 for stmt in self.indexes.iter() {
490 let mut has_index = false;
491 if let Some(existing_table) = existing_table {
492 for existing_index in existing_table.get_indexes() {
493 if existing_index.get_index_spec().get_column_names()
494 == stmt.get_index_spec().get_column_names()
495 {
496 has_index = true;
497 break;
498 }
499 }
500 }
501 if !has_index {
502 let mut stmt = stmt.clone();
504 stmt.if_not_exists();
505 db.execute(&stmt)?;
506 }
507 }
508 if let Some(existing_table) = existing_table {
509 for column_def in self.table.get_columns() {
512 if column_def.get_column_spec().unique {
513 let col_name = column_def.get_column_name();
514 let col_exists = existing_table
515 .get_columns()
516 .iter()
517 .any(|c| c.get_column_name() == col_name);
518 if !col_exists {
519 continue;
522 }
523 let already_unique = existing_table.get_indexes().iter().any(|idx| {
524 if !idx.is_unique_key() {
525 return false;
526 }
527 let cols = idx.get_index_spec().get_column_names();
528 cols.len() == 1 && cols[0] == col_name
529 });
530 if !already_unique {
531 let table_name =
532 self.table.get_table_name().expect("table must have a name");
533 let tbl_str = table_name.sea_orm_table().to_string();
534 let table_ref = index_table_ref(table_name.clone(), db_backend);
535 db.execute(
536 Index::create()
537 .name(format!("idx-{tbl_str}-{col_name}"))
538 .table(table_ref)
539 .col(col_name.into_iden())
540 .unique()
541 .if_not_exists(),
542 )?;
543 }
544 }
545 }
546 }
547 if let Some(existing_table) = existing_table {
548 for existing_index in existing_table.get_indexes() {
551 if existing_index.is_unique_key() {
552 let mut has_index = false;
553 for stmt in self.indexes.iter() {
554 if existing_index.get_index_spec().get_column_names()
555 == stmt.get_index_spec().get_column_names()
556 {
557 has_index = true;
558 break;
559 }
560 }
561 if !has_index {
566 let index_cols = existing_index.get_index_spec().get_column_names();
567 if index_cols.len() == 1 {
568 for column_def in self.table.get_columns() {
569 if column_def.get_column_name() == index_cols[0]
570 && column_def.get_column_spec().unique
571 {
572 has_index = true;
573 break;
574 }
575 }
576 }
577 }
578 if !has_index
579 && let Some(drop_existing) = existing_index
580 .get_index_spec()
581 .get_name()
582 .map(|s| s.to_owned())
583 {
584 if db_backend == DbBackend::Postgres {
585 db.execute(
590 TableAlterStatement::new()
591 .table(
592 self.table.get_table_name().expect("Checked above").clone(),
593 )
594 .drop_constraint(drop_existing),
595 )?;
596 } else {
597 db.execute(sea_query::Index::drop().name(drop_existing))?;
598 }
599 }
600 }
601 }
602 }
603 Ok(())
604 }
605
606 fn debug_print(
607 &self,
608 f: &mut std::fmt::Formatter<'_>,
609 backend: &DbBackend,
610 ) -> std::fmt::Result {
611 write!(f, "EntitySchemaInfo {{")?;
612 write!(f, " table: {:?}", backend.build(&self.table).to_string())?;
613 write!(f, " enums: [")?;
614 for (i, stmt) in self.enums.iter().enumerate() {
615 if i > 0 {
616 write!(f, ", ")?;
617 }
618 write!(f, "{:?}", backend.build(stmt).to_string())?;
619 }
620 write!(f, " ]")?;
621 write!(f, " indexes: [")?;
622 for (i, stmt) in self.indexes.iter().enumerate() {
623 if i > 0 {
624 write!(f, ", ")?;
625 }
626 write!(f, "{:?}", backend.build(stmt).to_string())?;
627 }
628 write!(f, " ]")?;
629 write!(f, " }}")
630 }
631}
632
633fn get_table_name(table_ref: Option<&TableRef>) -> TableName {
634 match table_ref {
635 Some(TableRef::Table(table_name, _)) => table_name.clone(),
636 None => panic!("Expect TableCreateStatement is properly built"),
637 _ => unreachable!("Unexpected {table_ref:?}"),
638 }
639}
640
641#[allow(dead_code)]
646fn render_column_type(backend: DbBackend, col_type: &sea_query::ColumnType) -> String {
647 use sea_query::backend::TableBuilder;
648 let mut sql = String::new();
649 match backend {
650 DbBackend::MySql => sea_query::MysqlQueryBuilder.prepare_column_type(col_type, &mut sql),
651 DbBackend::Postgres => {
652 sea_query::PostgresQueryBuilder.prepare_column_type(col_type, &mut sql)
653 }
654 DbBackend::Sqlite => sea_query::SqliteQueryBuilder.prepare_column_type(col_type, &mut sql),
655 }
656 sql
657}
658
659fn compare_foreign_key(a: &ForeignKeyCreateStatement, b: &ForeignKeyCreateStatement) -> bool {
660 let a = a.get_foreign_key();
661 let b = b.get_foreign_key();
662
663 a.get_name() == b.get_name()
664 || (a.get_ref_table() == b.get_ref_table()
665 && a.get_columns() == b.get_columns()
666 && a.get_ref_columns() == b.get_ref_columns())
667}