1use std::borrow::Cow;
14use std::collections::HashSet;
15use std::path::PathBuf;
16
17use inquire::validator::Validation;
18use inquire::{Confirm, MultiSelect, Select, Text};
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21
22use crate::config::{Config, Dialect};
23use crate::error::CliError;
24use crate::output;
25
26#[derive(clap::Args, Debug)]
29pub struct NewOptions {
30 #[arg(long)]
32 pub dialect: Option<Dialect>,
33
34 #[arg(long)]
36 pub schema: Option<String>,
37
38 #[arg(long)]
40 pub json: bool,
41
42 #[arg(long, requires = "json", value_name = "PATH")]
44 pub from: Option<PathBuf>,
45
46 #[arg(long = "export-json", value_name = "PATH")]
48 pub export_json: Option<PathBuf>,
49
50 #[arg(long = "schema-help")]
52 pub schema_help: bool,
53}
54
55pub fn run(config: Option<&Config>, options: &NewOptions) -> Result<(), CliError> {
65 if options.schema_help {
67 print_json_schema();
68 return Ok(());
69 }
70
71 let def = if options.json {
73 load_json(options.from.as_deref())?
74 } else {
75 collect_interactively(config, options)?
76 };
77
78 validate_schema(&def)?;
80
81 if let Some(ref export_path) = options.export_json {
83 export_to_json(&def, export_path)?;
84 }
85
86 let output_path = if let Some(ref s) = options.schema {
88 s.clone()
89 } else {
90 def.output_path.clone()
91 };
92
93 let code = match def.dialect {
95 Dialect::Sqlite | Dialect::Turso => generate_sqlite(
96 &def.tables,
97 &def.indexes,
98 &def.foreign_keys,
99 &def.schema_name,
100 def.casing,
101 ),
102 Dialect::Postgresql => generate_postgres(
103 &def.tables,
104 &def.indexes,
105 &def.foreign_keys,
106 &def.enums,
107 &def.schema_name,
108 def.casing,
109 ),
110 Dialect::Mysql => generate_mysql(
111 &def.tables,
112 &def.indexes,
113 &def.foreign_keys,
114 &def.enums,
115 &def.schema_name,
116 def.casing,
117 )?,
118 };
119
120 let path = PathBuf::from(&output_path);
122 if let Some(parent) = path.parent() {
123 std::fs::create_dir_all(parent)
124 .map_err(|e| CliError::IoError(format!("Failed to create directory: {e}")))?;
125 }
126 std::fs::write(&path, &code)
127 .map_err(|e| CliError::IoError(format!("Failed to write schema file: {e}")))?;
128
129 println!();
131 println!("{}", output::success("Schema generated successfully!"));
132 println!();
133 println!(
134 " Tables: {}",
135 def.tables
136 .iter()
137 .map(|t| t.name.as_str())
138 .collect::<Vec<_>>()
139 .join(", ")
140 );
141 if !def.indexes.is_empty() {
142 println!(
143 " Indexes: {}",
144 def.indexes
145 .iter()
146 .map(|i| i.name.as_str())
147 .collect::<Vec<_>>()
148 .join(", ")
149 );
150 }
151 if !def.foreign_keys.is_empty() {
152 println!(" Foreign keys: {}", def.foreign_keys.len());
153 }
154 println!(" Output: {output_path}");
155 if let Some(ref export_path) = options.export_json {
156 println!(" JSON export: {}", export_path.display());
157 }
158 println!();
159 println!("Next steps:");
160 println!(
161 " Run {} to generate your first migration",
162 output::heading("drizzle generate")
163 );
164
165 Ok(())
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
172pub struct SchemaDefinition {
173 pub dialect: Dialect,
175 #[serde(default = "default_casing")]
177 pub casing: FieldCasing,
178 #[serde(default = "default_schema_name")]
180 pub schema_name: String,
181 #[serde(default = "default_output_path")]
183 pub output_path: String,
184 #[serde(default)]
186 pub enums: Vec<EnumDef>,
187 pub tables: Vec<TableDef>,
189 #[serde(default)]
191 pub indexes: Vec<IndexDef>,
192 #[serde(default)]
194 pub foreign_keys: Vec<ForeignKeyDef>,
195}
196
197const fn default_casing() -> FieldCasing {
198 FieldCasing::Snake
199}
200
201fn default_schema_name() -> String {
202 "AppSchema".to_string()
203}
204
205fn default_output_path() -> String {
206 "src/schema.rs".to_string()
207}
208
209fn default_fk_action() -> String {
210 "No Action".to_string()
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
217pub struct EnumDef {
218 pub name: String,
220 pub variants: Vec<String>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
226pub struct TableDef {
227 pub name: String,
229 pub columns: Vec<ColumnDef>,
231 #[serde(default)]
233 pub strict: bool,
234 #[serde(default)]
236 pub without_rowid: bool,
237 #[serde(default = "default_pg_schema")]
239 pub pg_schema: String,
240}
241
242fn default_pg_schema() -> String {
243 "public".to_string()
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
253#[serde(rename_all = "lowercase")]
254pub enum AutoGenKind {
255 Autoincrement,
257 Identity,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
263pub struct ColumnDef {
264 pub name: String,
266 pub sql_type: String,
268 #[serde(default)]
270 pub not_null: bool,
271 #[serde(default)]
273 pub primary_key: bool,
274 #[serde(default)]
276 pub unique: bool,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub default: Option<String>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub auto_gen: Option<AutoGenKind>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub enum_name: Option<String>,
286}
287
288impl ColumnDef {
289 #[must_use]
291 pub const fn is_autoincrement(&self) -> bool {
292 matches!(self.auto_gen, Some(AutoGenKind::Autoincrement))
293 }
294
295 #[must_use]
297 pub const fn is_identity(&self) -> bool {
298 matches!(self.auto_gen, Some(AutoGenKind::Identity))
299 }
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
304pub struct IndexDef {
305 pub name: String,
307 pub table: String,
309 pub columns: Vec<String>,
311 #[serde(default)]
313 pub unique: bool,
314 #[serde(default)]
316 pub pg_schema: String,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
321pub struct ForeignKeyDef {
322 pub name: String,
324 pub table: String,
326 pub columns: Vec<String>,
328 pub table_to: String,
330 pub columns_to: Vec<String>,
332 #[serde(default = "default_fk_action")]
334 pub on_delete: String,
335 #[serde(default = "default_fk_action")]
337 pub on_update: String,
338 #[serde(default)]
340 pub pg_schema: String,
341 #[serde(default)]
343 pub pg_schema_to: String,
344}
345
346#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
348pub enum FieldCasing {
349 #[serde(rename = "snake_case")]
351 Snake,
352 #[serde(rename = "camelCase")]
354 Camel,
355}
356
357fn load_json(from: Option<&std::path::Path>) -> Result<SchemaDefinition, CliError> {
360 let content = if let Some(path) = from {
361 std::fs::read_to_string(path)
362 .map_err(|e| CliError::IoError(format!("Failed to read {}: {e}", path.display())))?
363 } else {
364 use std::io::Read;
365 let mut buf = String::new();
366 std::io::stdin()
367 .read_to_string(&mut buf)
368 .map_err(|e| CliError::IoError(format!("Failed to read stdin: {e}")))?;
369 buf
370 };
371 serde_json::from_str(&content)
372 .map_err(|e| CliError::Other(format!("Invalid JSON schema definition: {e}")))
373}
374
375fn export_to_json(def: &SchemaDefinition, path: &std::path::Path) -> Result<(), CliError> {
376 let json = serde_json::to_string_pretty(def)
377 .map_err(|e| CliError::Other(format!("Failed to serialize schema: {e}")))?;
378 if let Some(parent) = path.parent() {
379 std::fs::create_dir_all(parent)
380 .map_err(|e| CliError::IoError(format!("Failed to create directory: {e}")))?;
381 }
382 std::fs::write(path, json)
383 .map_err(|e| CliError::IoError(format!("Failed to write JSON: {e}")))?;
384 Ok(())
385}
386
387fn print_json_schema() {
388 let schema = schemars::schema_for!(SchemaDefinition);
389 let json = serde_json::to_string_pretty(&schema).expect("schema serialization cannot fail");
390 println!("{json}");
391 println!();
392 println!(
393 "Valid on_delete/on_update actions: \"No Action\", \"Cascade\", \"Set Null\", \"Set Default\", \"Restrict\""
394 );
395 println!();
396 println!("Tip: Run `drizzle new --export-json schema.json` to export an interactive");
397 println!(
398 "session as valid JSON, then edit and replay with `drizzle new --json --from schema.json`."
399 );
400}
401
402const VALID_FK_ACTIONS: &[&str] = &[
405 "No Action",
406 "Cascade",
407 "Set Null",
408 "Set Default",
409 "Restrict",
410];
411
412fn validate_schema(def: &SchemaDefinition) -> Result<(), CliError> {
413 if def.tables.is_empty() {
415 return Err(CliError::Other(
416 "Schema must have at least one table".into(),
417 ));
418 }
419
420 let mut table_names = HashSet::new();
422 for table in &def.tables {
423 if !is_valid_identifier(&table.name) {
424 return Err(CliError::Other(format!(
425 "Invalid table name: '{}'",
426 table.name
427 )));
428 }
429 if !table_names.insert(&table.name) {
430 return Err(CliError::Other(format!(
431 "Duplicate table name: '{}'",
432 table.name
433 )));
434 }
435 validate_table(table, def.dialect)?;
436 }
437
438 let enum_names = validate_enums(def)?;
439
440 for table in &def.tables {
442 for col in &table.columns {
443 if let Some(ref en) = col.enum_name
444 && !enum_names.contains(en.as_str())
445 {
446 return Err(CliError::Other(format!(
447 "Column '{}.{}' references unknown enum '{}'",
448 table.name, col.name, en
449 )));
450 }
451 }
452 }
453
454 validate_indexes(def)?;
455 validate_foreign_keys(def)?;
456
457 Ok(())
458}
459
460fn validate_table(table: &TableDef, dialect: Dialect) -> Result<(), CliError> {
463 if table.columns.is_empty() {
464 return Err(CliError::Other(format!(
465 "Table '{}' must have at least one column",
466 table.name
467 )));
468 }
469
470 let mut col_names = HashSet::new();
471 for col in &table.columns {
472 if !is_valid_identifier(&col.name) {
473 return Err(CliError::Other(format!(
474 "Invalid column name '{}' in table '{}'",
475 col.name, table.name
476 )));
477 }
478 if !col_names.insert(&col.name) {
479 return Err(CliError::Other(format!(
480 "Duplicate column name '{}' in table '{}'",
481 col.name, table.name
482 )));
483 }
484 }
485
486 match dialect {
487 Dialect::Sqlite | Dialect::Turso => {
488 for col in &table.columns {
489 if col.is_identity() {
490 return Err(CliError::Other(format!(
491 "Column '{}.{}': 'identity' is only supported for PostgreSQL",
492 table.name, col.name
493 )));
494 }
495 if col.enum_name.is_some() {
496 return Err(CliError::Other(format!(
497 "Column '{}.{}': 'enum_name' is only supported for PostgreSQL",
498 table.name, col.name
499 )));
500 }
501 }
502 }
503 Dialect::Postgresql => {
504 if table.strict {
505 return Err(CliError::Other(format!(
506 "Table '{}': 'strict' is only supported for SQLite",
507 table.name
508 )));
509 }
510 if table.without_rowid {
511 return Err(CliError::Other(format!(
512 "Table '{}': 'without_rowid' is only supported for SQLite",
513 table.name
514 )));
515 }
516 for col in &table.columns {
517 if col.is_autoincrement() {
518 return Err(CliError::Other(format!(
519 "Column '{}.{}': 'autoincrement' is only supported for SQLite (use 'identity' for PostgreSQL)",
520 table.name, col.name
521 )));
522 }
523 }
524 }
525 Dialect::Mysql => {
526 if table.strict || table.without_rowid {
527 return Err(CliError::Other(format!(
528 "Table '{}': SQLite table options are not supported for MySQL",
529 table.name
530 )));
531 }
532 for col in &table.columns {
533 if col.is_identity() {
534 return Err(CliError::Other(format!(
535 "Column '{}.{}': 'identity' is only supported for PostgreSQL (use 'autoincrement' for MySQL)",
536 table.name, col.name
537 )));
538 }
539 }
540 }
541 }
542
543 Ok(())
544}
545
546fn validate_enums(def: &SchemaDefinition) -> Result<HashSet<&str>, CliError> {
548 if matches!(def.dialect, Dialect::Sqlite | Dialect::Turso) && !def.enums.is_empty() {
549 return Err(CliError::Other(
550 "Enums are only supported for PostgreSQL and MySQL".into(),
551 ));
552 }
553 let mut enum_names = HashSet::new();
554 for e in &def.enums {
555 if !is_valid_identifier(&e.name) {
556 return Err(CliError::Other(format!("Invalid enum name: '{}'", e.name)));
557 }
558 if !enum_names.insert(e.name.as_str()) {
559 return Err(CliError::Other(format!(
560 "Duplicate enum name: '{}'",
561 e.name
562 )));
563 }
564 if e.variants.is_empty() {
565 return Err(CliError::Other(format!(
566 "Enum '{}' must have at least one variant",
567 e.name
568 )));
569 }
570 }
571 Ok(enum_names)
572}
573
574fn validate_indexes(def: &SchemaDefinition) -> Result<(), CliError> {
576 for idx in &def.indexes {
577 let table = def.tables.iter().find(|t| t.name == idx.table);
578 let Some(table) = table else {
579 return Err(CliError::Other(format!(
580 "Index '{}' references unknown table '{}'",
581 idx.name, idx.table
582 )));
583 };
584 for col_name in &idx.columns {
585 if !table.columns.iter().any(|c| &c.name == col_name) {
586 return Err(CliError::Other(format!(
587 "Index '{}' references unknown column '{}.{}'",
588 idx.name, idx.table, col_name
589 )));
590 }
591 }
592 }
593 Ok(())
594}
595
596fn validate_foreign_keys(def: &SchemaDefinition) -> Result<(), CliError> {
599 for fk in &def.foreign_keys {
600 let src = def.tables.iter().find(|t| t.name == fk.table);
602 let Some(src) = src else {
603 return Err(CliError::Other(format!(
604 "Foreign key '{}' references unknown source table '{}'",
605 fk.name, fk.table
606 )));
607 };
608 for col_name in &fk.columns {
609 if !src.columns.iter().any(|c| &c.name == col_name) {
610 return Err(CliError::Other(format!(
611 "Foreign key '{}' references unknown source column '{}.{}'",
612 fk.name, fk.table, col_name
613 )));
614 }
615 }
616
617 let tgt = def.tables.iter().find(|t| t.name == fk.table_to);
619 let Some(tgt) = tgt else {
620 return Err(CliError::Other(format!(
621 "Foreign key '{}' references unknown target table '{}'",
622 fk.name, fk.table_to
623 )));
624 };
625 for col_name in &fk.columns_to {
626 if !tgt.columns.iter().any(|c| &c.name == col_name) {
627 return Err(CliError::Other(format!(
628 "Foreign key '{}' references unknown target column '{}.{}'",
629 fk.name, fk.table_to, col_name
630 )));
631 }
632 }
633
634 if !VALID_FK_ACTIONS.contains(&fk.on_delete.as_str()) {
636 return Err(CliError::Other(format!(
637 "Foreign key '{}': invalid on_delete action '{}'. Valid: {}",
638 fk.name,
639 fk.on_delete,
640 VALID_FK_ACTIONS.join(", ")
641 )));
642 }
643 if !VALID_FK_ACTIONS.contains(&fk.on_update.as_str()) {
644 return Err(CliError::Other(format!(
645 "Foreign key '{}': invalid on_update action '{}'. Valid: {}",
646 fk.name,
647 fk.on_update,
648 VALID_FK_ACTIONS.join(", ")
649 )));
650 }
651 if def.dialect == Dialect::Mysql
652 && (fk.on_delete == "Set Default" || fk.on_update == "Set Default")
653 {
654 return Err(CliError::Other(format!(
655 "Foreign key '{}': MySQL does not support SET DEFAULT",
656 fk.name
657 )));
658 }
659 if fk.columns.len() != fk.columns_to.len() {
660 return Err(CliError::Other(format!(
661 "Foreign key '{}': source and target column counts must match",
662 fk.name
663 )));
664 }
665 }
666 Ok(())
667}
668
669fn collect_interactively(
672 config: Option<&Config>,
673 options: &NewOptions,
674) -> Result<SchemaDefinition, CliError> {
675 let dialect = resolve_dialect(config, options.dialect)?;
677 let casing = prompt_casing()?;
678 let output_path = resolve_output_path(config, options.schema.clone())?;
679 let schema_name = prompt_schema_name()?;
680
681 let enums: Vec<EnumDef> = if matches!(dialect, Dialect::Postgresql | Dialect::Mysql) {
683 prompt_enums()?
684 } else {
685 Vec::new()
686 };
687
688 let mut tables: Vec<TableDef> = Vec::new();
690 loop {
691 let table = prompt_table(dialect, &enums)?;
692 tables.push(table);
693 if !confirm("Add another table?", false)? {
694 break;
695 }
696 }
697
698 let indexes: Vec<IndexDef> = if confirm("Add indexes?", false)? {
700 prompt_indexes(&tables)?
701 } else {
702 Vec::new()
703 };
704
705 let foreign_keys: Vec<ForeignKeyDef> =
707 if tables.len() > 1 && confirm("Add foreign keys?", false)? {
708 prompt_foreign_keys(&tables, dialect)?
709 } else {
710 Vec::new()
711 };
712
713 Ok(SchemaDefinition {
714 dialect,
715 casing,
716 schema_name,
717 output_path,
718 enums,
719 tables,
720 indexes,
721 foreign_keys,
722 })
723}
724
725fn resolve_dialect(
728 config: Option<&Config>,
729 cli_dialect: Option<Dialect>,
730) -> Result<Dialect, CliError> {
731 if let Some(d) = cli_dialect {
732 return Ok(d);
733 }
734 if let Some(c) = config {
735 return Ok(c.dialect());
736 }
737 let options = vec!["SQLite", "PostgreSQL", "MySQL"];
738 let answer = Select::new("Select database dialect:", options)
739 .prompt()
740 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
741 match answer {
742 "SQLite" => Ok(Dialect::Sqlite),
743 "PostgreSQL" => Ok(Dialect::Postgresql),
744 "MySQL" => Ok(Dialect::Mysql),
745 _ => unreachable!(),
746 }
747}
748
749fn prompt_casing() -> Result<FieldCasing, CliError> {
750 let options = vec!["snake_case (default)", "camelCase"];
751 let answer = Select::new("Select field casing:", options)
752 .prompt()
753 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
754 match answer {
755 s if s.starts_with("snake") => Ok(FieldCasing::Snake),
756 s if s.starts_with("camel") => Ok(FieldCasing::Camel),
757 _ => Ok(FieldCasing::Snake),
758 }
759}
760
761fn resolve_output_path(
762 config: Option<&Config>,
763 cli_schema: Option<String>,
764) -> Result<String, CliError> {
765 if let Some(s) = cli_schema {
766 return Ok(s);
767 }
768 let default = config.map_or_else(
769 || "src/schema.rs".to_string(),
770 super::super::config::Config::schema_display,
771 );
772 Text::new("Schema output path:")
773 .with_default(&default)
774 .prompt()
775 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))
776}
777
778fn prompt_schema_name() -> Result<String, CliError> {
779 Text::new("Schema struct name:")
780 .with_default("AppSchema")
781 .prompt()
782 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))
783}
784
785fn prompt_enums() -> Result<Vec<EnumDef>, CliError> {
788 let mut enums = Vec::new();
789 if !confirm("Define any enums?", false)? {
790 return Ok(enums);
791 }
792 loop {
793 let name = Text::new("Enum name:")
794 .with_validator(|s: &str| {
795 if s.is_empty() {
796 Ok(Validation::Invalid("Name cannot be empty".into()))
797 } else if !is_valid_identifier(s) {
798 Ok(Validation::Invalid(
799 "Must be a valid Rust identifier".into(),
800 ))
801 } else {
802 Ok(Validation::Valid)
803 }
804 })
805 .prompt()
806 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
807
808 let mut variants = Vec::new();
809 loop {
810 let variant = Text::new(" Enum variant (empty to finish):")
811 .prompt()
812 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
813 if variant.is_empty() {
814 break;
815 }
816 variants.push(variant);
817 }
818 if variants.is_empty() {
819 println!(" Skipping enum with no variants.");
820 } else {
821 enums.push(EnumDef { name, variants });
822 }
823 if !confirm("Add another enum?", false)? {
824 break;
825 }
826 }
827 Ok(enums)
828}
829
830fn prompt_table(dialect: Dialect, enums: &[EnumDef]) -> Result<TableDef, CliError> {
833 let name = Text::new("Table name:")
834 .with_validator(|s: &str| {
835 if s.is_empty() {
836 Ok(Validation::Invalid("Name cannot be empty".into()))
837 } else if !is_valid_identifier(s) {
838 Ok(Validation::Invalid(
839 "Must be a valid Rust identifier (letters, digits, underscores)".into(),
840 ))
841 } else {
842 Ok(Validation::Valid)
843 }
844 })
845 .prompt()
846 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
847
848 let mut strict = false;
849 let mut without_rowid = false;
850 let mut pg_schema = "public".to_string();
851
852 match dialect {
853 Dialect::Sqlite | Dialect::Turso => {
854 let table_opts = vec!["strict", "without_rowid"];
855 let selected = MultiSelect::new("Table options (space to toggle):", table_opts)
856 .prompt()
857 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
858 strict = selected.contains(&"strict");
859 without_rowid = selected.contains(&"without_rowid");
860 }
861 Dialect::Postgresql => {
862 pg_schema = Text::new("PostgreSQL schema:")
863 .with_default("public")
864 .prompt()
865 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
866 }
867 Dialect::Mysql => {}
868 }
869
870 let mut columns = Vec::new();
872 println!();
873 println!(" Define columns for '{name}':");
874 loop {
875 let col = prompt_column(dialect, enums)?;
876 columns.push(col);
877 if !confirm(" Add another column?", true)? {
878 break;
879 }
880 }
881
882 Ok(TableDef {
883 name,
884 columns,
885 strict,
886 without_rowid,
887 pg_schema,
888 })
889}
890
891fn prompt_column(dialect: Dialect, enums: &[EnumDef]) -> Result<ColumnDef, CliError> {
892 let col_name = Text::new(" Column name:")
893 .with_validator(|s: &str| {
894 if s.is_empty() {
895 Ok(Validation::Invalid("Name cannot be empty".into()))
896 } else if !is_valid_identifier(s) {
897 Ok(Validation::Invalid("Must be a valid identifier".into()))
898 } else {
899 Ok(Validation::Valid)
900 }
901 })
902 .prompt()
903 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
904
905 let (sql_type, enum_name) = prompt_type(dialect, enums)?;
906
907 let nullable = confirm(" Nullable (Option<T>)?", false)?;
908
909 let constraint_opts = match dialect {
910 Dialect::Sqlite | Dialect::Turso => {
911 vec!["Primary Key", "Autoincrement", "Unique", "Default value"]
912 }
913 Dialect::Postgresql => {
914 vec![
915 "Primary Key",
916 "Identity (auto-increment)",
917 "Unique",
918 "Default value",
919 ]
920 }
921 Dialect::Mysql => {
922 vec!["Primary Key", "Autoincrement", "Unique", "Default value"]
923 }
924 };
925 let selected = MultiSelect::new(" Column constraints (space to toggle):", constraint_opts)
926 .prompt()
927 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
928
929 let primary_key = selected.iter().any(|s| s.starts_with("Primary"));
930 let autoincrement = selected.iter().any(|s| s.starts_with("Autoincrement"));
931 let identity = selected.iter().any(|s| s.starts_with("Identity"));
932 let unique = selected.iter().any(|s| s.starts_with("Unique"));
933 let has_default = selected.iter().any(|s| s.starts_with("Default"));
934
935 let default = if has_default {
936 let val = Text::new(" Default value:")
937 .prompt()
938 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
939 Some(val)
940 } else {
941 None
942 };
943
944 let auto_gen = if autoincrement {
945 Some(AutoGenKind::Autoincrement)
946 } else if identity {
947 Some(AutoGenKind::Identity)
948 } else {
949 None
950 };
951
952 Ok(ColumnDef {
953 name: col_name,
954 sql_type,
955 not_null: !nullable,
956 primary_key,
957 unique,
958 default,
959 auto_gen,
960 enum_name,
961 })
962}
963
964fn prompt_type(dialect: Dialect, enums: &[EnumDef]) -> Result<(String, Option<String>), CliError> {
965 let mut options: Vec<String> = match dialect {
966 Dialect::Sqlite | Dialect::Turso => {
967 vec![
968 "i32".into(),
969 "i64".into(),
970 "f64".into(),
971 "String".into(),
972 "bool".into(),
973 "Vec<u8>".into(),
974 ]
975 }
976 Dialect::Postgresql => {
977 vec![
978 "i16".into(),
979 "i32".into(),
980 "i64".into(),
981 "f32".into(),
982 "f64".into(),
983 "String".into(),
984 "bool".into(),
985 "Vec<u8>".into(),
986 "uuid::Uuid".into(),
987 "chrono::NaiveDate".into(),
988 "chrono::NaiveDateTime".into(),
989 "chrono::DateTime<chrono::Utc>".into(),
990 "serde_json::Value".into(),
991 ]
992 }
993 Dialect::Mysql => {
994 vec![
995 "i8".into(),
996 "i16".into(),
997 "i32".into(),
998 "i64".into(),
999 "u8".into(),
1000 "u16".into(),
1001 "u32".into(),
1002 "u64".into(),
1003 "f32".into(),
1004 "f64".into(),
1005 "String".into(),
1006 "bool".into(),
1007 "Vec<u8>".into(),
1008 "chrono::NaiveDate".into(),
1009 "chrono::NaiveDateTime".into(),
1010 "serde_json::Value".into(),
1011 ]
1012 }
1013 };
1014
1015 for e in enums {
1017 options.push(format!("enum:{}", e.name));
1018 }
1019
1020 let refs: Vec<&str> = options.iter().map(std::string::String::as_str).collect();
1021 let chosen = Select::new(" Rust type:", refs)
1022 .prompt()
1023 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1024
1025 if let Some(enum_name) = chosen.strip_prefix("enum:") {
1027 return Ok((enum_name.to_string(), Some(enum_name.to_string())));
1029 }
1030
1031 let sql_type = match dialect {
1032 Dialect::Sqlite | Dialect::Turso => match chosen {
1033 "i32" | "i64" => "integer",
1034 "f64" => "real",
1035 "bool" => "boolean",
1036 "Vec<u8>" => "blob",
1037 _ => "text",
1038 },
1039 Dialect::Postgresql => match chosen {
1040 "i16" => "int2",
1041 "i32" => "int4",
1042 "i64" => "int8",
1043 "f32" => "float4",
1044 "f64" => "float8",
1045 "bool" => "bool",
1046 "Vec<u8>" => "bytea",
1047 "uuid::Uuid" => "uuid",
1048 "chrono::NaiveDate" => "date",
1049 "chrono::NaiveDateTime" => "timestamp",
1050 "chrono::DateTime<chrono::Utc>" => "timestamptz",
1051 "serde_json::Value" => "jsonb",
1052 _ => "text",
1053 },
1054 Dialect::Mysql => match chosen {
1055 "i8" => "tinyint",
1056 "u8" => "tinyint unsigned",
1057 "i16" => "smallint",
1058 "u16" => "smallint unsigned",
1059 "i32" => "int",
1060 "u32" => "int unsigned",
1061 "i64" => "bigint",
1062 "u64" => "bigint unsigned",
1063 "f32" => "float",
1064 "f64" => "double",
1065 "bool" => "boolean",
1066 "Vec<u8>" => "blob",
1067 "chrono::NaiveDate" => "date",
1068 "chrono::NaiveDateTime" => "datetime",
1069 "serde_json::Value" => "json",
1070 _ => "text",
1071 },
1072 };
1073
1074 Ok((sql_type.to_string(), None))
1075}
1076
1077fn prompt_indexes(tables: &[TableDef]) -> Result<Vec<IndexDef>, CliError> {
1080 let mut indexes = Vec::new();
1081 loop {
1082 let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect();
1083 let table_name = Select::new("Index on which table?", table_names)
1084 .prompt()
1085 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1086
1087 let table = tables.iter().find(|t| t.name == table_name).unwrap();
1088 let col_names: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
1089
1090 if col_names.is_empty() {
1091 println!(" Table has no columns, skipping.");
1092 if !confirm("Add another index?", false)? {
1093 break;
1094 }
1095 continue;
1096 }
1097
1098 let selected_cols = MultiSelect::new("Select columns for index:", col_names)
1099 .prompt()
1100 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1101
1102 if selected_cols.is_empty() {
1103 println!(" No columns selected, skipping.");
1104 if !confirm("Add another index?", false)? {
1105 break;
1106 }
1107 continue;
1108 }
1109
1110 let is_unique = confirm(" Unique index?", false)?;
1111
1112 let suggested_name = format!("{}_{}_idx", table_name, selected_cols.join("_"));
1113 let idx_name = Text::new(" Index name:")
1114 .with_default(&suggested_name)
1115 .prompt()
1116 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1117
1118 indexes.push(IndexDef {
1119 name: idx_name,
1120 table: table_name.to_string(),
1121 columns: selected_cols
1122 .into_iter()
1123 .map(std::string::ToString::to_string)
1124 .collect(),
1125 unique: is_unique,
1126 pg_schema: table.pg_schema.clone(),
1127 });
1128
1129 if !confirm("Add another index?", false)? {
1130 break;
1131 }
1132 }
1133 Ok(indexes)
1134}
1135
1136fn prompt_foreign_keys(
1139 tables: &[TableDef],
1140 dialect: Dialect,
1141) -> Result<Vec<ForeignKeyDef>, CliError> {
1142 let mut fks = Vec::new();
1143 let action_options = vec![
1144 "No Action",
1145 "Cascade",
1146 "Set Null",
1147 "Set Default",
1148 "Restrict",
1149 ];
1150 loop {
1151 let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect();
1152
1153 let src_table_name = Select::new("Source table:", table_names.clone())
1154 .prompt()
1155 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1156 let src_table = tables.iter().find(|t| t.name == src_table_name).unwrap();
1157 let src_col_names: Vec<&str> = src_table.columns.iter().map(|c| c.name.as_str()).collect();
1158
1159 let src_cols = MultiSelect::new("Source column(s):", src_col_names)
1160 .prompt()
1161 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1162
1163 let tgt_table_name = Select::new("Target (referenced) table:", table_names)
1164 .prompt()
1165 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1166 let tgt_table = tables.iter().find(|t| t.name == tgt_table_name).unwrap();
1167 let tgt_col_names: Vec<&str> = tgt_table.columns.iter().map(|c| c.name.as_str()).collect();
1168
1169 let tgt_cols = MultiSelect::new("Target column(s):", tgt_col_names)
1170 .prompt()
1171 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1172
1173 let on_delete = Select::new("ON DELETE action:", action_options.clone())
1174 .prompt()
1175 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1176
1177 let on_update = Select::new("ON UPDATE action:", action_options.clone())
1178 .prompt()
1179 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))?;
1180
1181 let fk_name = format!("{}_{}_fk", src_table_name, src_cols.join("_"));
1182
1183 let pg_schema_to = match dialect {
1184 Dialect::Postgresql => tgt_table.pg_schema.clone(),
1185 _ => String::new(),
1186 };
1187
1188 fks.push(ForeignKeyDef {
1189 name: fk_name,
1190 table: src_table_name.to_string(),
1191 columns: src_cols
1192 .into_iter()
1193 .map(std::string::ToString::to_string)
1194 .collect(),
1195 table_to: tgt_table_name.to_string(),
1196 columns_to: tgt_cols
1197 .into_iter()
1198 .map(std::string::ToString::to_string)
1199 .collect(),
1200 on_delete: on_delete.to_string(),
1201 on_update: on_update.to_string(),
1202 pg_schema: src_table.pg_schema.clone(),
1203 pg_schema_to,
1204 });
1205
1206 if !confirm("Add another foreign key?", false)? {
1207 break;
1208 }
1209 }
1210 Ok(fks)
1211}
1212
1213fn generate_sqlite(
1216 tables: &[TableDef],
1217 indexes: &[IndexDef],
1218 fks: &[ForeignKeyDef],
1219 schema_name: &str,
1220 casing: FieldCasing,
1221) -> String {
1222 use drizzle_migrations::sqlite::codegen;
1223 use drizzle_migrations::sqlite::collection::SQLiteDDL;
1224 use drizzle_types::sqlite::ddl::{
1225 Column, ForeignKey, Index, IndexColumn, PrimaryKey, Table, UniqueConstraint,
1226 };
1227
1228 let mut ddl = SQLiteDDL::new();
1229
1230 for (table_idx, table) in tables.iter().enumerate() {
1231 let mut t = Table::new(table.name.clone());
1232 if table.strict {
1233 t = t.strict();
1234 }
1235 if table.without_rowid {
1236 t = t.without_rowid();
1237 }
1238 ddl.tables.push(t);
1239
1240 let mut pk_cols: Vec<String> = Vec::new();
1241 let mut unique_cols: Vec<String> = Vec::new();
1242
1243 for (col_idx, col) in table.columns.iter().enumerate() {
1244 let mut column =
1245 Column::new(table.name.clone(), col.name.clone(), col.sql_type.clone());
1246 if col.not_null {
1247 column = column.not_null();
1248 }
1249 if col.is_autoincrement() {
1250 column = column.autoincrement();
1251 }
1252 if let Some(ref default) = col.default {
1253 column = column.default_value(default.clone());
1254 }
1255 column.ordinal_position = Some(
1257 i32::try_from(col_idx)
1258 .ok()
1259 .and_then(|i| i.checked_add(1))
1260 .unwrap_or(i32::MAX),
1261 );
1262 ddl.columns.push(column);
1263
1264 if col.primary_key {
1265 pk_cols.push(col.name.clone());
1266 }
1267 if col.unique {
1268 unique_cols.push(col.name.clone());
1269 }
1270 }
1271
1272 if !pk_cols.is_empty() {
1273 ddl.pks.push(PrimaryKey::from_strings(
1274 table.name.clone(),
1275 format!("{}_pk", table.name),
1276 pk_cols,
1277 ));
1278 }
1279 for uc in unique_cols {
1280 ddl.uniques.push(UniqueConstraint::from_strings(
1281 table.name.clone(),
1282 format!("{}_{}_unique", table.name, uc),
1283 vec![uc],
1284 ));
1285 }
1286
1287 let _ = table_idx;
1289 }
1290
1291 for idx in indexes {
1293 let columns: Vec<IndexColumn> = idx
1294 .columns
1295 .iter()
1296 .map(|c| IndexColumn::new(c.clone()))
1297 .collect();
1298 let mut index = Index::new(idx.table.clone(), idx.name.clone(), columns);
1299 if idx.unique {
1300 index = index.unique();
1301 }
1302 ddl.indexes.push(index);
1303 }
1304
1305 for fk in fks {
1307 let mut foreign_key = ForeignKey::from_strings(
1308 fk.table.clone(),
1309 fk.name.clone(),
1310 fk.columns.clone(),
1311 fk.table_to.clone(),
1312 fk.columns_to.clone(),
1313 );
1314 if fk.on_delete != "No Action" {
1315 foreign_key = foreign_key.on_delete(fk.on_delete.to_uppercase());
1316 }
1317 if fk.on_update != "No Action" {
1318 foreign_key = foreign_key.on_update(fk.on_update.to_uppercase());
1319 }
1320 ddl.fks.push(foreign_key);
1321 }
1322
1323 let field_casing = match casing {
1324 FieldCasing::Snake => codegen::FieldCasing::Snake,
1325 FieldCasing::Camel => codegen::FieldCasing::Camel,
1326 };
1327
1328 let options = codegen::CodegenOptions {
1329 module_doc: Some("Generated by `drizzle new`".to_string()),
1330 include_schema: true,
1331 schema_name: schema_name.to_string(),
1332 use_pub: true,
1333 field_casing,
1334 };
1335
1336 codegen::generate_rust_schema(&ddl, &options).code
1337}
1338
1339fn generate_mysql(
1340 tables: &[TableDef],
1341 indexes: &[IndexDef],
1342 fks: &[ForeignKeyDef],
1343 enums: &[EnumDef],
1344 schema_name: &str,
1345 casing: FieldCasing,
1346) -> Result<String, CliError> {
1347 use drizzle_migrations::mysql::codegen;
1348 use drizzle_migrations::mysql::collection::MySQLDDL;
1349 use drizzle_migrations::mysql::ddl::{
1350 Column, ForeignKey, Index, IndexColumn, InlineEnum, InlineType, PrimaryKey, Table,
1351 UniqueConstraint,
1352 };
1353
1354 let enum_values = enums
1355 .iter()
1356 .map(|definition| (definition.name.as_str(), definition.variants.as_slice()))
1357 .collect::<std::collections::HashMap<_, _>>();
1358 let mut ddl = MySQLDDL::new();
1359
1360 for table in tables {
1361 ddl.tables.push(Table::new(table.name.clone()));
1362 let mut primary_columns = Vec::new();
1363
1364 for column_definition in &table.columns {
1365 let mut column = Column::new(
1366 table.name.clone(),
1367 column_definition.name.clone(),
1368 column_definition.sql_type.clone(),
1369 );
1370 column.not_null = column_definition.not_null;
1371 column.autoincrement = column_definition.is_autoincrement();
1372 column.default = column_definition
1373 .default
1374 .as_ref()
1375 .map(|value| Cow::Owned(value.clone()));
1376 if let Some(enum_name) = column_definition.enum_name.as_deref()
1377 && let Some(values) = enum_values.get(enum_name)
1378 {
1379 column.inline_type =
1380 Some(InlineType::Enum(InlineEnum::new(values.iter().cloned())));
1381 }
1382 ddl.columns.push(column);
1383
1384 if column_definition.primary_key {
1385 primary_columns.push(column_definition.name.clone());
1386 }
1387 if column_definition.unique {
1388 ddl.uniques.push(UniqueConstraint::new(
1389 table.name.clone(),
1390 format!("{}_{}_unique", table.name, column_definition.name),
1391 [column_definition.name.clone()],
1392 ));
1393 }
1394 }
1395
1396 if !primary_columns.is_empty() {
1397 ddl.pks
1398 .push(PrimaryKey::new(table.name.clone(), primary_columns));
1399 }
1400 }
1401
1402 for definition in indexes {
1403 let columns = definition
1404 .columns
1405 .iter()
1406 .cloned()
1407 .map(IndexColumn::column)
1408 .collect();
1409 let mut index = Index::new(definition.table.clone(), definition.name.clone(), columns);
1410 index.unique = definition.unique;
1411 ddl.indexes.push(index);
1412 }
1413
1414 for definition in fks {
1415 let mut foreign_key = ForeignKey::new(
1416 definition.table.clone(),
1417 definition.name.clone(),
1418 definition.columns.clone(),
1419 definition.table_to.clone(),
1420 definition.columns_to.clone(),
1421 );
1422 foreign_key.on_delete = mysql_referential_action(&definition.on_delete);
1423 foreign_key.on_update = mysql_referential_action(&definition.on_update);
1424 ddl.fks.push(foreign_key);
1425 }
1426
1427 let options = codegen::CodegenOptions {
1428 module_doc: Some("Generated by `drizzle new`".to_string()),
1429 include_schema: true,
1430 schema_name: schema_name.to_string(),
1431 use_pub: true,
1432 field_casing: match casing {
1433 FieldCasing::Snake => codegen::FieldCasing::Snake,
1434 FieldCasing::Camel => codegen::FieldCasing::Camel,
1435 },
1436 };
1437 codegen::generate_rust_schema(&ddl, &options)
1438 .map(|generated| generated.code)
1439 .map_err(|error| CliError::Other(format!("Cannot generate MySQL schema: {error}")))
1440}
1441
1442fn mysql_referential_action(
1443 action: &str,
1444) -> Option<drizzle_migrations::mysql::ddl::ReferentialAction> {
1445 use drizzle_migrations::mysql::ddl::ReferentialAction;
1446
1447 match action {
1448 "Cascade" => Some(ReferentialAction::Cascade),
1449 "Set Null" => Some(ReferentialAction::SetNull),
1450 "Restrict" => Some(ReferentialAction::Restrict),
1451 "No Action" => Some(ReferentialAction::NoAction),
1452 _ => None,
1455 }
1456}
1457
1458fn generate_postgres(
1459 tables: &[TableDef],
1460 indexes: &[IndexDef],
1461 fks: &[ForeignKeyDef],
1462 enums: &[EnumDef],
1463 schema_name: &str,
1464 casing: FieldCasing,
1465) -> String {
1466 use drizzle_migrations::postgres::codegen;
1467 use drizzle_migrations::postgres::collection::PostgresDDL;
1468 use drizzle_types::postgres::ddl::{Enum, Table};
1469
1470 let mut ddl = PostgresDDL::new();
1471
1472 for e in enums {
1474 let values: Vec<Cow<'static, str>> =
1475 e.variants.iter().map(|v| Cow::Owned(v.clone())).collect();
1476 ddl.enums.push(Enum::new(
1477 "public",
1478 Cow::<str>::Owned(e.name.clone()),
1479 Cow::<[Cow<'static, str>]>::Owned(values),
1480 ));
1481 }
1482
1483 for table in tables {
1485 ddl.tables
1486 .push(Table::new(table.pg_schema.clone(), table.name.clone()));
1487 add_postgres_table_columns(&mut ddl, table);
1488 }
1489
1490 add_postgres_indexes(&mut ddl, indexes);
1491 add_postgres_foreign_keys(&mut ddl, fks);
1492
1493 let field_casing = match casing {
1494 FieldCasing::Snake => codegen::FieldCasing::Snake,
1495 FieldCasing::Camel => codegen::FieldCasing::Camel,
1496 };
1497
1498 let options = codegen::CodegenOptions {
1499 module_doc: Some("Generated by `drizzle new`".to_string()),
1500 include_schema: true,
1501 schema_name: schema_name.to_string(),
1502 use_pub: true,
1503 field_casing,
1504 };
1505
1506 codegen::generate_rust_schema(&ddl, &options).code
1507}
1508
1509fn add_postgres_table_columns(
1511 ddl: &mut drizzle_migrations::postgres::collection::PostgresDDL,
1512 table: &TableDef,
1513) {
1514 use drizzle_types::postgres::ddl::{Column, PrimaryKey, UniqueConstraint};
1515
1516 let mut pk_cols: Vec<String> = Vec::new();
1517 let mut unique_cols: Vec<String> = Vec::new();
1518
1519 for (col_idx, col) in table.columns.iter().enumerate() {
1520 let mut column = Column::new(
1521 table.pg_schema.clone(),
1522 table.name.clone(),
1523 col.name.clone(),
1524 col.sql_type.clone(),
1525 );
1526 if col.not_null {
1527 column = column.not_null();
1528 }
1529 if let Some(ref default) = col.default {
1530 column = column.default_value(default.clone());
1531 }
1532 if col.is_identity() {
1533 use drizzle_types::postgres::ddl::Identity;
1534 let seq_name = format!("{}_{}_seq", table.name, col.name);
1535 column.identity = Some(Identity::always(seq_name));
1536 }
1537 if col.enum_name.is_some() {
1538 column.type_schema = Some(Cow::Owned(table.pg_schema.clone()));
1540 }
1541 column.ordinal_position = Some(
1542 i32::try_from(col_idx)
1543 .ok()
1544 .and_then(|i| i.checked_add(1))
1545 .unwrap_or(i32::MAX),
1546 );
1547 ddl.columns.push(column);
1548
1549 if col.primary_key {
1550 pk_cols.push(col.name.clone());
1551 }
1552 if col.unique {
1553 unique_cols.push(col.name.clone());
1554 }
1555 }
1556
1557 if !pk_cols.is_empty() {
1558 ddl.pks.push(PrimaryKey::from_strings(
1559 table.pg_schema.clone(),
1560 table.name.clone(),
1561 format!("{}_pk", table.name),
1562 pk_cols,
1563 ));
1564 }
1565 for uc in unique_cols {
1566 ddl.uniques.push(UniqueConstraint::from_strings(
1567 table.pg_schema.clone(),
1568 table.name.clone(),
1569 format!("{}_{}_unique", table.name, uc),
1570 vec![uc],
1571 ));
1572 }
1573}
1574
1575fn add_postgres_indexes(
1577 ddl: &mut drizzle_migrations::postgres::collection::PostgresDDL,
1578 indexes: &[IndexDef],
1579) {
1580 use drizzle_types::postgres::ddl::{Index, IndexColumn};
1581 for idx in indexes {
1582 let columns: Vec<IndexColumn> = idx
1583 .columns
1584 .iter()
1585 .map(|c| IndexColumn::new(c.clone()))
1586 .collect();
1587 let mut index = Index::new(
1588 idx.pg_schema.clone(),
1589 idx.table.clone(),
1590 idx.name.clone(),
1591 columns,
1592 );
1593 if idx.unique {
1594 index = index.unique();
1595 }
1596 ddl.indexes.push(index);
1597 }
1598}
1599
1600fn add_postgres_foreign_keys(
1602 ddl: &mut drizzle_migrations::postgres::collection::PostgresDDL,
1603 fks: &[ForeignKeyDef],
1604) {
1605 use drizzle_types::postgres::ddl::ForeignKey;
1606 for fk in fks {
1607 let mut foreign_key = ForeignKey::from_strings(
1608 fk.pg_schema.clone(),
1609 fk.table.clone(),
1610 fk.name.clone(),
1611 fk.columns.clone(),
1612 fk.pg_schema_to.clone(),
1613 fk.table_to.clone(),
1614 fk.columns_to.clone(),
1615 );
1616 if fk.on_delete != "No Action" {
1617 foreign_key = foreign_key.on_delete(fk.on_delete.to_uppercase());
1618 }
1619 if fk.on_update != "No Action" {
1620 foreign_key = foreign_key.on_update(fk.on_update.to_uppercase());
1621 }
1622 ddl.fks.push(foreign_key);
1623 }
1624}
1625
1626fn confirm(message: &str, default: bool) -> Result<bool, CliError> {
1629 Confirm::new(message)
1630 .with_default(default)
1631 .prompt()
1632 .map_err(|e| CliError::Other(format!("Prompt cancelled: {e}")))
1633}
1634
1635fn is_valid_identifier(s: &str) -> bool {
1636 if s.is_empty() {
1637 return false;
1638 }
1639 let mut chars = s.chars();
1640 let first = chars.next().unwrap();
1641 if !first.is_ascii_alphabetic() && first != '_' {
1642 return false;
1643 }
1644 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
1645}
1646
1647#[cfg(test)]
1650mod tests {
1651 use super::*;
1652
1653 fn minimal_sqlite_def() -> SchemaDefinition {
1654 SchemaDefinition {
1655 dialect: Dialect::Sqlite,
1656 casing: FieldCasing::Snake,
1657 schema_name: "TestSchema".into(),
1658 output_path: "src/schema.rs".into(),
1659 enums: vec![],
1660 tables: vec![TableDef {
1661 name: "users".into(),
1662 columns: vec![ColumnDef {
1663 name: "id".into(),
1664 sql_type: "integer".into(),
1665 not_null: true,
1666 primary_key: true,
1667 unique: false,
1668 default: None,
1669 auto_gen: None,
1670 enum_name: None,
1671 }],
1672 strict: false,
1673 without_rowid: false,
1674 pg_schema: String::new(),
1675 }],
1676 indexes: vec![],
1677 foreign_keys: vec![],
1678 }
1679 }
1680
1681 fn minimal_mysql_def() -> SchemaDefinition {
1682 let mut definition = minimal_sqlite_def();
1683 definition.dialect = Dialect::Mysql;
1684 definition.enums = vec![EnumDef {
1685 name: "status".into(),
1686 variants: vec!["active".into(), "disabled".into()],
1687 }];
1688 definition.tables[0].columns[0].sql_type = "bigint unsigned".into();
1689 definition.tables[0].columns[0].auto_gen = Some(AutoGenKind::Autoincrement);
1690 definition.tables[0].columns.push(ColumnDef {
1691 name: "status".into(),
1692 sql_type: "status".into(),
1693 not_null: true,
1694 primary_key: false,
1695 unique: true,
1696 default: Some("'active'".into()),
1697 auto_gen: None,
1698 enum_name: Some("status".into()),
1699 });
1700 definition
1701 }
1702
1703 #[test]
1704 fn validate_minimal_schema() {
1705 let def = minimal_sqlite_def();
1706 assert!(validate_schema(&def).is_ok());
1707 }
1708
1709 #[test]
1710 fn mysql_generation_uses_mysql_macros_and_dialect_features() {
1711 let definition = minimal_mysql_def();
1712 validate_schema(&definition).expect("valid MySQL definition");
1713 let code = generate_mysql(
1714 &definition.tables,
1715 &definition.indexes,
1716 &definition.foreign_keys,
1717 &definition.enums,
1718 &definition.schema_name,
1719 definition.casing,
1720 )
1721 .expect("valid MySQL definition");
1722
1723 assert!(code.contains("MySQLTable"), "{code}");
1724 assert!(code.contains("MySQLEnum"), "{code}");
1725 assert!(code.contains("AUTO_INCREMENT"), "{code}");
1726 assert!(code.contains("UNIQUE"), "{code}");
1727 assert!(!code.contains("SQLiteTable"), "{code}");
1728 assert!(!code.contains("PostgresTable"), "{code}");
1729 }
1730
1731 #[test]
1732 fn mysql_rejects_set_default_foreign_keys() {
1733 let mut definition = minimal_mysql_def();
1734 definition.tables.push(TableDef {
1735 name: "posts".into(),
1736 columns: vec![ColumnDef {
1737 name: "user_id".into(),
1738 sql_type: "bigint unsigned".into(),
1739 not_null: true,
1740 primary_key: false,
1741 unique: false,
1742 default: None,
1743 auto_gen: None,
1744 enum_name: None,
1745 }],
1746 strict: false,
1747 without_rowid: false,
1748 pg_schema: String::new(),
1749 });
1750 definition.foreign_keys.push(ForeignKeyDef {
1751 name: "posts_user_fk".into(),
1752 table: "posts".into(),
1753 columns: vec!["user_id".into()],
1754 table_to: "users".into(),
1755 columns_to: vec!["id".into()],
1756 on_delete: "Set Default".into(),
1757 on_update: "No Action".into(),
1758 pg_schema: String::new(),
1759 pg_schema_to: String::new(),
1760 });
1761
1762 let error = validate_schema(&definition).expect_err("SET DEFAULT is invalid in MySQL");
1763 assert!(error.to_string().contains("does not support SET DEFAULT"));
1764 }
1765
1766 #[test]
1767 fn validate_rejects_empty_tables() {
1768 let mut def = minimal_sqlite_def();
1769 def.tables.clear();
1770 let err = validate_schema(&def).unwrap_err();
1771 assert!(err.to_string().contains("at least one table"));
1772 }
1773
1774 #[test]
1775 fn validate_rejects_duplicate_table_names() {
1776 let mut def = minimal_sqlite_def();
1777 def.tables.push(def.tables[0].clone());
1778 let err = validate_schema(&def).unwrap_err();
1779 assert!(err.to_string().contains("Duplicate table name"));
1780 }
1781
1782 #[test]
1783 fn validate_rejects_empty_columns() {
1784 let mut def = minimal_sqlite_def();
1785 def.tables[0].columns.clear();
1786 let err = validate_schema(&def).unwrap_err();
1787 assert!(err.to_string().contains("at least one column"));
1788 }
1789
1790 #[test]
1791 fn validate_rejects_duplicate_column_names() {
1792 let mut def = minimal_sqlite_def();
1793 let dup = def.tables[0].columns[0].clone();
1794 def.tables[0].columns.push(dup);
1795 let err = validate_schema(&def).unwrap_err();
1796 assert!(err.to_string().contains("Duplicate column name"));
1797 }
1798
1799 #[test]
1800 fn validate_rejects_identity_on_sqlite() {
1801 let mut def = minimal_sqlite_def();
1802 def.tables[0].columns[0].auto_gen = Some(AutoGenKind::Identity);
1803 let err = validate_schema(&def).unwrap_err();
1804 assert!(err.to_string().contains("identity"));
1805 assert!(err.to_string().contains("PostgreSQL"));
1806 }
1807
1808 #[test]
1809 fn validate_rejects_autoincrement_on_postgres() {
1810 let mut def = minimal_sqlite_def();
1811 def.dialect = Dialect::Postgresql;
1812 def.tables[0].columns[0].auto_gen = Some(AutoGenKind::Autoincrement);
1813 let err = validate_schema(&def).unwrap_err();
1814 assert!(err.to_string().contains("autoincrement"));
1815 assert!(err.to_string().contains("SQLite"));
1816 }
1817
1818 #[test]
1819 fn validate_rejects_strict_on_postgres() {
1820 let mut def = minimal_sqlite_def();
1821 def.dialect = Dialect::Postgresql;
1822 def.tables[0].strict = true;
1823 let err = validate_schema(&def).unwrap_err();
1824 assert!(err.to_string().contains("strict"));
1825 assert!(err.to_string().contains("SQLite"));
1826 }
1827
1828 #[test]
1829 fn validate_rejects_enums_on_sqlite() {
1830 let mut def = minimal_sqlite_def();
1831 def.enums.push(EnumDef {
1832 name: "status".into(),
1833 variants: vec!["active".into()],
1834 });
1835 let err = validate_schema(&def).unwrap_err();
1836 assert!(err.to_string().contains("Enums"));
1837 assert!(err.to_string().contains("PostgreSQL"));
1838 }
1839
1840 #[test]
1841 fn validate_rejects_unknown_enum_reference() {
1842 let mut def = minimal_sqlite_def();
1843 def.dialect = Dialect::Postgresql;
1844 def.tables[0].columns[0].enum_name = Some("nonexistent".into());
1845 let err = validate_schema(&def).unwrap_err();
1846 assert!(err.to_string().contains("unknown enum"));
1847 }
1848
1849 #[test]
1850 fn validate_rejects_bad_fk_table_ref() {
1851 let mut def = minimal_sqlite_def();
1852 def.foreign_keys.push(ForeignKeyDef {
1853 name: "test_fk".into(),
1854 table: "nonexistent".into(),
1855 columns: vec!["id".into()],
1856 table_to: "users".into(),
1857 columns_to: vec!["id".into()],
1858 on_delete: "No Action".into(),
1859 on_update: "No Action".into(),
1860 pg_schema: String::new(),
1861 pg_schema_to: String::new(),
1862 });
1863 let err = validate_schema(&def).unwrap_err();
1864 assert!(err.to_string().contains("unknown source table"));
1865 }
1866
1867 #[test]
1868 fn validate_rejects_bad_fk_action() {
1869 let mut def = minimal_sqlite_def();
1870 def.tables.push(TableDef {
1871 name: "posts".into(),
1872 columns: vec![ColumnDef {
1873 name: "user_id".into(),
1874 sql_type: "integer".into(),
1875 not_null: true,
1876 primary_key: false,
1877 unique: false,
1878 default: None,
1879 auto_gen: None,
1880 enum_name: None,
1881 }],
1882 strict: false,
1883 without_rowid: false,
1884 pg_schema: String::new(),
1885 });
1886 def.foreign_keys.push(ForeignKeyDef {
1887 name: "posts_user_id_fk".into(),
1888 table: "posts".into(),
1889 columns: vec!["user_id".into()],
1890 table_to: "users".into(),
1891 columns_to: vec!["id".into()],
1892 on_delete: "INVALID".into(),
1893 on_update: "No Action".into(),
1894 pg_schema: String::new(),
1895 pg_schema_to: String::new(),
1896 });
1897 let err = validate_schema(&def).unwrap_err();
1898 assert!(err.to_string().contains("invalid on_delete"));
1899 }
1900
1901 #[test]
1902 fn validate_rejects_bad_index_column_ref() {
1903 let mut def = minimal_sqlite_def();
1904 def.indexes.push(IndexDef {
1905 name: "test_idx".into(),
1906 table: "users".into(),
1907 columns: vec!["nonexistent".into()],
1908 unique: false,
1909 pg_schema: String::new(),
1910 });
1911 let err = validate_schema(&def).unwrap_err();
1912 assert!(err.to_string().contains("unknown column"));
1913 }
1914
1915 #[test]
1916 fn json_round_trip() {
1917 let def = minimal_sqlite_def();
1918 let json = serde_json::to_string_pretty(&def).unwrap();
1919 let parsed: SchemaDefinition = serde_json::from_str(&json).unwrap();
1920 assert_eq!(parsed.dialect, def.dialect);
1921 assert_eq!(parsed.tables.len(), 1);
1922 assert_eq!(parsed.tables[0].name, "users");
1923 assert_eq!(parsed.tables[0].columns[0].name, "id");
1924 }
1925
1926 #[test]
1927 fn json_defaults_applied() {
1928 let json = r#"{
1929 "dialect": "sqlite",
1930 "tables": [{
1931 "name": "items",
1932 "columns": [{"name": "id", "sql_type": "integer"}]
1933 }]
1934 }"#;
1935 let def: SchemaDefinition = serde_json::from_str(json).unwrap();
1936 assert_eq!(def.schema_name, "AppSchema");
1937 assert_eq!(def.output_path, "src/schema.rs");
1938 assert!(def.enums.is_empty());
1939 assert!(def.indexes.is_empty());
1940 assert!(def.foreign_keys.is_empty());
1941 assert!(!def.tables[0].columns[0].not_null);
1942 assert!(!def.tables[0].columns[0].primary_key);
1943 }
1944
1945 #[test]
1946 fn json_fk_action_defaults() {
1947 let json = r#"{
1948 "name": "test_fk",
1949 "table": "a",
1950 "columns": ["x"],
1951 "table_to": "b",
1952 "columns_to": ["y"]
1953 }"#;
1954 let fk: ForeignKeyDef = serde_json::from_str(json).unwrap();
1955 assert_eq!(fk.on_delete, "No Action");
1956 assert_eq!(fk.on_update, "No Action");
1957 }
1958}