Skip to content
Merged
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
17 changes: 15 additions & 2 deletions src/infrastructure/persistence/catalog_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import { DatabaseSync } from "node:sqlite";
import { dirname } from "@std/path";
import { ensureDirSync } from "@std/fs";
import { getLogger } from "@logtape/logtape";

const logger = getLogger(["swamp", "persistence", "catalog"]);

/**
* A single row in the catalog table, representing one version of a data
Expand Down Expand Up @@ -559,9 +562,19 @@ export class CatalogStore {
* Must be called outside any open transaction. Acquires an exclusive lock
* and rewrites the entire database file — on a large catalog this may take
* several seconds. Safe to call only when no other connections are active.
*
* Returns `true` if VACUUM succeeded, `false` if it was skipped due to a
* runtime limitation (e.g. SQLITE_LIMIT_ATTACHED=0 in the canary Deno
* runtime).
*/
vacuum(): void {
this.db.exec("VACUUM");
vacuum(): boolean {
try {
this.db.exec("VACUUM");
return true;
} catch (error) {
logger.warn`VACUUM skipped: ${error}`;
return false;
}
}

/**
Expand Down
11 changes: 11 additions & 0 deletions src/infrastructure/persistence/catalog_store_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,17 @@ Deno.test("CatalogStore: checkpoint returns WAL page counts and truncates WAL",
store.close();
});

Deno.test("CatalogStore: vacuum returns boolean and does not throw", () => {
const dbPath = makeTempDbPath();
const store = new CatalogStore(dbPath);
store.upsert(makeRow());

const result = store.vacuum();
assertEquals(typeof result, "boolean");

store.close();
});

Deno.test("CatalogStore: invalidate clears populated flag but keeps data", () => {
const dbPath = makeTempDbPath();
const store = new CatalogStore(dbPath);
Expand Down
7 changes: 5 additions & 2 deletions src/libswamp/datastores/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface DatastoreCompactData {
walPagesCheckpointed: number;
/** Bytes reclaimed from the main database file after VACUUM. */
dbBytesReclaimed: number;
/** True when VACUUM was skipped due to a runtime limitation. */
vacuumSkipped: boolean;
}

export type DatastoreCompactEvent =
Expand All @@ -38,7 +40,7 @@ export type DatastoreCompactEvent =
/** Dependencies for the datastore compact operation. */
export interface DatastoreCompactDeps {
checkpoint: () => { walPagesTotal: number; walPagesCheckpointed: number };
vacuum: () => void;
vacuum: () => boolean;
catalogDbSize: () => Promise<number>;
}

Expand All @@ -58,7 +60,7 @@ export async function* datastoreCompact(
const beforeSize = await deps.catalogDbSize();

yield { kind: "vacuuming" } as const;
deps.vacuum();
const vacuumed = deps.vacuum();
const afterSize = await deps.catalogDbSize();

yield {
Expand All @@ -67,6 +69,7 @@ export async function* datastoreCompact(
walPagesTotal: stats.walPagesTotal,
walPagesCheckpointed: stats.walPagesCheckpointed,
dbBytesReclaimed: Math.max(0, beforeSize - afterSize),
vacuumSkipped: !vacuumed,
},
};
})(),
Expand Down
19 changes: 18 additions & 1 deletion src/libswamp/datastores/compact_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function makeDeps(
): DatastoreCompactDeps {
return {
checkpoint: () => ({ walPagesTotal: 10, walPagesCheckpointed: 10 }),
vacuum: () => {},
vacuum: () => true,
catalogDbSize: () => Promise.resolve(0),
...overrides,
};
Expand Down Expand Up @@ -102,5 +102,22 @@ Deno.test("datastoreCompact: dbBytesReclaimed is 0 when db size does not decreas
const completed = events.find((e) => e.kind === "completed");
if (completed?.kind === "completed") {
assertEquals(completed.data.dbBytesReclaimed, 0);
assertEquals(completed.data.vacuumSkipped, false);
}
});

Deno.test("datastoreCompact: reports vacuumSkipped when vacuum returns false", async () => {
await initializeLogging({});
const ctx = createLibSwampContext({});
const deps = makeDeps({
vacuum: () => false,
});

const events = await collectEvents(datastoreCompact(ctx, deps));
const completed = events.find((e) => e.kind === "completed");
assertEquals(completed?.kind, "completed");
if (completed?.kind === "completed") {
assertEquals(completed.data.vacuumSkipped, true);
assertEquals(completed.data.dbBytesReclaimed, 0);
}
});
6 changes: 5 additions & 1 deletion src/presentation/renderers/datastore_compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ class LogDatastoreCompactRenderer implements Renderer<DatastoreCompactEvent> {
} else {
logger.info`WAL checkpointed and truncated`;
}
if (e.data.dbBytesReclaimed > 0) {
if (e.data.vacuumSkipped) {
logger
.warn`VACUUM skipped (runtime limitation) — WAL checkpoint still reclaimed space`;
} else if (e.data.dbBytesReclaimed > 0) {
logger
.info`Catalog compacted: reclaimed ${e.data.dbBytesReclaimed} bytes`;
} else {
Expand All @@ -72,6 +75,7 @@ class JsonDatastoreCompactRenderer implements Renderer<DatastoreCompactEvent> {
walPagesTotal: e.data.walPagesTotal,
walPagesCheckpointed: e.data.walPagesCheckpointed,
dbBytesReclaimed: e.data.dbBytesReclaimed,
vacuumSkipped: e.data.vacuumSkipped,
},
null,
2,
Expand Down
Loading