1use crate::{BannerVersion, BigIntegerType, DateTimeCrate, GenerateSubcommands};
2use core::time;
3use sea_orm_codegen::{
4 BannerVersion as CodegenBannerVersion, BigIntegerType as CodegenBigIntegerType,
5 DateTimeCrate as CodegenDateTimeCrate, EntityFormat, EntityTransformer, EntityWriterContext,
6 MergeReport, OutputFile, WithPrelude, WithSerde, merge_entity_files,
7};
8use std::{error::Error, fs, path::Path, process::Command, str::FromStr};
9use tracing_subscriber::{EnvFilter, prelude::*};
10use url::Url;
11
12fn split_by_comma_ignoring_parentheses(s: &str) -> Vec<String> {
16 let mut result = Vec::new();
17 let mut current = String::new();
18 let mut paren_depth = 0usize;
19 let mut bracket_depth = 0usize;
20 let mut brace_depth = 0usize;
21
22 for c in s.chars() {
23 match c {
24 '(' => {
25 paren_depth += 1;
26 current.push(c);
27 }
28 ')' => {
29 paren_depth = paren_depth.saturating_sub(1);
30 current.push(c);
31 }
32 '[' => {
33 bracket_depth += 1;
34 current.push(c);
35 }
36 ']' => {
37 bracket_depth = bracket_depth.saturating_sub(1);
38 current.push(c);
39 }
40 '{' => {
41 brace_depth += 1;
42 current.push(c);
43 }
44 '}' => {
45 brace_depth = brace_depth.saturating_sub(1);
46 current.push(c);
47 }
48 ',' if paren_depth == 0 && bracket_depth == 0 && brace_depth == 0 => {
49 let trimmed = current.trim();
50 if !trimmed.is_empty() {
51 result.push(trimmed.to_string());
52 }
53 current.clear();
54 }
55 _ => {
56 current.push(c);
57 }
58 }
59 }
60
61 let trimmed = current.trim();
63 if !trimmed.is_empty() {
64 result.push(trimmed.to_string());
65 }
66
67 result
68}
69
70fn process_comma_separated_values(values: Vec<String>) -> Vec<String> {
74 values
75 .into_iter()
76 .flat_map(|s| split_by_comma_ignoring_parentheses(&s))
77 .collect()
78}
79
80#[cfg(feature = "sqlx-sqlite")]
86fn sqlite_column_is_generated(col: &sea_schema::sqlite::def::ColumnInfo) -> bool {
87 use sea_schema::sqlite::def::ColumnVisibility;
88 matches!(
89 col.hidden,
90 ColumnVisibility::GeneratedVirtual | ColumnVisibility::GeneratedStored
91 )
92}
93
94#[cfg(feature = "sqlx-postgres")]
95fn remove_postgres_partial_unique_indexes(table: &mut sea_schema::postgres::def::TableDef) {
96 table
97 .unique_constraints
98 .retain(|constraint| !constraint.is_partial);
99}
100
101fn database_name_from_url(url: &Url) -> Option<&str> {
109 match url.path_segments() {
110 Some(mut segments) => segments.next(),
111 None => Some(url.path()),
112 }
113 .filter(|name| !name.is_empty())
114}
115
116pub async fn run_generate_command(
117 command: GenerateSubcommands,
118 verbose: bool,
119) -> Result<(), Box<dyn Error>> {
120 match command {
121 GenerateSubcommands::Entity {
122 entity_format,
123 compact_format: _,
124 expanded_format,
125 frontend_format,
126 include_hidden_tables,
127 tables,
128 ignore_tables,
129 max_connections,
130 acquire_timeout,
131 output_dir,
132 database_schema,
133 database_url,
134 with_prelude,
135 with_serde,
136 serde_skip_deserializing_primary_key,
137 serde_skip_hidden_column,
138 with_copy_enums,
139 date_time_crate,
140 big_integer_type,
141 lib,
142 model_extra_derives,
143 model_extra_attributes,
144 enum_extra_derives,
145 enum_extra_attributes,
146 column_extra_derives,
147 seaography,
148 impl_active_model_behavior,
149 preserve_user_modifications,
150 banner_version,
151 er_diagram,
152 } => {
153 if verbose {
154 let _ = tracing_subscriber::fmt()
155 .with_max_level(tracing::Level::DEBUG)
156 .with_test_writer()
157 .try_init();
158 } else {
159 let filter_layer = EnvFilter::try_new("sea_orm_codegen=info").unwrap();
160 let fmt_layer = tracing_subscriber::fmt::layer()
161 .with_target(false)
162 .with_level(false)
163 .without_time();
164
165 let _ = tracing_subscriber::registry()
166 .with(filter_layer)
167 .with(fmt_layer)
168 .try_init();
169 }
170
171 let url = Url::parse(&database_url)?;
174
175 let is_sqlite = url.scheme() == "sqlite";
180
181 let filter_tables =
183 |table: &String| -> bool { tables.is_empty() || tables.contains(table) };
184
185 let filter_hidden_tables = |table: &str| -> bool {
186 if include_hidden_tables {
187 true
188 } else {
189 !table.starts_with('_')
190 }
191 };
192
193 let filter_skip_tables = |table: &String| -> bool { !ignore_tables.contains(table) };
194
195 let _database_name = if !is_sqlite {
196 database_name_from_url(&url).unwrap_or_else(|| {
200 panic!(
201 "There is no database name as part of the url path: {}",
202 url.as_str()
203 )
204 })
205 } else {
206 Default::default()
207 };
208
209 let (schema_name, table_stmts) = match url.scheme() {
210 "mysql" => {
211 #[cfg(not(feature = "sqlx-mysql"))]
212 {
213 panic!("mysql feature is off")
214 }
215 #[cfg(feature = "sqlx-mysql")]
216 {
217 use sea_schema::mysql::discovery::SchemaDiscovery;
218 use sqlx::MySql;
219
220 println!("Connecting to MySQL ...");
221 let connection = sqlx_connect::<MySql>(
222 max_connections,
223 acquire_timeout,
224 url.as_str(),
225 None,
226 )
227 .await?;
228 println!("Discovering schema ...");
229 let schema_discovery = SchemaDiscovery::new(connection, _database_name);
230 let schema = schema_discovery.discover().await?;
231 let table_stmts = schema
232 .tables
233 .into_iter()
234 .filter(|schema| filter_tables(&schema.info.name))
235 .filter(|schema| filter_hidden_tables(&schema.info.name))
236 .filter(|schema| filter_skip_tables(&schema.info.name))
237 .map(|mut schema| {
238 schema.columns.retain(|col| !col.extra.generated);
240 schema.write()
241 })
242 .collect();
243 (None, table_stmts)
244 }
245 }
246 "sqlite" => {
247 #[cfg(not(feature = "sqlx-sqlite"))]
248 {
249 panic!("sqlite feature is off")
250 }
251 #[cfg(feature = "sqlx-sqlite")]
252 {
253 use sea_schema::sqlite::discovery::SchemaDiscovery;
254 use sqlx::Sqlite;
255
256 println!("Connecting to SQLite ...");
257 let connection = sqlx_connect::<Sqlite>(
258 max_connections,
259 acquire_timeout,
260 url.as_str(),
261 None,
262 )
263 .await?;
264 println!("Discovering schema ...");
265 let schema_discovery = SchemaDiscovery::new(connection);
266 let schema = schema_discovery
267 .discover()
268 .await?
269 .merge_indexes_into_table();
270 let table_stmts = schema
271 .tables
272 .into_iter()
273 .filter(|schema| filter_tables(&schema.name))
274 .filter(|schema| filter_hidden_tables(&schema.name))
275 .filter(|schema| filter_skip_tables(&schema.name))
276 .map(|mut schema| {
277 schema
281 .columns
282 .retain(|col| !sqlite_column_is_generated(col));
283 schema.write()
284 })
285 .collect();
286 (None, table_stmts)
287 }
288 }
289 "postgres" | "postgresql" => {
290 #[cfg(not(feature = "sqlx-postgres"))]
291 {
292 panic!("postgres feature is off")
293 }
294 #[cfg(feature = "sqlx-postgres")]
295 {
296 use sea_schema::postgres::discovery::SchemaDiscovery;
297 use sqlx::Postgres;
298
299 println!("Connecting to Postgres ...");
300 let schema = database_schema.as_deref().unwrap_or("public");
301 let connection = sqlx_connect::<Postgres>(
302 max_connections,
303 acquire_timeout,
304 url.as_str(),
305 Some(schema),
306 )
307 .await?;
308 println!("Discovering schema ...");
309 let schema_discovery = SchemaDiscovery::new(connection, schema);
310 let schema = schema_discovery.discover().await?;
311 let table_stmts = schema
312 .tables
313 .into_iter()
314 .filter(|schema| filter_tables(&schema.info.name))
315 .filter(|schema| filter_hidden_tables(&schema.info.name))
316 .filter(|schema| filter_skip_tables(&schema.info.name))
317 .map(|mut schema| {
318 schema.columns.retain(|col| col.generated.is_none());
320 remove_postgres_partial_unique_indexes(&mut schema);
322 schema.write()
323 })
324 .collect();
325 (database_schema, table_stmts)
326 }
327 }
328 _ => unimplemented!("{} is not supported", url.scheme()),
329 };
330 println!("... discovered.");
331
332 let model_extra_derives = process_comma_separated_values(model_extra_derives);
336 let model_extra_attributes = process_comma_separated_values(model_extra_attributes);
337 let enum_extra_derives = process_comma_separated_values(enum_extra_derives);
338 let enum_extra_attributes = process_comma_separated_values(enum_extra_attributes);
339 let column_extra_derives = process_comma_separated_values(column_extra_derives);
340
341 let writer_context = EntityWriterContext::new(
342 if expanded_format {
343 EntityFormat::Expanded
344 } else if frontend_format {
345 EntityFormat::Frontend
346 } else if let Some(entity_format) = entity_format {
347 EntityFormat::from_str(&entity_format).expect("Invalid entity-format option")
348 } else {
349 EntityFormat::default()
350 },
351 WithPrelude::from_str(&with_prelude).expect("Invalid prelude option"),
352 WithSerde::from_str(&with_serde).expect("Invalid serde derive option"),
353 with_copy_enums,
354 date_time_crate.into(),
355 big_integer_type.into(),
356 schema_name,
357 lib,
358 serde_skip_deserializing_primary_key,
359 serde_skip_hidden_column,
360 model_extra_derives,
361 model_extra_attributes,
362 enum_extra_derives,
363 enum_extra_attributes,
364 column_extra_derives,
365 seaography,
366 impl_active_model_behavior,
367 banner_version.into(),
368 );
369 let entity_writer = EntityTransformer::transform(table_stmts)?;
370
371 let dir = Path::new(&output_dir);
372 fs::create_dir_all(dir)?;
373
374 if er_diagram {
375 let diagram = entity_writer.generate_er_diagram();
376 let diagram_path = dir.join("entities.mermaid");
377 fs::write(&diagram_path, &diagram)?;
378 println!("Writing {}", diagram_path.display());
379 }
380
381 let output = entity_writer.generate(&writer_context);
382
383 let mut merge_fallback_files: Vec<String> = Vec::new();
384
385 for OutputFile { name, content } in output.files.iter() {
386 let file_path = dir.join(name);
387 println!("Writing {}", file_path.display());
388
389 if !matches!(
390 name.as_str(),
391 "mod.rs" | "lib.rs" | "prelude.rs" | "sea_orm_active_enums.rs"
392 ) && file_path.exists()
393 && preserve_user_modifications
394 {
395 let prev_content = fs::read_to_string(&file_path)?;
396 match merge_entity_files(&prev_content, content) {
397 Ok(merged) => {
398 fs::write(file_path, merged)?;
399 }
400 Err(MergeReport {
401 output,
402 warnings,
403 fallback_applied,
404 }) => {
405 for message in warnings {
406 eprintln!("{message}");
407 }
408 fs::write(file_path, output)?;
409 if fallback_applied {
410 merge_fallback_files.push(name.clone());
411 }
412 }
413 }
414 } else {
415 fs::write(file_path, content)?;
416 };
417 }
418
419 for OutputFile { name, .. } in output.files.iter() {
421 let exit_status = Command::new("rustfmt").arg(dir.join(name)).status()?; if !exit_status.success() {
423 return Err(format!("Fail to format file `{name}`").into());
425 }
426 }
427
428 if merge_fallback_files.is_empty() {
429 println!("... Done.");
430 } else {
431 return Err(format!(
432 "Merge fallback applied for {} file(s): \n{}",
433 merge_fallback_files.len(),
434 merge_fallback_files.join("\n")
435 )
436 .into());
437 }
438 }
439 }
440
441 Ok(())
442}
443
444async fn sqlx_connect<DB>(
445 max_connections: u32,
446 acquire_timeout: u64,
447 url: &str,
448 schema: Option<&str>,
449) -> Result<sqlx::Pool<DB>, Box<dyn Error>>
450where
451 DB: sqlx::Database,
452 for<'a> &'a mut <DB as sqlx::Database>::Connection: sqlx::Executor<'a>,
453{
454 let mut pool_options = sqlx::pool::PoolOptions::<DB>::new()
455 .max_connections(max_connections)
456 .acquire_timeout(time::Duration::from_secs(acquire_timeout));
457 if let Some(schema) = schema {
460 let sql = format!("SET search_path = '{schema}'");
461 pool_options = pool_options.after_connect(move |conn, _| {
462 let sql = sql.clone();
463 Box::pin(async move {
464 sqlx::Executor::execute(conn, sqlx::AssertSqlSafe(sql))
465 .await
466 .map(|_| ())
467 })
468 });
469 }
470 pool_options.connect(url).await.map_err(Into::into)
471}
472
473impl From<DateTimeCrate> for CodegenDateTimeCrate {
474 fn from(date_time_crate: DateTimeCrate) -> CodegenDateTimeCrate {
475 match date_time_crate {
476 DateTimeCrate::Chrono => CodegenDateTimeCrate::Chrono,
477 DateTimeCrate::Time => CodegenDateTimeCrate::Time,
478 }
479 }
480}
481
482impl From<BigIntegerType> for CodegenBigIntegerType {
483 fn from(date_time_crate: BigIntegerType) -> CodegenBigIntegerType {
484 match date_time_crate {
485 BigIntegerType::I64 => CodegenBigIntegerType::I64,
486 BigIntegerType::I32 => CodegenBigIntegerType::I32,
487 }
488 }
489}
490
491impl From<BannerVersion> for CodegenBannerVersion {
492 fn from(banner_version: BannerVersion) -> CodegenBannerVersion {
493 match banner_version {
494 BannerVersion::Off => CodegenBannerVersion::Off,
495 BannerVersion::Major => CodegenBannerVersion::Major,
496 BannerVersion::Minor => CodegenBannerVersion::Minor,
497 BannerVersion::Patch => CodegenBannerVersion::Patch,
498 }
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use clap::Parser;
505
506 use super::*;
507 use crate::{Cli, Commands};
508
509 #[test]
510 fn test_database_name_from_url() {
511 let cases = [
512 ("postgres:my_db", Some("my_db")),
514 ("postgresql:my_db", Some("my_db")),
515 ("postgres://user:pass@localhost:5432/my_db", Some("my_db")),
517 ("postgres:///my_db", Some("my_db")),
518 ("mysql://root:root@localhost:3306/my_db", Some("my_db")),
519 ("postgres://localhost/my_db/extra", Some("my_db")),
521 ("postgresql://root:root@localhost:3306", None),
523 ("mysql://root:root@localhost:3306/", None),
524 ("postgres:", None),
525 ];
526
527 for (input, expected) in cases {
528 let url = Url::parse(input).unwrap_or_else(|e| panic!("could not parse {input}: {e}"));
529 assert_eq!(
530 database_name_from_url(&url),
531 expected,
532 "unexpected database name for {input}"
533 );
534 }
535 }
536
537 #[test]
538 #[should_panic(
539 expected = "called `Result::unwrap()` on an `Err` value: RelativeUrlWithoutBase"
540 )]
541 fn test_generate_entity_no_protocol() {
542 let cli = Cli::parse_from([
543 "sea-orm-cli",
544 "generate",
545 "entity",
546 "--database-url",
547 "://root:root@localhost:3306/database",
548 ]);
549
550 match cli.command {
551 Commands::Generate { command } => {
552 smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
553 }
554 _ => unreachable!(),
555 }
556 }
557
558 #[test]
559 #[should_panic(
560 expected = "There is no database name as part of the url path: postgresql://root:root@localhost:3306"
561 )]
562 fn test_generate_entity_no_database_section() {
563 let cli = Cli::parse_from([
564 "sea-orm-cli",
565 "generate",
566 "entity",
567 "--database-url",
568 "postgresql://root:root@localhost:3306",
569 ]);
570
571 match cli.command {
572 Commands::Generate { command } => {
573 smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
574 }
575 _ => unreachable!(),
576 }
577 }
578
579 #[test]
580 #[should_panic(
581 expected = "There is no database name as part of the url path: mysql://root:root@localhost:3306/"
582 )]
583 fn test_generate_entity_no_database_path() {
584 let cli = Cli::parse_from([
585 "sea-orm-cli",
586 "generate",
587 "entity",
588 "--database-url",
589 "mysql://root:root@localhost:3306/",
590 ]);
591
592 match cli.command {
593 Commands::Generate { command } => {
594 smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
595 }
596 _ => unreachable!(),
597 }
598 }
599
600 #[test]
601 #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: EmptyHost")]
602 fn test_generate_entity_no_host() {
603 let cli = Cli::parse_from([
604 "sea-orm-cli",
605 "generate",
606 "entity",
607 "--database-url",
608 "postgres://root:root@/database",
609 ]);
610
611 match cli.command {
612 Commands::Generate { command } => {
613 smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
614 }
615 _ => unreachable!(),
616 }
617 }
618
619 #[test]
620 fn test_split_by_comma_simple() {
621 let result = super::split_by_comma_ignoring_parentheses("a,b,c");
623 assert_eq!(result, vec!["a", "b", "c"]);
624 }
625
626 #[test]
627 fn test_split_by_comma_with_parentheses() {
628 let result = super::split_by_comma_ignoring_parentheses("test(a, b)");
630 assert_eq!(result, vec!["test(a, b)"]);
631
632 let result = super::split_by_comma_ignoring_parentheses("attr1,test(a, b)");
634 assert_eq!(result, vec!["attr1", "test(a, b)"]);
635 }
636
637 #[test]
638 fn test_split_by_comma_with_nested_parentheses() {
639 let result =
641 super::split_by_comma_ignoring_parentheses("cfg_attr(debug_assertions, derive(Debug))");
642 assert_eq!(result, vec!["cfg_attr(debug_assertions, derive(Debug))"]);
643
644 let result = super::split_by_comma_ignoring_parentheses(
646 "cfg_attr(feature1, attr(a, b)),cfg_attr(feature2, attr(c, d))",
647 );
648 assert_eq!(
649 result,
650 vec![
651 "cfg_attr(feature1, attr(a, b))",
652 "cfg_attr(feature2, attr(c, d))"
653 ]
654 );
655 }
656
657 #[test]
658 fn test_split_by_comma_with_brackets() {
659 let result = super::split_by_comma_ignoring_parentheses(
661 "serde(rename_all = \"camelCase\"),ts(export)",
662 );
663 assert_eq!(
664 result,
665 vec!["serde(rename_all = \"camelCase\")", "ts(export)"]
666 );
667
668 let result = super::split_by_comma_ignoring_parentheses("attr[key, value],other");
670 assert_eq!(result, vec!["attr[key, value]", "other"]);
671 }
672
673 #[test]
674 fn test_split_by_comma_with_braces() {
675 let result = super::split_by_comma_ignoring_parentheses("derive{a, b},other");
677 assert_eq!(result, vec!["derive{a, b}", "other"]);
678 }
679
680 #[test]
681 fn test_split_by_comma_empty() {
682 let result = super::split_by_comma_ignoring_parentheses("");
684 assert!(result.is_empty());
685
686 let result = super::split_by_comma_ignoring_parentheses(" ");
688 assert!(result.is_empty());
689 }
690
691 #[test]
692 fn test_split_by_comma_whitespace_handling() {
693 let result = super::split_by_comma_ignoring_parentheses(" a , b ");
695 assert_eq!(result, vec!["a", "b"]);
696
697 let result = super::split_by_comma_ignoring_parentheses("test( a , b )");
699 assert_eq!(result, vec!["test( a , b )"]);
700 }
701
702 #[test]
703 fn test_process_comma_separated_values() {
704 let input = vec![
706 "attr1,attr2".to_string(),
707 "test(a, b)".to_string(),
708 "attr3".to_string(),
709 ];
710 let result = super::process_comma_separated_values(input);
711 assert_eq!(result, vec!["attr1", "attr2", "test(a, b)", "attr3"]);
712 }
713
714 #[test]
715 fn test_split_by_comma_real_world_examples() {
716 let result = super::split_by_comma_ignoring_parentheses(
718 "cfg_attr(debug_assertions, derive(Debug)),serde(rename_all = \"camelCase\")",
719 );
720 assert_eq!(
721 result,
722 vec![
723 "cfg_attr(debug_assertions, derive(Debug))",
724 "serde(rename_all = \"camelCase\")"
725 ]
726 );
727
728 let result = super::split_by_comma_ignoring_parentheses(
730 "derive(Debug, Clone),derive(Serialize, Deserialize)",
731 );
732 assert_eq!(
733 result,
734 vec!["derive(Debug, Clone)", "derive(Serialize, Deserialize)"]
735 );
736 }
737
738 #[cfg(feature = "sqlx-sqlite")]
742 #[test]
743 fn test_generate_entity_skips_sqlite_generated_columns() {
744 use sea_schema::sea_query::ColumnType;
745 use sea_schema::sqlite::def::{ColumnInfo, ColumnVisibility, DefaultType, TableDef};
746
747 let col = |cid, name: &str, hidden| ColumnInfo {
748 cid,
749 name: name.to_owned(),
750 r#type: ColumnType::Integer,
751 not_null: true,
752 default_value: DefaultType::Unspecified,
753 primary_key: cid == 0,
754 hidden,
755 };
756
757 let mut table = TableDef {
758 name: "widget".to_owned(),
759 foreign_keys: vec![],
760 indexes: vec![],
761 constraints: vec![],
762 columns: vec![
763 col(0, "id", ColumnVisibility::Visible),
764 col(1, "w", ColumnVisibility::Visible),
765 col(2, "area", ColumnVisibility::GeneratedVirtual),
766 col(3, "area_stored", ColumnVisibility::GeneratedStored),
767 ],
768 auto_increment: false,
769 };
770
771 assert!(!super::sqlite_column_is_generated(&table.columns[0]));
773 assert!(!super::sqlite_column_is_generated(&table.columns[1]));
774 assert!(super::sqlite_column_is_generated(&table.columns[2]));
775 assert!(super::sqlite_column_is_generated(&table.columns[3]));
776
777 table
779 .columns
780 .retain(|col| !super::sqlite_column_is_generated(col));
781 let stmt = table.write();
782 let names: Vec<String> = stmt
783 .get_columns()
784 .iter()
785 .map(|c| c.get_column_name())
786 .collect();
787 assert_eq!(names, ["id", "w"]);
788 }
789
790 #[cfg(all(feature = "sqlx-sqlite", feature = "tokio"))]
797 #[tokio::test]
798 async fn test_sqlite_discovery_reports_generated_columns() {
799 use sea_schema::sqlite::discovery::SchemaDiscovery;
800 use sqlx::sqlite::SqlitePoolOptions;
801
802 let pool = SqlitePoolOptions::new()
806 .max_connections(1)
807 .connect("sqlite::memory:")
808 .await
809 .expect("open in-memory sqlite");
810
811 sqlx::query(
812 "CREATE TABLE widget ( \
813 id INTEGER PRIMARY KEY, \
814 w INTEGER NOT NULL, \
815 h INTEGER NOT NULL, \
816 area_virtual INTEGER GENERATED ALWAYS AS (w * h) VIRTUAL, \
817 area_stored INTEGER GENERATED ALWAYS AS (w * h) STORED \
818 )",
819 )
820 .execute(&pool)
821 .await
822 .expect("create table");
823
824 let schema = SchemaDiscovery::new(pool)
825 .discover()
826 .await
827 .expect("discover schema");
828
829 let table = schema
830 .tables
831 .iter()
832 .find(|table| table.name == "widget")
833 .expect("widget table discovered");
834
835 let generated: Vec<&str> = table
838 .columns
839 .iter()
840 .filter(|col| super::sqlite_column_is_generated(col))
841 .map(|col| col.name.as_str())
842 .collect();
843 assert_eq!(generated, ["area_virtual", "area_stored"]);
844
845 let kept: Vec<&str> = table
848 .columns
849 .iter()
850 .filter(|col| !super::sqlite_column_is_generated(col))
851 .map(|col| col.name.as_str())
852 .collect();
853 assert_eq!(kept, ["id", "w", "h"]);
854 }
855
856 #[cfg(feature = "sqlx-postgres")]
857 #[test]
858 fn filter_out_postgres_partial_unique_indexes() {
859 use sea_schema::postgres::def::{TableDef, TableInfo, Unique};
860
861 let unique_constraint = Unique {
862 name: "login_email_key".to_owned(),
863 columns: vec!["email".to_owned()],
864 is_partial: false,
865 };
866 let partial_constraint = Unique {
867 name: "login_human_login_id_key".to_owned(),
868 columns: vec!["human_login_id".to_owned()],
869 is_partial: true,
870 };
871 let mut table = TableDef {
872 info: TableInfo {
873 name: "login".to_owned(),
874 of_type: None,
875 },
876 columns: vec![],
877 check_constraints: vec![],
878 not_null_constraints: vec![],
879 unique_constraints: vec![unique_constraint.clone(), partial_constraint],
880 primary_key_constraints: vec![],
881 reference_constraints: vec![],
882 exclusion_constraints: vec![],
883 };
884
885 super::remove_postgres_partial_unique_indexes(&mut table);
886
887 assert_eq!(table.unique_constraints.len(), 1);
888 assert_eq!(table.unique_constraints[0], unique_constraint);
889 }
890}