Skip to content

Commit e94d2a2

Browse files
feat(manifest): drop unimplemented json sidecar store (V-L2-F2, #112) (#144)
Drop the never-implemented `json` sidecar store (V-L2-F2). The octad data layer is intrinsically relational, so `json` is removed from the storage enum, docs, template, and validate/doctor messaging rather than built. Closes #112
1 parent fd77e9f commit e94d2a2

4 files changed

Lines changed: 97 additions & 23 deletions

File tree

src/abi/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ impl AccessPolicy {
460460
/// with validation and path resolution.
461461
#[derive(Debug, Clone, Serialize, Deserialize)]
462462
pub struct SidecarConfig {
463-
/// Storage backend: "sqlite" or "json".
463+
/// Storage backend: "sqlite" (default) or "postgres"/"postgresql".
464464
pub storage: String,
465465
/// File path for the sidecar database.
466466
pub path: String,

src/codegen/overlay.rs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -74,30 +74,35 @@ fn must_validate_identifier(name: &str) -> &str {
7474
/// genuinely dialect-divergent fragment is the metadata upsert
7575
/// (`INSERT OR IGNORE` vs `INSERT … ON CONFLICT DO NOTHING`), which lives
7676
/// in the [`sqlite`] / [`postgres`] modules.
77+
///
78+
/// [`from_storage`](SqlDialect::from_storage) is the single source of
79+
/// truth for which `[sidecar].storage` values are accepted; `generate`,
80+
/// `validate`, and `doctor` all defer to it.
7781
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
7882
pub enum SqlDialect {
7983
Sqlite,
8084
Postgres,
8185
}
8286

8387
impl SqlDialect {
84-
/// Map a `[sidecar].storage` value to a dialect. `sqlite` →
85-
/// [`SqlDialect::Sqlite`]; `postgres`/`postgresql` →
86-
/// [`SqlDialect::Postgres`]. `json` and unknown values are rejected
87-
/// (the previous behaviour silently emitted SQLite DDL regardless,
88-
/// V-L2-F1). The JSON store is tracked separately by #112.
88+
/// Map a `[sidecar].storage` value to a dialect (case-insensitive):
89+
/// `sqlite` → [`SqlDialect::Sqlite`]; `postgres`/`postgresql` →
90+
/// [`SqlDialect::Postgres`]. Every other value is rejected rather than
91+
/// silently emitting SQLite DDL regardless of the backend (V-L2-F1).
92+
///
93+
/// The octad data layer is intrinsically relational (hash-chains under
94+
/// `BEGIN IMMEDIATE`, partial-unique temporal indexes, `CHECK`
95+
/// constraints, recursive-CTE lineage acyclicity), so the
96+
/// never-implemented `json` document store was dropped rather than
97+
/// built (V-L2-F2, #112). It is now an unsupported value like any
98+
/// other — no special-casing, no "coming soon" pointer.
8999
pub fn from_storage(storage: &str) -> anyhow::Result<Self> {
90100
match storage.to_lowercase().as_str() {
91101
"sqlite" => Ok(SqlDialect::Sqlite),
92102
"postgres" | "postgresql" => Ok(SqlDialect::Postgres),
93-
"json" => anyhow::bail!(
94-
"[sidecar].storage = \"json\" is not implemented (it previously \
95-
emitted SQLite DDL silently). Use \"sqlite\". The JSON sidecar \
96-
store is tracked by hyperpolymath/verisimiser#112."
97-
),
98103
other => anyhow::bail!(
99-
"unknown [sidecar].storage {other:?}; supported: \"sqlite\" \
100-
(\"postgres\" for a PostgreSQL sidecar; \"json\" is #112)."
104+
"unsupported [sidecar].storage {other:?}; supported values are \
105+
\"sqlite\" (default) and \"postgres\"/\"postgresql\"."
101106
),
102107
}
103108
}
@@ -879,10 +884,18 @@ mod tests {
879884
SqlDialect::from_storage("PostgreSQL").unwrap(),
880885
SqlDialect::Postgres
881886
);
887+
// V-L2-F2 (#112): the json store was dropped, never implemented. It
888+
// is now rejected like any other unsupported value, and the error
889+
// advertises only the supported stores — it must NOT imply json is
890+
// planned (no "#112" / "not implemented" pointer).
882891
let json_err = SqlDialect::from_storage("json").unwrap_err().to_string();
883892
assert!(
884-
json_err.contains("not implemented") && json_err.contains("#112"),
885-
"json must be rejected with the #112 pointer, got: {json_err}"
893+
json_err.contains("unsupported") && json_err.contains("sqlite"),
894+
"json must be rejected as an unsupported store, got: {json_err}"
895+
);
896+
assert!(
897+
!json_err.contains("#112") && !json_err.to_lowercase().contains("not implemented"),
898+
"the dropped json store must not be advertised as planned, got: {json_err}"
886899
);
887900
assert!(SqlDialect::from_storage("mariadb").is_err());
888901
}

src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,9 @@ fn main() -> Result<()> {
209209
// Create output directory.
210210
std::fs::create_dir_all(&output)?;
211211

212-
// The sidecar DDL dialect follows [sidecar].storage. This
213-
// rejects `json` (tracked by #112) instead of silently
214-
// emitting SQLite DDL for a non-SQLite store (V-L2-F1).
212+
// The sidecar DDL dialect follows [sidecar].storage. Any value
213+
// other than sqlite/postgres is rejected here (V-L2-F1) instead
214+
// of silently emitting SQLite DDL for a non-SQLite store.
215215
let dialect = codegen::overlay::SqlDialect::from_storage(&m.sidecar.storage)?;
216216

217217
// Generate sidecar overlay schema. Errors here surface invalid

src/manifest/mod.rs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,11 @@ mod octad_tests {
279279
/// temporal versions, and access policies. It never writes to your target database.
280280
#[derive(Debug, Clone, Serialize, Deserialize)]
281281
pub struct SidecarConfig {
282-
/// Storage backend for the sidecar. `"sqlite"` (default) is the only
283-
/// implemented store; `"postgres"`/`"postgresql"` selects the
284-
/// PostgreSQL DDL dialect. `"json"` is **not implemented** and is
285-
/// rejected at `generate` time — tracked by #112 (V-L2-F2). The
286-
/// dialect mapping lives in `codegen::overlay::SqlDialect`.
282+
/// Storage backend for the sidecar. `"sqlite"` (default) is the
283+
/// reference store; `"postgres"`/`"postgresql"` selects the PostgreSQL
284+
/// DDL dialect. Any other value is rejected at `validate` and
285+
/// `generate` time by `codegen::overlay::SqlDialect::from_storage`,
286+
/// the single source of truth for supported stores.
287287
#[serde(default = "default_sidecar_storage")]
288288
pub storage: String,
289289

@@ -394,6 +394,41 @@ mod validate_manifest_tests {
394394
assert_eq!(failed, vec!["schema-source-exists"]);
395395
}
396396

397+
/// An unsupported `[sidecar].storage` (here the dropped `json` store,
398+
/// V-L2-F2 / #112) must fail `sidecar-storage-supported`, and the
399+
/// failure detail must not advertise json as a planned option.
400+
#[test]
401+
fn unsupported_storage_fails() {
402+
let dir = tempfile::tempdir().expect("tempdir");
403+
let path = dir.path().join("verisimiser.toml");
404+
let sidecar_path = dir.path().join("sidecar.db");
405+
let body = format!(
406+
"[project]\n\
407+
name = \"test\"\n\
408+
[database]\n\
409+
backend = \"sqlite\"\n\
410+
[sidecar]\n\
411+
storage = \"json\"\n\
412+
path = \"{}\"\n",
413+
sidecar_path.display().to_string().replace('\\', "/")
414+
);
415+
std::fs::write(&path, body).expect("write");
416+
417+
let report = validate_manifest(path.to_str().unwrap());
418+
assert!(!report.passed);
419+
let storage = report
420+
.checks
421+
.iter()
422+
.find(|c| c.name == "sidecar-storage-supported")
423+
.expect("storage check must run");
424+
assert!(!storage.passed, "json storage must fail the check");
425+
let detail = storage.detail.as_deref().unwrap_or_default();
426+
assert!(
427+
detail.contains("unsupported") && !detail.contains("#112"),
428+
"detail must reject json plainly without a 'coming soon' pointer; got: {detail}"
429+
);
430+
}
431+
397432
/// A malformed manifest must fail `manifest-loads` and stop further
398433
/// checks (because the rest depend on having a parsed manifest).
399434
#[test]
@@ -621,6 +656,7 @@ enable-constraints = {enable_constraints}
621656
enable-simulation = {enable_simulation}
622657
623658
[sidecar]
659+
# storage backend: "sqlite" (default) or "postgres"/"postgresql"
624660
storage = "{sidecar_storage}"
625661
path = "{sidecar_path}"
626662
@@ -734,6 +770,9 @@ impl ValidationReport {
734770
/// set, the file at that path is readable.
735771
/// 3. **`sidecar-path-writable`** — the parent directory of
736772
/// `[sidecar].path` is writable (or createable).
773+
/// 4. **`sidecar-storage-supported`** — `[sidecar].storage` names a
774+
/// backend `codegen` can emit (`sqlite`/`postgres`). Catches typos and
775+
/// the dropped `json` store (V-L2-F2 / #112).
737776
///
738777
/// Out of scope here: V-L2-E1 backend/target_db conflict (own issue),
739778
/// target-DB reachability (needs live connection).
@@ -820,6 +859,28 @@ pub fn validate_manifest(path: &str) -> ValidationReport {
820859
)),
821860
});
822861
}
862+
863+
// 4. Sidecar storage backend is supported. Delegates to the one
864+
// validator (`SqlDialect::from_storage`) so `validate`/`doctor`
865+
// and `generate` agree on the accepted set. This is where a typo'd
866+
// or dropped backend (e.g. the never-implemented `json` store,
867+
// V-L2-F2 / #112) is surfaced before it reaches codegen.
868+
let storage_check = ValidationCheck {
869+
name: "sidecar-storage-supported".to_string(),
870+
description: "[sidecar].storage names a supported backend".to_string(),
871+
passed: true,
872+
detail: None,
873+
};
874+
checks.push(
875+
match crate::codegen::overlay::SqlDialect::from_storage(&m.sidecar.storage) {
876+
Ok(_) => storage_check,
877+
Err(e) => ValidationCheck {
878+
passed: false,
879+
detail: Some(e.to_string()),
880+
..storage_check
881+
},
882+
},
883+
);
823884
}
824885

825886
let passed = checks.iter().all(|c| c.passed);

0 commit comments

Comments
 (0)