Skip to content

Commit e30d125

Browse files
L33gn21claude
andcommitted
feat(exporter): add Prisma ORM exporter
Add a Prisma schema generator as a fourth ORM backend alongside SeaORM, SQLAlchemy, and SQLModel. Renders enum + model blocks with full schema context (relations, indexes, unique/composite constraints, native types, default attributes, referential actions) and wires Prisma into the cross-ORM test harness and CLI export command. Model names use plural PascalCase; string default values escape backslashes correctly. Snapshots cover the full column-type matrix plus relation, enum, default, and reserved-identifier scenarios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8c050b6 commit e30d125

8 files changed

Lines changed: 721 additions & 5 deletions

File tree

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ use rayon::prelude::*;
88
use tokio::fs;
99
use vespertide_config::VespertideConfig;
1010
use vespertide_core::TableDef;
11-
use vespertide_exporter::{Orm, render_entity_with_schema, seaorm::SeaOrmExporterWithConfig};
11+
use vespertide_exporter::{
12+
Orm, render_entity_with_schema, prisma::PrismaExporterWithConfig,
13+
seaorm::SeaOrmExporterWithConfig,
14+
};
1215

1316
use crate::parallel_config::{EXPORT_RENDER_PAR_MIN_LEN, EXPORT_RENDER_PAR_THRESHOLD};
1417
use crate::utils::load_config;
@@ -19,6 +22,7 @@ pub enum OrmArg {
1922
Sqlalchemy,
2023
Sqlmodel,
2124
Jpa,
25+
Prisma,
2226
}
2327

2428
impl From<OrmArg> for Orm {
@@ -28,6 +32,7 @@ impl From<OrmArg> for Orm {
2832
OrmArg::Sqlalchemy => Orm::SqlAlchemy,
2933
OrmArg::Sqlmodel => Orm::SqlModel,
3034
OrmArg::Jpa => Orm::Jpa,
35+
OrmArg::Prisma => Orm::Prisma,
3136
}
3237
}
3338
}
@@ -51,6 +56,11 @@ pub async fn cmd_export(orm: OrmArg, export_dir: Option<PathBuf>) -> Result<()>
5156

5257
let target_root = resolve_export_dir(export_dir, &config);
5358

59+
// Prisma uses a single-file output strategy
60+
if matches!(orm, OrmArg::Prisma) {
61+
return cmd_export_prisma(config, normalized_models, target_root).await;
62+
}
63+
5464
// Clean the export directory before regenerating
5565
let orm_kind: Orm = orm.into();
5666
clean_export_dir(&target_root, orm_kind).await?;
@@ -238,6 +248,7 @@ async fn clean_export_dir(root: &Path, orm: Orm) -> Result<()> {
238248
Orm::SeaOrm => "rs",
239249
Orm::SqlAlchemy | Orm::SqlModel => "py",
240250
Orm::Jpa => "java",
251+
Orm::Prisma => "prisma",
241252
};
242253

