Skip to content

Commit 2f4095b

Browse files
committed
feat(cli): export --orm prisma에 --backend(-b) 플래그 추가
1 parent 3990cb2 commit 2f4095b

2 files changed

Lines changed: 110 additions & 15 deletions

File tree

crates/vespertide-cli/src/commands/export.rs

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ use tokio::fs;
99
use vespertide_config::VespertideConfig;
1010
use vespertide_core::TableDef;
1111
use vespertide_exporter::{
12-
Orm, prisma::PrismaExporterWithConfig, render_entity_with_schema,
12+
Orm,
13+
prisma::{PrismaExporterWithConfig, PrismaProvider},
14+
render_entity_with_schema,
1315
seaorm::SeaOrmExporterWithConfig,
1416
};
17+
use vespertide_query::DatabaseBackend;
1518

1619
use crate::parallel_config::{EXPORT_RENDER_PAR_MIN_LEN, EXPORT_RENDER_PAR_THRESHOLD};
1720
use crate::utils::load_config;
@@ -37,7 +40,21 @@ impl From<OrmArg> for Orm {
3740
}
3841
}
3942

40-
pub async fn cmd_export(orm: OrmArg, export_dir: Option<PathBuf>) -> Result<()> {
43+
/// A Prisma schema is provider-specific (`datasource` provider + `@db.*`
44+
/// native attrs), so the export backend maps onto the Prisma provider.
45+
fn prisma_provider_for_backend(backend: DatabaseBackend) -> PrismaProvider {
46+
match backend {
47+
DatabaseBackend::Postgres => PrismaProvider::Postgres,
48+
DatabaseBackend::MySql => PrismaProvider::MySql,
49+
DatabaseBackend::Sqlite => PrismaProvider::Sqlite,
50+
}
51+
}
52+
53+
pub async fn cmd_export(
54+
orm: OrmArg,
55+
export_dir: Option<PathBuf>,
56+
backend: DatabaseBackend,
57+
) -> Result<()> {
4158
let config = load_config()?;
4259
let models = load_models_recursive(config.models_dir())
4360
.await
@@ -58,7 +75,8 @@ pub async fn cmd_export(orm: OrmArg, export_dir: Option<PathBuf>) -> Result<()>
5875

5976
// Prisma uses a single-file output strategy
6077
if matches!(orm, OrmArg::Prisma) {
61-
return cmd_export_prisma(config, normalized_models, target_root).await;
78+
let provider = prisma_provider_for_backend(backend);
79+
return cmd_export_prisma(config, normalized_models, target_root, provider).await;
6280
}
6381

6482
// Clean the export directory before regenerating
@@ -183,7 +201,7 @@ fn render_export_entity(
183201

184202
/// Derive `crate::` prefix from the export directory path.
185203
///
186-
/// For example: `src/models` `crate::models`, `src/db/entities` `crate::db::entities`.
204+
/// For example: `src/models` ??`crate::models`, `src/db/entities` ??`crate::db::entities`.
187205
/// If the path doesn't start with `src/`, returns empty string (fallback to `super::` behavior).
188206
fn export_dir_to_crate_prefix(export_dir: &Path) -> String {
189207
let normalized = export_dir.to_string_lossy().replace('\\', "/");
@@ -199,8 +217,8 @@ fn export_dir_to_crate_prefix(export_dir: &Path) -> String {
199217

200218
/// Convert a relative model file path to Rust module path segments.
201219
///
202-
/// For example: `admin/admin.json` `["admin", "admin"]`
203-
/// `estimate/estimate_checker.vespertide.json` `["estimate", "estimate_checker"]`
220+
/// For example: `admin/admin.json` ??`["admin", "admin"]`
221+
/// `estimate/estimate_checker.vespertide.json` ??`["estimate", "estimate_checker"]`
204222
fn rel_path_to_module_segments(rel_path: &Path) -> Vec<String> {
205223
let mut segments = Vec::new();
206224

@@ -442,9 +460,11 @@ async fn cmd_export_prisma(
442460
config: VespertideConfig,
443461
normalized_models: Vec<(TableDef, PathBuf)>,
444462
target_root: PathBuf,
463+
provider: PrismaProvider,
445464
) -> Result<()> {
446465
let all_tables: Vec<TableDef> = normalized_models.iter().map(|(t, _)| t.clone()).collect();
447-
let content = PrismaExporterWithConfig::new(config.prisma()).render_schema(&all_tables);
466+
let content = PrismaExporterWithConfig::with_provider(config.prisma(), provider)
467+
.render_schema(&all_tables);
448468

449469
clean_export_dir(&target_root, Orm::Prisma).await?;
450470

@@ -560,7 +580,9 @@ mod tests {
560580
let model = sample_table("users");
561581
write_model(Path::new("models/users.json"), &model);
562582

563-
cmd_export(OrmArg::Seaorm, None).await.unwrap();
583+
cmd_export(OrmArg::Seaorm, None, DatabaseBackend::Postgres)
584+
.await
585+
.unwrap();
564586

565587
let out = PathBuf::from("src/models/users.rs");
566588
assert!(out.exists());
@@ -585,9 +607,13 @@ mod tests {
585607
write_model(Path::new("models/blog/posts.json"), &model);
586608

587609
let custom = PathBuf::from("out_dir");
588-
cmd_export(OrmArg::Seaorm, Some(custom.clone()))
589-
.await
590-
.unwrap();
610+
cmd_export(
611+
OrmArg::Seaorm,
612+
Some(custom.clone()),
613+
DatabaseBackend::Postgres,
614+
)
615+
.await
616+
.unwrap();
591617

592618
let out = custom.join("blog/posts.rs");
593619
assert!(out.exists());
@@ -615,7 +641,9 @@ mod tests {
615641
let model = sample_table("items");
616642
write_model(Path::new("models/items.json"), &model);
617643

618-
cmd_export(OrmArg::Sqlalchemy, None).await.unwrap();
644+
cmd_export(OrmArg::Sqlalchemy, None, DatabaseBackend::Postgres)
645+
.await
646+
.unwrap();
619647

620648
let out = PathBuf::from("src/models/items.py");
621649
assert!(out.exists());
@@ -633,14 +661,73 @@ mod tests {
633661
let model = sample_table("orders");
634662
write_model(Path::new("models/orders.json"), &model);
635663

636-
cmd_export(OrmArg::Sqlmodel, None).await.unwrap();
664+
cmd_export(OrmArg::Sqlmodel, None, DatabaseBackend::Postgres)
665+
.await
666+
.unwrap();
637667

638668
let out = PathBuf::from("src/models/orders.py");
639669
assert!(out.exists());
640670
let content = std_fs::read_to_string(out).unwrap();
641671
assert!(content.contains("orders"));
642672
}
643673

674+
#[rstest]
675+
#[case::postgres(
676+
DatabaseBackend::Postgres,
677+
"provider = \"postgresql\"",
678+
"@db.Timestamptz",
679+
None
680+
)]
681+
#[case::mysql(
682+
DatabaseBackend::MySql,
683+
"provider = \"mysql\"",
684+
"@db.Timestamp",
685+
Some("@db.Timestamptz")
686+
)]
687+
#[case::sqlite(
688+
DatabaseBackend::Sqlite,
689+
"provider = \"sqlite\"",
690+
"DateTime",
691+
Some("@db.")
692+
)]
693+
#[tokio::test]
694+
#[serial]
695+
async fn export_prisma_writes_provider_specific_schema(
696+
#[case] backend: DatabaseBackend,
697+
#[case] provider_line: &str,
698+
#[case] expected: &str,
699+
#[case] absent: Option<&str>,
700+
) {
701+
let tmp = tempdir().unwrap();
702+
let _guard = CwdGuard::new(&tmp.path().to_path_buf());
703+
write_config();
704+
705+
let mut model = sample_table("events");
706+
model.columns.push(ColumnDef {
707+
name: "occurred_at".into(),
708+
r#type: ColumnType::Simple(SimpleColumnType::Timestamptz),
709+
nullable: false,
710+
default: None,
711+
comment: None,
712+
primary_key: None,
713+
unique: None,
714+
index: None,
715+
foreign_key: None,
716+
});
717+
write_model(Path::new("models/events.json"), &model);
718+
719+
cmd_export(OrmArg::Prisma, None, backend).await.unwrap();
720+
721+
let out = PathBuf::from("src/models/schema.prisma");
722+
assert!(out.exists());
723+
let content = std_fs::read_to_string(out).unwrap();
724+
assert!(content.contains(provider_line));
725+
assert!(content.contains(expected));
726+
if let Some(absent) = absent {
727+
assert!(!content.contains(absent));
728+
}
729+
}
730+
644731
#[tokio::test]
645732
#[serial]
646733
async fn load_models_recursive_returns_empty_when_absent() {
@@ -730,6 +817,7 @@ mod tests {
730817
#[case(OrmArg::Sqlalchemy, Orm::SqlAlchemy)]
731818
#[case(OrmArg::Sqlmodel, Orm::SqlModel)]
732819
#[case(OrmArg::Jpa, Orm::Jpa)]
820+
#[case(OrmArg::Prisma, Orm::Prisma)]
733821
fn orm_arg_maps_to_enum(#[case] arg: OrmArg, #[case] expected: Orm) {
734822
assert_eq!(Orm::from(arg), expected);
735823
}
@@ -979,7 +1067,7 @@ mod tests {
9791067
use std::path::Path;
9801068
let root = Path::new("src/models");
9811069

982-
// snake_case model PascalCase .java
1070+
// snake_case model ??PascalCase .java
9831071
let rel_path = Path::new("order_item.json");
9841072
let out = build_output_path(root, rel_path, Orm::Jpa);
9851073
assert_eq!(out, Path::new("src/models/OrderItem.java"));

crates/vespertide-cli/src/main.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ enum Commands {
100100
/// Output directory (defaults to config modelsDir or src/models).
101101
#[arg(short = 'd', long = "export-dir")]
102102
export_dir: Option<std::path::PathBuf>,
103+
/// Database backend for Prisma schema generation (other ORMs ignore this).
104+
#[arg(short = 'b', long = "backend", value_enum, default_value = "postgres")]
105+
backend: BackendArg,
103106
},
104107
/// Export schema as ERD diagrams (SVG, Mermaid, Graphviz DOT).
105108
Erd {
@@ -143,7 +146,11 @@ async fn main() -> Result<()> {
143146
delete_null_rows,
144147
}) => cmd_revision(message, fill_with, delete_null_rows).await,
145148
Some(Commands::Init) => cmd_init().await,
146-
Some(Commands::Export { orm, export_dir }) => cmd_export(orm, export_dir).await,
149+
Some(Commands::Export {
150+
orm,
151+
export_dir,
152+
backend,
153+
}) => cmd_export(orm, export_dir, backend.into()).await,
147154
Some(Commands::Erd {
148155
format,
149156
output,

0 commit comments

Comments
 (0)