use jerrycan::platform::design::Design;
use jerrycan::platform::scaffold;
use std::fs;
const GOLDEN: &str = include_str!("../../../conformance/designs/todo-api.design.json");
fn db_design() -> Design {
let mut v: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
v["dependencies"] = serde_json::json!(["db", "validate"]);
serde_json::from_value(v).unwrap()
}
fn scaffold_db() -> (tempfile::TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("todo-api");
scaffold::scaffold(&root, &db_design()).unwrap();
(tmp, root)
}
#[test]
fn db_mode_emits_sql_repos_with_di_factories() {
let (_t, root) = scaffold_db();
let repo = fs::read_to_string(root.join("crates/routes/todos/src/repo.rs")).unwrap();
assert!(
repo.contains("pub(crate) async fn todo_repo(db: Dep<Db>)"),
"{repo}"
);
assert!(
repo.contains("use jerrycan::db::sea_orm;"),
"facade alias resolves bare sea_orm:: paths: {repo}"
);
assert!(
repo.contains("todo::Entity::find()") && repo.contains(".all(self.db.conn())"),
"reads go through SeaORM entity finders: {repo}"
);
assert!(
!repo.contains("SELECT ") && !repo.contains("self.db.sql("),
"no raw SQL strings in generated repos: {repo}"
);
assert!(
!repo.contains("build_any_sqlx")
&& !repo.contains("self.db.pool()")
&& !repo.contains("sea_query"),
"repos are SeaORM now, not sea-query/sqlx: {repo}"
);
assert!(
repo.contains("id: sea_orm::ActiveValue::NotSet,"),
"synthetic pk is DB-assigned on insert: {repo}"
);
assert!(
!repo.contains(".last_insert_id()"),
"sqlite must not rely on last_insert_id: {repo}"
);
assert!(
repo.contains("pub async fn update(&self, id: i64, item: Todo)"),
"PUT/PATCH handlers need a persisting update: {repo}"
);
assert!(
repo.contains("title: Set(item.title),") && repo.contains("done: Set(item.done),"),
"update sets every non-pk field via the ActiveModel: {repo}"
);
assert!(repo.contains("map_err(db_error)"), "{repo}");
let lib = fs::read_to_string(root.join("crates/routes/todos/src/lib.rs")).unwrap();
assert!(lib.contains(".provide_dep(repo::todo_repo)"), "{lib}");
assert!(
!lib.contains("TodoRepo::new()"),
"no in-memory provide in db mode: {lib}"
);
}
#[test]
fn db_mode_emits_dual_dialect_migrations_from_entities() {
let (_t, root) = scaffold_db();
let sqlite = fs::read_to_string(
root.join("crates/routes/todos/migrations/sqlite/0001_create_tables.sql"),
)
.unwrap();
let postgres = fs::read_to_string(
root.join("crates/routes/todos/migrations/postgres/0001_create_tables.sql"),
)
.unwrap();
assert!(
sqlite.contains("CREATE TABLE \"todos\"") && sqlite.contains("PRIMARY KEY AUTOINCREMENT"),
"{sqlite}"
);
assert!(sqlite.to_lowercase().contains("\"title\" text not null"));
assert!(
sqlite.to_lowercase().contains("\"done\" boolean")
&& !sqlite.to_lowercase().contains("\"done\" boolean not null"),
"optional bool field is a nullable native boolean: {sqlite}"
);
assert!(postgres.to_lowercase().contains("bigserial"), "{postgres}");
assert!(
postgres.to_lowercase().contains("\"done\" bool")
&& !postgres.to_lowercase().contains("\"done\" bool not null"),
"optional bool field is a nullable native boolean: {postgres}"
);
assert!(root.join("crates/routes/todos/migrations/sqlite").exists());
let users = fs::read_to_string(
root.join("crates/routes/users/migrations/sqlite/0001_create_tables.sql"),
)
.unwrap();
assert!(users.contains("CREATE TABLE \"users\""));
}
#[test]
fn db_mode_wires_main_and_aggregated_migrations() {
let (_t, root) = scaffold_db();
let main_rs = fs::read_to_string(root.join("crates/app/src/main.rs")).unwrap();
assert!(
main_rs.contains("jerrycan::db::Db::from_env().await?"),
"{main_rs}"
);
assert!(
main_rs.contains("db.migrate(migrations::MIGRATIONS).await?"),
"{main_rs}"
);
assert!(main_rs.contains(".extend(db)"), "{main_rs}");
assert!(
main_rs.contains("OpenApi::new(include_str!"),
"validate mode mounts the doc: {main_rs}"
);
let agg = fs::read_to_string(root.join("crates/app/src/migrations.rs")).unwrap();
assert!(agg.contains("pub const MIGRATIONS"), "{agg}");
assert!(
agg.contains("routes/todos/migrations/sqlite/0001_create_tables.sql"),
"{agg}"
);
let ws = fs::read_to_string(root.join("Cargo.toml")).unwrap();
assert!(ws.contains("features = [\"db\", \"validate\"]"), "{ws}");
assert!(root.join("openapi.json").exists());
}
fn rustfmt(root: &std::path::Path, src: &str) -> String {
use std::io::Write as _;
let mut child = std::process::Command::new("rustfmt")
.args(["--edition", "2024", "--emit", "stdout"])
.current_dir(root)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("rustfmt must be runnable (pinned toolchain component)");
child
.stdin
.take()
.unwrap()
.write_all(src.as_bytes())
.unwrap();
let out = child.wait_with_output().unwrap();
assert!(
out.status.success(),
"rustfmt failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
const LONG_MOUNTS_AND_CORS: &str = r#"{
"name": "invites-app", "contract_version": 2,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"cors": {
"origins": ["https://app.example", "https://admin.example"],
"methods": ["GET", "POST", "PUT", "DELETE"],
"headers": ["content-type", "authorization"],
"allow_credentials": true
},
"storage": { "buckets": [
{ "name": "organization-documents", "visibility": "public", "max_size": "5MB" }
]},
"modules": [
{ "name": "organization-invitations",
"entities": [{ "name": "Invitation", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "email", "type": "string" } ]}],
"endpoints": [{ "operation_id": "list_invitations", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Invitation", "list": true } }] }
]
}"#;
#[test]
fn tool_owned_main_and_migrations_are_rustfmt_fixpoints() {
for design_src in [
include_str!("../../../conformance/designs/reference-slice.design.json"),
include_str!("../../../conformance/designs/limits-api.design.json"),
GOLDEN,
LONG_MOUNTS_AND_CORS,
] {
let design: Design = serde_json::from_str(design_src).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("app");
scaffold::scaffold(&root, &design).unwrap();
for rel in ["crates/app/src/main.rs", "crates/app/src/migrations.rs"] {
let path = root.join(rel);
if !path.exists() {
continue;
}
let emitted = fs::read_to_string(&path).unwrap();
let formatted = rustfmt(&root, &emitted);
assert_eq!(
emitted, formatted,
"{} must be a rustfmt fixpoint for design `{}` — otherwise an \
agent's `cargo fmt` rewrites it and JL0003 fires on a file the \
agent never touched",
rel, design.name
);
}
}
}
const FMT_PROBE: &str = r#"{
"name": "fmt-probe", "contract_version": 0, "dependencies": [],
"modules": [{
"name": "metrics",
"entities": [{ "name": "OrganizationSubscriptionRecord",
"fields": [{ "name": "label", "type": "string" }] }],
"endpoints": [
{ "operation_id": "stats", "method": "GET", "path": "/stats",
"success": { "status": 200 } },
{ "operation_id": "create_organization_subscription_record",
"method": "POST", "path": "/",
"request_body": { "entity": "OrganizationSubscriptionRecord" },
"success": { "status": 201, "entity": "OrganizationSubscriptionRecord" } }
]
}]
}"#;
const LONG_CHILD_TENANT: &str = r#"{
"name": "long-child-tenant-app",
"contract_version": 2,
"auth": { "model": "jwt", "roles": ["owner", "member"] },
"dependencies": ["db", "auth", "validate"],
"tenancy": { "entity": "Workspace", "member_roles": ["owner", "member"] },
"modules": [
{
"name": "users",
"entities": [
{ "name": "User", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "email", "type": "string", "unique": true },
{ "name": "password", "type": "string" },
{ "name": "role", "type": "string", "values": ["admin", "user"] }
]}
],
"endpoints": [
{ "operation_id": "register", "method": "POST", "path": "/register",
"public": true,
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" },
"errors": [
{ "status": 409, "when": "email already registered" },
{ "status": 422, "when": "request body fails validation" }
] },
{ "operation_id": "login", "method": "POST", "path": "/login",
"public": true,
"success": { "status": 200 },
"errors": [{ "status": 401, "when": "invalid email or password" }] }
]
},
{
"name": "workspaces",
"entities": [
{ "name": "Workspace", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "name", "type": "string" }
]}
],
"endpoints": [
{ "operation_id": "list_workspaces", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "Workspace", "list": true } },
{ "operation_id": "create_workspace", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "Workspace" },
"success": { "status": 201, "entity": "Workspace" },
"errors": [{ "status": 422, "when": "request body fails validation" }] }
]
},
{
"name": "billing-records",
"entities": [
{ "name": "SubscriptionBillingRecord",
"belongs_to": [{ "entity": "Workspace", "on_delete": "cascade" }],
"fields": [
{ "name": "id", "type": "integer" },
{ "name": "label", "type": "string" },
{ "name": "status", "type": "string", "values": ["new", "active", "closed"] }
]}
],
"endpoints": [
{ "operation_id": "list_billing_records", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "SubscriptionBillingRecord", "list": true } },
{ "operation_id": "create_billing_record", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "SubscriptionBillingRecord" },
"success": { "status": 201, "entity": "SubscriptionBillingRecord" },
"errors": [{ "status": 422, "when": "request body fails validation" }] }
]
}
]
}"#;
const OWNER_SCOPED_LONG: &str = r#"{
"name": "owner-scoped-app",
"contract_version": 2,
"auth": { "model": "jwt", "roles": ["user"] },
"dependencies": ["db", "auth", "validate"],
"modules": [
{
"name": "users",
"entities": [
{ "name": "User", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "email", "type": "string", "unique": true },
{ "name": "password", "type": "string" },
{ "name": "role", "type": "string", "values": ["admin", "user"] }
]}
],
"endpoints": [
{ "operation_id": "register", "method": "POST", "path": "/register",
"public": true,
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" },
"errors": [
{ "status": 409, "when": "email already registered" },
{ "status": 422, "when": "request body fails validation" }
] },
{ "operation_id": "login", "method": "POST", "path": "/login",
"public": true,
"success": { "status": 200 },
"errors": [{ "status": 401, "when": "invalid email or password" }] }
]
},
{
"name": "preferences",
"entities": [
{ "name": "CustomerSubscriptionPreference",
"belongs_to": [{ "entity": "User" }],
"fields": [
{ "name": "id", "type": "integer" },
{ "name": "label", "type": "string" },
{ "name": "channel", "type": "string", "values": ["email", "sms"] }
]}
],
"endpoints": [
{ "operation_id": "list_preferences", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "CustomerSubscriptionPreference", "list": true } },
{ "operation_id": "create_preference", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "CustomerSubscriptionPreference" },
"success": { "status": 201, "entity": "CustomerSubscriptionPreference" },
"errors": [{ "status": 422, "when": "request body fails validation" }] },
{ "operation_id": "update_preference", "method": "PUT", "path": "/{id}",
"auth_required": true,
"request_body": { "entity": "CustomerSubscriptionPreference" },
"success": { "status": 200, "entity": "CustomerSubscriptionPreference" },
"errors": [{ "status": 404, "when": "unknown id" }] }
]
}
]
}"#;
fn agent_owned_stub_files(routes: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
walk(&p, out);
} else if matches!(
p.file_name().and_then(|n| n.to_str()),
Some("handlers.rs") | Some("repo.rs")
) {
out.push(p);
}
}
}
walk(routes, &mut out);
out.sort();
out
}
#[test]
fn scaffold_stub_handlers_and_repos_are_rustfmt_fixpoints() {
let cases: &[(&str, &str)] = &[
("todo-api (memory)", GOLDEN),
("fmt-probe (memory, short+long)", FMT_PROBE),
(
"limits-api (db)",
include_str!("../../../conformance/designs/limits-api.design.json"),
),
(
"reference-slice (db tenant)",
include_str!("../../../conformance/designs/reference-slice.design.json"),
),
("long-child (db tenant, long child)", LONG_CHILD_TENANT),
("owner-scoped (db per-user, long)", OWNER_SCOPED_LONG),
];
for (label, src) in cases {
let design: Design = serde_json::from_str(src).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("app");
scaffold::scaffold(&root, &design).unwrap();
let files = agent_owned_stub_files(&root.join("crates/routes"));
assert!(
!files.is_empty(),
"{label}: scaffold produced no handlers.rs/repo.rs to check"
);
for path in files {
let emitted = fs::read_to_string(&path).unwrap();
let formatted = rustfmt(&root, &emitted);
let rel = path.strip_prefix(&root).unwrap_or(&path).display();
assert_eq!(
emitted, formatted,
"{label}: {rel} must be a rustfmt fixpoint — a fresh scaffold's \
agent-owned stub must survive `cargo fmt --check` untouched (#165)"
);
}
}
}
#[test]
fn sql_identifiers_are_quoted_so_reserved_words_survive() {
let mut v: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
v["dependencies"] = serde_json::json!(["db"]);
v["modules"][0]["entities"][0]["fields"][0]["name"] = serde_json::json!("order");
let design: Design = serde_json::from_value(v).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("todo-api");
scaffold::scaffold(&root, &design).unwrap();
let sqlite = fs::read_to_string(
root.join("crates/routes/todos/migrations/sqlite/0001_create_tables.sql"),
)
.unwrap();
assert!(
sqlite.to_lowercase().contains("\"order\" text"),
"reserved-word column must be quoted: {sqlite}"
);
}
#[test]
fn tenancy_generates_the_tenant_guard_in_shared() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: jerrycan::platform::design::Design = serde_json::from_str(s).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("app");
jerrycan::platform::scaffold::scaffold(&root, &d).unwrap();
let shared = std::fs::read_to_string(root.join("crates/shared/src/lib.rs")).unwrap();
assert!(shared.contains("pub struct Tenant"), "{shared}");
assert!(
shared.contains("pub async fn tenant("),
"guard factory: {shared}"
);
assert!(
shared.contains("workspace_members"),
"membership check: {shared}"
);
let main_rs = std::fs::read_to_string(root.join("crates/app/src/main.rs")).unwrap();
assert!(
main_rs.contains(".provide_dep(shared::tenant)"),
"{main_rs}"
);
}
#[test]
fn reference_slice_design_is_valid_contract_v2() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: jerrycan::platform::design::Design = serde_json::from_str(s).unwrap();
assert_eq!(d.contract_version, 2);
let qs = jerrycan::platform::questions::validate(&d);
assert!(qs.is_empty(), "{qs:?}");
assert_eq!(d.tenant_owned().len(), 2); }
#[test]
fn generate_migration_emits_numbered_pair_and_rewires() {
let (_t, root) = scaffold_db();
let created =
jerrycan::platform::genroute::generate_migration(&root, "todos", "add_due_index").unwrap();
assert!(
created
.iter()
.any(|p| p.ends_with("migrations/sqlite/0002_add_due_index.sql")),
"{created:?}"
);
assert!(
created
.iter()
.any(|p| p.ends_with("migrations/postgres/0002_add_due_index.sql")),
"{created:?}"
);
let agg = std::fs::read_to_string(root.join("crates/app/src/migrations.rs")).unwrap();
assert!(agg.contains("0002_add_due_index"), "{agg}");
let again = jerrycan::platform::genroute::generate_migration(&root, "todos", "more").unwrap();
assert!(
again.iter().any(|p| p.ends_with("0003_more.sql")),
"{again:?}"
);
}
#[tokio::test]
async fn schema_verify_flags_staleness_with_jc0520() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: jerrycan::platform::design::Design = serde_json::from_str(s).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("app");
jerrycan::platform::scaffold::scaffold(&root, &d).unwrap();
let c = jerrycan::platform::schema::derive_schema(&root, &d)
.await
.unwrap();
std::fs::write(
root.join("schema.json"),
jerrycan::platform::schema::render(&c),
)
.unwrap();
assert!(
jerrycan::platform::schema::verify_fresh(&root, &d)
.await
.unwrap()
.is_empty()
);
std::fs::write(root.join("schema.json"), "{}").unwrap();
let diags = jerrycan::platform::schema::verify_fresh(&root, &d)
.await
.unwrap();
assert!(diags.iter().any(|x| x.code == "JC0520"), "{diags:?}");
std::fs::remove_file(root.join("schema.json")).unwrap();
assert!(
!jerrycan::platform::schema::verify_fresh(&root, &d)
.await
.unwrap()
.is_empty()
);
}
#[tokio::test]
async fn cross_module_fk_lets_per_module_migration_insert_under_fk_enforcement() {
use jerrycan::db::sea_orm::ConnectionTrait;
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: Design = serde_json::from_str(s).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("app");
scaffold::scaffold(&root, &d).unwrap();
let all = jerrycan::platform::mounting::collect_migrations(&root).unwrap();
let leads_only: Vec<_> = all
.into_iter()
.filter(|m| m.name.starts_with("leads_"))
.collect();
assert!(
!leads_only.is_empty(),
"leads module must own a migration file"
);
let db = jerrycan::db::Db::connect("sqlite::memory:").await.unwrap();
db.migrate_owned(&leads_only).await.unwrap();
db.conn()
.execute_unprepared(
"INSERT INTO leads (workspace_id, phone, name, status) VALUES (1, '555', 'A', 'new')",
)
.await
.expect("cross-module fk is unenforced: insert with a dangling workspace_id must succeed");
let count = db
.conn()
.query_one(jerrycan::db::sea_orm::Statement::from_string(
jerrycan::db::sea_orm::DatabaseBackend::Sqlite,
"SELECT COUNT(*) AS n FROM leads".to_string(),
))
.await
.unwrap()
.unwrap();
let n: i64 = count
.try_get::<i64>("", "n")
.or_else(|_| count.try_get::<i32>("", "n").map(i64::from))
.unwrap();
assert_eq!(n, 1, "the lead row persisted");
}
#[test]
fn memory_mode_is_unchanged() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("todo-api");
let design: Design = serde_json::from_str(GOLDEN).unwrap();
scaffold::scaffold(&root, &design).unwrap();
let repo = fs::read_to_string(root.join("crates/routes/todos/src/repo.rs")).unwrap();
assert!(repo.contains("BTreeMap"), "in-memory repo stays: {repo}");
let main_rs = fs::read_to_string(root.join("crates/app/src/main.rs")).unwrap();
assert!(!main_rs.contains("jerrycan::db"));
assert!(!root.join("crates/app/src/migrations.rs").exists());
assert!(
root.join("openapi.json").exists(),
"openapi.json is emitted in every mode"
);
}
#[test]
fn db_mode_emits_composite_unique_index_both_dialects() {
const LIKES: &str = r#"{
"name": "likes-api", "contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "engagement",
"entities": [
{ "name": "User", "fields": [{ "name": "email", "type": "string" }] },
{ "name": "Post", "fields": [{ "name": "title", "type": "string" }] },
{ "name": "Like",
"belongs_to": [{ "entity": "User" }, { "entity": "Post" }],
"unique": [["user_id", "post_id"]],
"fields": [{ "name": "reaction", "type": "string" }] }
],
"endpoints": [
{ "operation_id": "create_like", "method": "POST", "path": "/likes",
"request_body": { "entity": "Like" },
"success": { "status": 201, "entity": "Like" } }
]
}]
}"#;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("likes-api");
let design: Design = serde_json::from_str(LIKES).unwrap();
scaffold::scaffold(&root, &design).unwrap();
for dialect in ["sqlite", "postgres"] {
let sql = fs::read_to_string(root.join(format!(
"crates/routes/engagement/migrations/{dialect}/0001_create_tables.sql"
)))
.unwrap();
assert!(
sql.contains(
"CREATE UNIQUE INDEX \"idx_likes_uc0\" ON \"likes\" (\"user_id\", \"post_id\")"
),
"{dialect}: the composite unique index must be emitted verbatim:\n{sql}"
);
assert_eq!(
sql.matches("CREATE UNIQUE INDEX").count(),
1,
"{dialect}: only the composite-unique entity gets an index:\n{sql}"
);
}
}
#[test]
fn db_mode_emits_aliased_fk_columns_with_distinct_constraint_names() {
const LEDGER: &str = r#"{
"name": "ledger-api", "contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "ledger",
"entities": [
{ "name": "Account", "fields": [{ "name": "name", "type": "string" }] },
{ "name": "Transfer",
"belongs_to": [
{ "entity": "Account", "as": "from_account" },
{ "entity": "Account", "as": "to_account" }
],
"fields": [{ "name": "amount", "type": "integer" }] },
{ "name": "Comment",
"belongs_to": [{ "entity": "Comment", "as": "parent", "on_delete": "cascade" }],
"fields": [{ "name": "body", "type": "string" }] }
],
"endpoints": [
{ "operation_id": "create_transfer", "method": "POST", "path": "/transfers",
"request_body": { "entity": "Transfer" },
"success": { "status": 201, "entity": "Transfer" } }
]
}]
}"#;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("ledger-api");
let design: Design = serde_json::from_str(LEDGER).unwrap();
scaffold::scaffold(&root, &design).unwrap();
for dialect in ["sqlite", "postgres"] {
let sql = fs::read_to_string(root.join(format!(
"crates/routes/ledger/migrations/{dialect}/0001_create_tables.sql"
)))
.unwrap();
assert!(
sql.contains("\"from_account_id\"") && sql.contains("\"to_account_id\""),
"{dialect}: both aliased fk columns must be emitted:\n{sql}"
);
assert!(
!sql.contains("\"account_id\""),
"{dialect}: the default account_id must NOT be emitted (the alias replaced it):\n{sql}"
);
assert_eq!(
sql.matches("REFERENCES \"accounts\"").count(),
2,
"{dialect}: two distinct FKs must reference accounts:\n{sql}"
);
assert!(
sql.contains("\"parent_id\"") && sql.contains("REFERENCES \"comments\""),
"{dialect}: the self-reference must emit parent_id → comments:\n{sql}"
);
}
let pg = fs::read_to_string(
root.join("crates/routes/ledger/migrations/postgres/0001_create_tables.sql"),
)
.unwrap();
assert!(
pg.contains("\"fk_transfers_from_account_id\"")
&& pg.contains("\"fk_transfers_to_account_id\"")
&& pg.contains("\"fk_comments_parent_id\""),
"postgres must name the three FK constraints distinctly:\n{pg}"
);
}