@@ -39,19 +39,52 @@ impl GcReport {
3939}
4040
4141/// Purge sidecar rows older than the retention bound. `dry_run = true`
42- /// reports what would be deleted without changing the DB .
42+ /// reports what would be deleted without changing the store .
4343///
44- /// Returns `Err` if the sidecar storage is not SQLite (unsupported in
45- /// this cut) or if the file is unreachable.
44+ /// Dispatches on the resolved [`StorageKind`](crate::sidecar::StorageKind):
45+ /// the `sqlite` and `json` (plain/ld/ndjson) backends are implemented;
46+ /// `postgres` gc is not yet. Returns `Err` for an unsupported storage or an
47+ /// unreachable store.
4648pub fn run_gc ( manifest : & Manifest , dry_run : bool ) -> Result < GcReport > {
47- if manifest. sidecar . storage != "sqlite" {
48- bail ! (
49- "verisimiser gc currently only supports the SQLite sidecar backend; \
50- [sidecar].storage is {:?}",
51- manifest. sidecar. storage
52- ) ;
49+ use crate :: sidecar:: StorageKind ;
50+ match StorageKind :: resolve ( & manifest. sidecar . storage , & manifest. sidecar . format ) ? {
51+ StorageKind :: Sqlite => run_gc_sqlite ( manifest, dry_run) ,
52+ StorageKind :: Json ( format) => run_gc_json ( manifest, dry_run, format) ,
53+ StorageKind :: Postgres => bail ! (
54+ "verisimiser gc supports the sqlite and json sidecar backends; \
55+ gc for [sidecar].storage = \" postgres\" is not yet implemented"
56+ ) ,
5357 }
58+ }
5459
60+ /// JSON-family gc: load the store, purge in memory, persist iff applying.
61+ /// The per-dimension semantics (incl. keeping the current temporal version)
62+ /// live in [`crate::sidecar::json::JsonStore::gc_purge`].
63+ fn run_gc_json (
64+ manifest : & Manifest ,
65+ dry_run : bool ,
66+ format : crate :: sidecar:: JsonFormat ,
67+ ) -> Result < GcReport > {
68+ let sidecar_path = & manifest. sidecar . path ;
69+ let mut store = crate :: sidecar:: json:: JsonStore :: open ( sidecar_path, format)
70+ . with_context ( || format ! ( "opening json sidecar at {}" , sidecar_path) ) ?;
71+ let counts = store. gc_purge ( & manifest. retention , dry_run) ;
72+ if !dry_run {
73+ store
74+ . save ( )
75+ . with_context ( || format ! ( "saving json sidecar at {}" , sidecar_path) ) ?;
76+ }
77+ Ok ( GcReport {
78+ sidecar : sidecar_path. clone ( ) ,
79+ dry_run,
80+ provenance_deleted : counts. provenance ,
81+ temporal_deleted : counts. temporal ,
82+ lineage_deleted : counts. lineage ,
83+ } )
84+ }
85+
86+ /// SQLite gc (the reference path).
87+ fn run_gc_sqlite ( manifest : & Manifest , dry_run : bool ) -> Result < GcReport > {
5588 let sidecar_path = & manifest. sidecar . path ;
5689 let conn = Connection :: open ( sidecar_path)
5790 . with_context ( || format ! ( "opening sidecar at {}" , sidecar_path) ) ?;
@@ -148,6 +181,7 @@ mod tests {
148181 . unwrap ( ) ;
149182 m. sidecar = SidecarConfig {
150183 storage : storage. to_string ( ) ,
184+ format : "plain" . to_string ( ) ,
151185 path : sidecar_path. to_string ( ) ,
152186 } ;
153187 m. retention = retention;
@@ -308,15 +342,67 @@ mod tests {
308342 }
309343
310344 #[ test]
311- fn gc_rejects_non_sqlite_backend ( ) {
312- // `postgres` is a valid generate-time dialect, but gc is SQLite-only
313- // and must refuse rather than silently no-op. (The `json` value was
314- // dropped as a storage option entirely in V-L2-F2 / #112 .)
345+ fn gc_rejects_postgres_backend ( ) {
346+ // `postgres` is a valid generate-time dialect, but gc is not yet
347+ // implemented for it and must refuse rather than silently no-op.
348+ // (The json family *is* now supported — see the json gc test below .)
315349 let m = fixture ( "/dev/null" , RetentionConfig :: default ( ) , "postgres" ) ;
316350 let err = run_gc ( & m, true ) . unwrap_err ( ) ;
317351 assert ! (
318- err. to_string( ) . contains( "only supports the SQLite sidecar" ) ,
319- "expected explicit unsupported-backend error; got: {err}"
352+ err. to_string( ) . contains( "not yet implemented" ) ,
353+ "expected explicit postgres-unsupported error; got: {err}"
354+ ) ;
355+ }
356+
357+ #[ test]
358+ fn gc_json_backend_purges_old_rows_and_persists ( ) {
359+ use crate :: sidecar:: JsonFormat ;
360+ use crate :: sidecar:: json:: { JsonStore , ProvenanceRow , SidecarData , encode} ;
361+
362+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
363+ let sidecar = dir. path ( ) . join ( "sidecar.json" ) ;
364+ let sidecar_str = sidecar. to_str ( ) . unwrap ( ) ;
365+
366+ // Seed one aged + one fresh provenance row directly (deterministic
367+ // timestamps; the append API always stamps "now").
368+ let aged = ProvenanceRow {
369+ hash : "old" . into ( ) ,
370+ previous_hash : String :: new ( ) ,
371+ entity_id : "e" . into ( ) ,
372+ table_name : "t" . into ( ) ,
373+ operation : "insert" . into ( ) ,
374+ actor : "a" . into ( ) ,
375+ timestamp : "2020-01-01T00:00:00+00:00" . into ( ) ,
376+ before_snapshot : None ,
377+ transformation : None ,
378+ } ;
379+ let fresh = ProvenanceRow {
380+ hash : "new" . into ( ) ,
381+ timestamp : "9999-01-01T00:00:00+00:00" . into ( ) ,
382+ ..aged. clone ( )
383+ } ;
384+ let data = SidecarData {
385+ provenance_log : vec ! [ aged, fresh] ,
386+ ..Default :: default ( )
387+ } ;
388+ std:: fs:: write ( & sidecar, encode ( & data, JsonFormat :: Plain ) . unwrap ( ) ) . unwrap ( ) ;
389+
390+ let m = fixture (
391+ sidecar_str,
392+ RetentionConfig {
393+ provenance_days : 30 ,
394+ temporal_days : 30 ,
395+ lineage_days : 30 ,
396+ } ,
397+ "json" ,
320398 ) ;
399+ let report = run_gc ( & m, false ) . unwrap ( ) ;
400+ assert_eq ! ( report. provenance_deleted, 1 , "old provenance row purged" ) ;
401+ assert_eq ! ( report. total( ) , 1 ) ;
402+
403+ // The purge was persisted: reopening shows only the fresh row.
404+ let reopened = JsonStore :: open ( & sidecar, JsonFormat :: Plain ) . unwrap ( ) ;
405+ assert_eq ! ( reopened. data( ) . provenance_log. len( ) , 1 ) ;
406+ assert_eq ! ( reopened. data( ) . provenance_log[ 0 ] . hash, "new" ) ;
321407 }
322408}
0 commit comments