Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ impl AccessPolicy {
/// with validation and path resolution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SidecarConfig {
/// Storage backend: "sqlite" or "json".
/// Storage backend: "sqlite" (default) or "postgres"/"postgresql".
pub storage: String,
/// File path for the sidecar database.
pub path: String,
Expand Down
40 changes: 28 additions & 12 deletions src/codegen/overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,29 @@ pub enum SqlDialect {
impl SqlDialect {
/// Map a `[sidecar].storage` value to a dialect. `sqlite` →
/// [`SqlDialect::Sqlite`]; `postgres`/`postgresql` →
/// [`SqlDialect::Postgres`]. `json` and unknown values are rejected
/// (the previous behaviour silently emitted SQLite DDL regardless,
/// V-L2-F1). The JSON store is tracked separately by #112.
/// [`SqlDialect::Postgres`]. Every other value is rejected rather than
/// silently emitting SQLite DDL regardless of the configured store
/// (V-L2-F1).
///
/// This is the single source of truth for "is this a supported sidecar
/// storage value?": [`crate::manifest::validate_manifest`] calls it so
/// `validate`, `doctor`, and `generate` cannot disagree on the closed
/// set.
///
/// V-L2-F2 (#112): a JSON sidecar store was evaluated here and dropped
/// rather than implemented. There is no storage-abstraction layer to
/// host a second runtime backend yet (provenance, temporal, drift, and
/// gc are all SQLite-specific), so leaving it advertised-but-rejected
/// was worse than removing it. If a flat-file store is wanted later it
/// should be designed against a real storage trait and re-added to this
/// match — until then it is just another unsupported value.
pub fn from_storage(storage: &str) -> anyhow::Result<Self> {
match storage.to_lowercase().as_str() {
"sqlite" => Ok(SqlDialect::Sqlite),
"postgres" | "postgresql" => Ok(SqlDialect::Postgres),
"json" => anyhow::bail!(
"[sidecar].storage = \"json\" is not implemented (it previously \
emitted SQLite DDL silently). Use \"sqlite\". The JSON sidecar \
store is tracked by hyperpolymath/verisimiser#112."
),
other => anyhow::bail!(
"unknown [sidecar].storage {other:?}; supported: \"sqlite\" \
(\"postgres\" for a PostgreSQL sidecar; \"json\" is #112)."
"unsupported [sidecar].storage {other:?}; supported backends \
are \"sqlite\" (default) and \"postgres\"/\"postgresql\"."
),
}
}
Expand Down Expand Up @@ -879,10 +887,18 @@ mod tests {
SqlDialect::from_storage("PostgreSQL").unwrap(),
SqlDialect::Postgres
);
// V-L2-F2 (#112): the JSON store was dropped, not implemented, so
// `json` is now rejected exactly like any other unsupported value —
// no special-case "coming soon" text and no #112 pointer.
let json_err = SqlDialect::from_storage("json").unwrap_err().to_string();
assert!(
json_err.contains("not implemented") && json_err.contains("#112"),
"json must be rejected with the #112 pointer, got: {json_err}"
json_err.contains("unsupported") && json_err.contains("sqlite"),
"json must be rejected as unsupported and list the supported \
backends, got: {json_err}"
);
assert!(
!json_err.contains("#112") && !json_err.contains("not implemented"),
"a dropped backend must not be advertised as tracked/coming, got: {json_err}"
);
assert!(SqlDialect::from_storage("mariadb").is_err());
}
Expand Down
5 changes: 4 additions & 1 deletion src/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,10 @@ mod tests {

#[test]
fn gc_rejects_non_sqlite_backend() {
let m = fixture("/dev/null", RetentionConfig::default(), "json");
// `postgres` is a valid generate-time dialect, but gc is SQLite-only
// and must refuse rather than silently no-op. (The `json` value was
// dropped as a storage option entirely in V-L2-F2 / #112.)
let m = fixture("/dev/null", RetentionConfig::default(), "postgres");
let err = run_gc(&m, true).unwrap_err();
assert!(
err.to_string().contains("only supports the SQLite sidecar"),
Expand Down
7 changes: 4 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,10 @@ fn main() -> Result<()> {
// Create output directory.
std::fs::create_dir_all(&output)?;

// The sidecar DDL dialect follows [sidecar].storage. This
// rejects `json` (tracked by #112) instead of silently
// emitting SQLite DDL for a non-SQLite store (V-L2-F1).
// The sidecar DDL dialect follows [sidecar].storage. Unknown or
// unsupported values are rejected here (and earlier by
// `validate`) instead of silently emitting SQLite DDL for a
// non-SQLite store (V-L2-F1 / V-L2-F2).
let dialect = codegen::overlay::SqlDialect::from_storage(&m.sidecar.storage)?;

// Generate sidecar overlay schema. Errors here surface invalid
Expand Down
113 changes: 108 additions & 5 deletions src/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,14 @@ mod octad_tests {
/// temporal versions, and access policies. It never writes to your target database.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SidecarConfig {
/// Storage backend for the sidecar. `"sqlite"` (default) is the only
/// implemented store; `"postgres"`/`"postgresql"` selects the
/// PostgreSQL DDL dialect. `"json"` is **not implemented** and is
/// rejected at `generate` time — tracked by #112 (V-L2-F2). The
/// dialect mapping lives in `codegen::overlay::SqlDialect`.
/// Storage backend for the sidecar. The supported set is closed:
/// `"sqlite"` (default) is the fully-featured runtime store used by
/// `generate`, `drift`, and `gc`; `"postgres"`/`"postgresql"` selects
/// the PostgreSQL DDL dialect emitted by `generate`. Any other value is
/// rejected — `validate`/`doctor` flag it up front via the
/// `sidecar-storage-supported` check, and `generate` refuses it rather
/// than silently emitting SQLite DDL. The canonical mapping lives in
/// [`crate::codegen::overlay::SqlDialect::from_storage`].
#[serde(default = "default_sidecar_storage")]
pub storage: String,

Expand Down Expand Up @@ -394,6 +397,78 @@ mod validate_manifest_tests {
assert_eq!(failed, vec!["schema-source-exists"]);
}

/// V-L2-F2 (#112): a dropped/unsupported `[sidecar].storage` value
/// (here the removed `json` store) must fail the
/// `sidecar-storage-supported` check at validate time, not slip
/// through to `generate`. Other checks still run independently.
#[test]
fn unsupported_storage_fails_storage_check() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("verisimiser.toml");
let sidecar_path = dir.path().join("sidecar.db");
let body = format!(
"[project]\n\
name = \"test\"\n\
[database]\n\
backend = \"sqlite\"\n\
[sidecar]\n\
storage = \"json\"\n\
path = \"{}\"\n",
sidecar_path.display().to_string().replace('\\', "/")
);
std::fs::write(&path, body).expect("write");

let report = validate_manifest(path.to_str().unwrap());
assert!(!report.passed, "json storage must not validate");
let failed: Vec<&str> = report
.checks
.iter()
.filter(|c| !c.passed)
.map(|c| c.name.as_str())
.collect();
assert_eq!(
failed,
vec!["sidecar-storage-supported"],
"only the storage check should fail; checks: {:?}",
report.checks
);
}

/// The PostgreSQL dialect remains a supported `storage` value (it
/// selects the postgres DDL for `generate`), so a postgres sidecar
/// must still pass validation.
#[test]
fn postgres_storage_passes_storage_check() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("verisimiser.toml");
let sidecar_path = dir.path().join("sidecar.db");
let body = format!(
"[project]\n\
name = \"test\"\n\
[database]\n\
backend = \"postgresql\"\n\
[sidecar]\n\
storage = \"postgres\"\n\
path = \"{}\"\n",
sidecar_path.display().to_string().replace('\\', "/")
);
std::fs::write(&path, body).expect("write");

let report = validate_manifest(path.to_str().unwrap());
assert!(
report.passed,
"postgres storage must validate; checks: {:?}",
report.checks
);
assert!(
report
.checks
.iter()
.any(|c| c.name == "sidecar-storage-supported" && c.passed),
"the storage-supported check must run and pass"
);
}

/// A malformed manifest must fail `manifest-loads` and stop further
/// checks (because the rest depend on having a parsed manifest).
#[test]
Expand Down Expand Up @@ -621,6 +696,7 @@ enable-constraints = {enable_constraints}
enable-simulation = {enable_simulation}

[sidecar]
# storage backend: "sqlite" (default) or "postgres"
storage = "{sidecar_storage}"
path = "{sidecar_path}"

Expand Down Expand Up @@ -734,6 +810,10 @@ impl ValidationReport {
/// set, the file at that path is readable.
/// 3. **`sidecar-path-writable`** — the parent directory of
/// `[sidecar].path` is writable (or createable).
/// 4. **`sidecar-storage-supported`** — `[sidecar].storage` is one of the
/// supported backends (`sqlite`, `postgres`/`postgresql`). Reuses the
/// same mapping `generate` uses so a manifest that validates cannot
/// then fail at generate time on an unsupported store. V-L2-F2 (#112).
///
/// Out of scope here: V-L2-E1 backend/target_db conflict (own issue),
/// target-DB reachability (needs live connection).
Expand Down Expand Up @@ -820,6 +900,29 @@ pub fn validate_manifest(path: &str) -> ValidationReport {
)),
});
}

// 4. Sidecar storage backend is one we support. Reuses the same
// mapping `generate` uses (codegen::overlay::SqlDialect) so a
// manifest that validates here cannot then fail at generate
// time on an unsupported store — and so a dropped backend
// (e.g. json, V-L2-F2 #112) is rejected consistently at
// validate, doctor, and generate.
let storage_check =
match crate::codegen::overlay::SqlDialect::from_storage(&m.sidecar.storage) {
Ok(_) => ValidationCheck {
name: "sidecar-storage-supported".to_string(),
description: "[sidecar].storage is a supported backend".to_string(),
passed: true,
detail: None,
},
Err(e) => ValidationCheck {
name: "sidecar-storage-supported".to_string(),
description: "[sidecar].storage is a supported backend".to_string(),
passed: false,
detail: Some(e.to_string()),
},
};
checks.push(storage_check);
}

let passed = checks.iter().all(|c| c.passed);
Expand Down
Loading