use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
struct Fixture {
root: PathBuf,
}
impl Fixture {
fn new(label: &str) -> Self {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock after Unix epoch")
.as_nanos();
let root =
std::env::temp_dir().join(format!("noxid-wo38-{label}-{}-{nonce}", std::process::id()));
fs::create_dir_all(root.join("server/db/migrations"))
.expect("create fixture migration directory");
fs::create_dir(root.join("data")).expect("create fixture data directory");
fs::write(
root.join("Noxid.toml"),
"[project]\nname = \"sqlite-test\"\n",
)
.expect("write project manifest");
Self { root }
}
fn migration(&self, name: &str, sql: &str) {
fs::write(self.root.join("server/db/migrations").join(name), sql).expect("write migration");
}
fn database_url(&self) -> &'static str {
"sqlite://data/app.db"
}
fn noxid(&self, arguments: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_noxid"))
.current_dir(&self.root)
.args(arguments)
.env("DATABASE_URL", self.database_url())
.output()
.expect("execute noxid")
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn output_text(output: &Output) -> String {
format!(
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
fn assert_success(output: &Output, context: &str) {
assert!(
output.status.success(),
"{context}: {}",
output_text(output)
);
}
#[test]
fn sqlite_adapter_url_affinity_and_round_trip_contracts_pass_without_docker() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let output = Command::new("node")
.args(["--test", "plugins/drizzle-orm/sqlite.test.mjs"])
.current_dir(root)
.output()
.expect("Node.js is required by the workspace test gate");
assert!(output.status.success(), "{}", output_text(&output));
}
#[test]
fn sqlite_migration_runner_has_status_apply_rollback_and_checksum_parity() {
let fixture = Fixture::new("migration-parity");
let first_sql = "CREATE TABLE migration_probe (id INTEGER PRIMARY KEY);\nINSERT INTO migration_probe VALUES (1);\n";
fixture.migration("0001-create-probe.sql", first_sql);
let pending = fixture.noxid(&["db", "status"]);
assert_success(&pending, "read pending SQLite migration status");
assert!(
output_text(&pending).contains("pending 0001-create-probe.sql"),
"{}",
output_text(&pending)
);
let migrated = fixture.noxid(&["db", "migrate"]);
assert_success(&migrated, "apply SQLite migration");
assert!(
output_text(&migrated).contains("applied 0001-create-probe.sql"),
"{}",
output_text(&migrated)
);
let applied = fixture.noxid(&["db", "status"]);
assert_success(&applied, "read applied SQLite migration status");
assert!(
output_text(&applied).contains("applied 0001-create-probe.sql"),
"{}",
output_text(&applied)
);
fixture.migration(
"0002-rollback.sql",
"CREATE TABLE rollback_probe (id INTEGER PRIMARY KEY);\nINSERT INTO missing_table VALUES (1);\n",
);
let failed = fixture.noxid(&["db", "migrate"]);
assert!(!failed.status.success(), "migration unexpectedly succeeded");
fixture.migration(
"0002-rollback.sql",
"CREATE TABLE rollback_probe (id INTEGER PRIMARY KEY);\nINSERT INTO rollback_probe VALUES (1);\n",
);
let recovered = fixture.noxid(&["db", "migrate"]);
assert_success(
&recovered,
"apply corrected migration after transactional rollback",
);
fixture.migration("0001-create-probe.sql", "SELECT 1;\n");
let checksum = fixture.noxid(&["db", "status"]);
assert!(!checksum.status.success());
assert!(
output_text(&checksum)
.contains("checksum mismatch for applied migration 0001-create-probe.sql"),
"{}",
output_text(&checksum)
);
}
#[test]
fn sqlite_migration_runner_refuses_missing_parent_before_opening_a_database() {
let fixture = Fixture::new("missing-parent");
let output = Command::new(env!("CARGO_BIN_EXE_noxid"))
.current_dir(&fixture.root)
.args(["db", "status"])
.env("DATABASE_URL", "sqlite://typo/app.db")
.output()
.expect("execute noxid");
assert!(!output.status.success());
let text = output_text(&output);
assert!(
text.contains("error[SQLITE_DATABASE_PARENT_MISSING]"),
"{text}"
);
assert!(!fixture.root.join("typo").exists());
}