243254
clean_dir_recursive(root, ext).await?;
@@ -330,6 +341,7 @@ fn build_output_path(root: &Path, rel_path: &Path, orm: Orm) -> PathBuf {
330341
Orm::SeaOrm => "rs",
331342
Orm::SqlAlchemy | Orm::SqlModel => "py",
332343
Orm::Jpa => "java",
344+
Orm::Prisma => "prisma",
333345
};
334346
// Java requires filename to match PascalCase class name
335347
let file_stem = if matches!(orm, Orm::Jpa) {
@@ -426,6 +438,36 @@ async fn ensure_mod_chain(root: &Path, rel_path: &Path) -> Result<()> {
426438
Ok(())
427439
}
428440

441+
async fn cmd_export_prisma(
442+
config: VespertideConfig,
443+
normalized_models: Vec<(TableDef, PathBuf)>,
444+
target_root: PathBuf,
445+
) -> Result<()> {
446+
let all_tables: Vec<TableDef> = normalized_models.iter().map(|(t, _)| t.clone()).collect();
447+
let content = PrismaExporterWithConfig::new(config.prisma()).render_schema(&all_tables);
448+
449+
clean_export_dir(&target_root, Orm::Prisma).await?;
450+
451+
if !target_root.exists() {
452+
fs::create_dir_all(&target_root)
453+
.await
454+
.with_context(|| format!("create export dir {}", target_root.display()))?;
455+
}
456+
457+
let out_path = target_root.join("schema.prisma");
458+
fs::write(&out_path, &content)
459+
.await
460+
.with_context(|| format!("write {}", out_path.display()))?;
461+
462+
println!(
463+
"Exported {} model(s) -> {}",
464+
normalized_models.len(),
465+
out_path.display()
466+
);
467+
468+
Ok(())
469+
}
470+
429471
#[async_recursion::async_recursion]
430472
async fn walk_models(
431473
root: &Path,

crates/vespertide-config/src/config.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,50 @@ impl SeaOrmConfig {
7878
}
7979
}
8080

81+
/// Prisma-specific export configuration.
82+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83+
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84+
#[serde(rename_all = "camelCase")]
85+
pub struct PrismaConfig {
86+
/// Database provider: postgresql, mysql, sqlite, sqlserver, mongodb, cockroachdb.
87+
#[serde(default = "default_prisma_provider")]
88+
pub provider: String,
89+
/// Optional output path for the generated Prisma client.
90+
#[serde(default, skip_serializing_if = "Option::is_none")]
91+
pub client_output: Option<String>,
92+
/// Optional relationMode override ("foreignKeys" or "prisma").
93+
#[serde(default, skip_serializing_if = "Option::is_none")]
94+
pub relation_mode: Option<String>,
95+
}
96+
97+
fn default_prisma_provider() -> String {
98+
"postgresql".to_string()
99+
}
100+
101+
impl Default for PrismaConfig {
102+
fn default() -> Self {
103+
Self {
104+
provider: default_prisma_provider(),
105+
client_output: None,
106+
relation_mode: None,
107+
}
108+
}
109+
}
110+
111+
impl PrismaConfig {
112+
pub fn provider(&self) -> &str {
113+
&self.provider
114+
}
115+
116+
pub fn client_output(&self) -> Option<&str> {
117+
self.client_output.as_deref()
118+
}
119+
120+
pub fn relation_mode(&self) -> Option<&str> {
121+
self.relation_mode.as_deref()
122+
}
123+
}
124+
81125
/// Top-level vespertide configuration.
82126
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83127
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
@@ -100,6 +144,9 @@ pub struct VespertideConfig {
100144
/// SeaORM-specific export configuration.
101145
#[serde(default)]
102146
pub seaorm: SeaOrmConfig,
147+
/// Prisma-specific export configuration.
148+
#[serde(default)]
149+
pub prisma: PrismaConfig,
103150
/// Prefix to add to all table names (including migration version table).
104151
/// Default: "" (no prefix)
105152
#[serde(default)]
@@ -138,6 +185,7 @@ impl Default for VespertideConfig {
138185
migration_filename_pattern: default_migration_filename_pattern(),
139186
model_export_dir: default_model_export_dir(),
140187
seaorm: SeaOrmConfig::default(),
188+
prisma: PrismaConfig::default(),
141189
prefix: String::new(),
142190
lock_timeout_ms: None,
143191
statement_timeout_ms: None,
@@ -191,6 +239,11 @@ impl VespertideConfig {
191239
&self.seaorm
192240
}
193241

242+
/// Prisma-specific export configuration.
243+
pub fn prisma(&self) -> &PrismaConfig {
244+
&self.prisma
245+
}
246+
194247
/// Prefix to add to all table names.
195248
pub fn prefix(&self) -> &str {
196249
&self.prefix

crates/vespertide-config/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub mod config;
77
pub mod file_format;
88
pub mod name_case;
99

10-
pub use config::{SeaOrmConfig, VespertideConfig, default_migration_filename_pattern};
10+
pub use config::{PrismaConfig, SeaOrmConfig, VespertideConfig, default_migration_filename_pattern};
1111
pub use file_format::FileFormat;
1212
pub use name_case::NameCase;
1313

crates/vespertide-exporter/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//! Helpers to convert `TableDef` models into ORM-specific representations
2-
//! such as `SeaORM`, `SQLAlchemy`, `SQLModel`, and JPA.
2+
//! such as `SeaORM`, `SQLAlchemy`, `SQLModel`, JPA, and Prisma.
33
44
pub mod jpa;
55
pub mod orm;
66
mod parallel_config;
7+
pub mod prisma;
78
pub mod seaorm;
89
pub mod sqlalchemy;
910
pub mod sqlmodel;
@@ -13,6 +14,7 @@ mod utils;
1314

1415
pub use jpa::JpaExporter;
1516
pub use orm::{Orm, OrmExporter, render_entity, render_entity_with_schema};
17+
pub use prisma::PrismaExporter;
1618
pub use seaorm::{SeaOrmExporter, render_entity as render_seaorm_entity};
1719
pub use sqlalchemy::SqlAlchemyExporter;
1820
pub use sqlmodel::SqlModelExporter;

crates/vespertide-exporter/src/orm.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use vespertide_core::TableDef;
22

33
use crate::{
4-
jpa::JpaExporter, seaorm::SeaOrmExporter, sqlalchemy::SqlAlchemyExporter,
5-
sqlmodel::SqlModelExporter,
4+
jpa::JpaExporter, prisma::PrismaExporter, seaorm::SeaOrmExporter,
5+
sqlalchemy::SqlAlchemyExporter, sqlmodel::SqlModelExporter,
66
};
77

88
/// Supported ORM targets.
@@ -12,6 +12,7 @@ pub enum Orm {
1212
SqlAlchemy,
1313
SqlModel,
1414
Jpa,
15+
Prisma,
1516
}
1617

1718
/// Standardized exporter interface for all supported ORMs.
@@ -36,6 +37,7 @@ pub fn render_entity(orm: Orm, table: &TableDef) -> Result<String, String> {
3637
Orm::SqlAlchemy => SqlAlchemyExporter.render_entity(table),
3738
Orm::SqlModel => SqlModelExporter.render_entity(table),
3839
Orm::Jpa => JpaExporter.render_entity(table),
40+
Orm::Prisma => PrismaExporter.render_entity(table),
3941
}
4042
}
4143

@@ -50,6 +52,7 @@ pub fn render_entity_with_schema(
5052
Orm::SqlAlchemy => SqlAlchemyExporter.render_entity_with_schema(table, schema),
5153
Orm::SqlModel => SqlModelExporter.render_entity_with_schema(table, schema),
5254
Orm::Jpa => JpaExporter.render_entity_with_schema(table, schema),
55+
Orm::Prisma => PrismaExporter.render_entity_with_schema(table, schema),
5356
}
5457
}
5558

@@ -64,6 +67,7 @@ mod tests {
6467
#[case::sqlalchemy(Orm::SqlAlchemy)]
6568
#[case::sqlmodel(Orm::SqlModel)]
6669
#[case::jpa(Orm::Jpa)]
70+
#[case::prisma(Orm::Prisma)]
6771
fn dispatch_render_entity_succeeds(#[case] orm: Orm) {
6872
let table = basic_single_pk();
6973
assert!(render_entity(orm, &table).is_ok());
@@ -74,6 +78,7 @@ mod tests {
7478
#[case::sqlalchemy(Orm::SqlAlchemy)]
7579
#[case::sqlmodel(Orm::SqlModel)]
7680
#[case::jpa(Orm::Jpa)]
81+
#[case::prisma(Orm::Prisma)]
7782
fn dispatch_render_entity_with_schema_succeeds(#[case] orm: Orm) {
7883
let table = basic_single_pk();
7984
let schema = vec![table.clone()];

0 commit comments

Comments
 (0)