diff --git a/src/server/db/queries/note.ts b/src/server/db/queries/note.ts index ba8c43e..b25457f 100644 --- a/src/server/db/queries/note.ts +++ b/src/server/db/queries/note.ts @@ -139,10 +139,9 @@ export function upsertNote( export function deleteNoteByAtUri(atUri: string): void { const db = getDb(); + // Cascades handle revisions, snapshots, current_note, and blobs. + // Backlinks use source_note_uri (not a FK), so delete explicitly. db.transaction(() => { - db.run("DELETE FROM current_note WHERE note_at_uri = ?", [atUri]); - db.run("DELETE FROM revisions WHERE note_at_uri = ?", [atUri]); - db.run("DELETE FROM snapshots WHERE note_at_uri = ?", [atUri]); db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [atUri]); db.run("DELETE FROM notes WHERE at_uri = ?", [atUri]); })(); diff --git a/src/server/db/queries/wiki.ts b/src/server/db/queries/wiki.ts index ee0f02d..40e81f0 100644 --- a/src/server/db/queries/wiki.ts +++ b/src/server/db/queries/wiki.ts @@ -1,7 +1,6 @@ import { escapeLikePattern } from "../../../lib/html.ts"; import { getDb } from "../index.ts"; import type { WikiRow } from "../types.ts"; -import { deleteNoteByAtUri } from "./note.ts"; export interface WikiWithNoteCount extends WikiRow { note_count: number; @@ -149,15 +148,16 @@ export function deleteWikiByAtUri(atUri: string): void { .get(atUri) as { slug: string } | null; if (!wiki) return; + // Backlinks use source_note_uri (not a FK target), so clean up before cascade. + // Deleting the wiki cascades to notes, revisions, snapshots, current_note, + // blobs, memberships, requests, and backlinks (via wiki_slug FK). db.transaction(() => { - const notes = db + const noteUris = db .query("SELECT at_uri FROM notes WHERE wiki_slug = ?") .all(wiki.slug) as { at_uri: string }[]; - for (const note of notes) { - deleteNoteByAtUri(note.at_uri); + for (const n of noteUris) { + db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [n.at_uri]); } - db.run("DELETE FROM memberships WHERE wiki_slug = ?", [wiki.slug]); - db.run("DELETE FROM requests WHERE wiki_slug = ?", [wiki.slug]); db.run("DELETE FROM wikis WHERE slug = ?", [wiki.slug]); })(); } diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 81c1e4c..0a7624a 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -3,6 +3,7 @@ import type { Database } from "bun:sqlite"; export function initSchema(db: Database): void { db.run("PRAGMA journal_mode = WAL"); db.run("PRAGMA foreign_keys = ON"); + db.run("PRAGMA busy_timeout = 5000"); db.run(` CREATE TABLE IF NOT EXISTS wikis ( @@ -21,7 +22,7 @@ export function initSchema(db: Database): void { db.run(` CREATE TABLE IF NOT EXISTS notes ( slug TEXT NOT NULL, - wiki_slug TEXT NOT NULL REFERENCES wikis(slug), + wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE, title TEXT NOT NULL, did TEXT NOT NULL, at_uri TEXT NOT NULL UNIQUE, @@ -33,7 +34,7 @@ export function initSchema(db: Database): void { db.run(` CREATE TABLE IF NOT EXISTS revisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - note_at_uri TEXT NOT NULL, + note_at_uri TEXT NOT NULL REFERENCES notes(at_uri) ON DELETE CASCADE, did TEXT NOT NULL, at_uri TEXT NOT NULL UNIQUE, parent_revision_uri TEXT, @@ -47,7 +48,7 @@ export function initSchema(db: Database): void { db.run(` CREATE TABLE IF NOT EXISTS snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, - note_at_uri TEXT NOT NULL, + note_at_uri TEXT NOT NULL REFERENCES notes(at_uri) ON DELETE CASCADE, revision_at_uri TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) @@ -56,7 +57,7 @@ export function initSchema(db: Database): void { db.run(` CREATE TABLE IF NOT EXISTS current_note ( - note_at_uri TEXT PRIMARY KEY, + note_at_uri TEXT PRIMARY KEY REFERENCES notes(at_uri) ON DELETE CASCADE, content TEXT NOT NULL, latest_revision_uri TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')) @@ -66,7 +67,7 @@ export function initSchema(db: Database): void { db.run(` CREATE TABLE IF NOT EXISTS memberships ( id INTEGER PRIMARY KEY AUTOINCREMENT, - wiki_slug TEXT NOT NULL REFERENCES wikis(slug), + wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE, did TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN ('admin', 'contributor', 'viewer')), at_uri TEXT NOT NULL UNIQUE, @@ -78,7 +79,7 @@ export function initSchema(db: Database): void { db.run(` CREATE TABLE IF NOT EXISTS requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, - wiki_slug TEXT NOT NULL REFERENCES wikis(slug), + wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE, did TEXT NOT NULL, at_uri TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL DEFAULT (datetime('now')), @@ -101,7 +102,7 @@ export function initSchema(db: Database): void { CREATE TABLE IF NOT EXISTS backlinks ( source_note_uri TEXT NOT NULL, target_note_slug TEXT NOT NULL, - wiki_slug TEXT NOT NULL, + wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE, PRIMARY KEY (source_note_uri, target_note_slug) ) `); @@ -109,7 +110,7 @@ export function initSchema(db: Database): void { db.run(` CREATE TABLE IF NOT EXISTS blobs ( cid TEXT PRIMARY KEY, - revision_at_uri TEXT NOT NULL, + revision_at_uri TEXT NOT NULL REFERENCES revisions(at_uri) ON DELETE CASCADE, mime_type TEXT NOT NULL, storage_key TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) @@ -165,9 +166,6 @@ export function initSchema(db: Database): void { db.run( "CREATE INDEX IF NOT EXISTS idx_backlinks_target ON backlinks(wiki_slug, target_note_slug)", ); - db.run( - "CREATE INDEX IF NOT EXISTS idx_blobs_revision ON blobs(revision_at_uri)", - ); db.run("CREATE INDEX IF NOT EXISTS idx_bookmarks_did ON bookmarks(did)"); db.run("CREATE INDEX IF NOT EXISTS idx_wikis_did ON wikis(did)"); } diff --git a/tests/helpers/cleanup.ts b/tests/helpers/cleanup.ts index 4d00015..67c2926 100644 --- a/tests/helpers/cleanup.ts +++ b/tests/helpers/cleanup.ts @@ -3,23 +3,16 @@ import { upsertWiki } from "../../src/server/db/queries/index.ts"; const db = getDb(); -function deleteNoteDependents(noteAtUris: { at_uri: string }[]): void { - for (const note of noteAtUris) { - db.run("DELETE FROM current_note WHERE note_at_uri = ?", [note.at_uri]); - db.run("DELETE FROM revisions WHERE note_at_uri = ?", [note.at_uri]); - db.run("DELETE FROM snapshots WHERE note_at_uri = ?", [note.at_uri]); - db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [note.at_uri]); - } -} - export function cleanupWikiAndDependents(slug: string): void { + // Backlinks use source_note_uri which isn't a FK target, clean up first. const notes = db .query("SELECT at_uri FROM notes WHERE wiki_slug = ?") .all(slug) as { at_uri: string }[]; - deleteNoteDependents(notes); - db.run("DELETE FROM notes WHERE wiki_slug = ?", [slug]); - db.run("DELETE FROM memberships WHERE wiki_slug = ?", [slug]); - db.run("DELETE FROM requests WHERE wiki_slug = ?", [slug]); + for (const note of notes) { + db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [note.at_uri]); + } + // Deleting the wiki cascades to notes, revisions, snapshots, current_note, + // blobs, memberships, requests, and backlinks (via wiki_slug FK). db.run("DELETE FROM wikis WHERE slug = ?", [slug]); } @@ -27,7 +20,10 @@ export function cleanupNotes(wikiSlug: string, slugGlob: string): void { const notes = db .query("SELECT at_uri FROM notes WHERE wiki_slug = ? AND slug GLOB ?") .all(wikiSlug, slugGlob) as { at_uri: string }[]; - deleteNoteDependents(notes); + for (const note of notes) { + db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [note.at_uri]); + } + // Deleting notes cascades to revisions, snapshots, current_note, blobs. db.run("DELETE FROM notes WHERE wiki_slug = ? AND slug GLOB ?", [ wikiSlug, slugGlob, diff --git a/tests/server/db/queries/blob.test.ts b/tests/server/db/queries/blob.test.ts index b1c3d23..108c0ca 100644 --- a/tests/server/db/queries/blob.test.ts +++ b/tests/server/db/queries/blob.test.ts @@ -1,4 +1,4 @@ -import { afterAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { getDb } from "../../../../src/server/db/index.ts"; import { getBlobsByCids, @@ -7,6 +7,13 @@ import { const db = getDb(); +const TEST_DID = "did:plc:blobtest"; +const WIKI_SLUG = "blob-test-wiki"; +const WIKI_AT_URI = `at://${TEST_DID}/wiki.lichen.wiki/${WIKI_SLUG}`; +const NOTE_AT_URI = `at://${TEST_DID}/wiki.lichen.note/note1`; +const REV_URI_1 = `at://${TEST_DID}/wiki.lichen.noteRevision/rev1`; +const REV_URI_2 = `at://${TEST_DID}/wiki.lichen.noteRevision/rev2`; + const TEST_CIDS = [ "bafytest-blob-1", "bafytest-blob-2", @@ -14,17 +21,37 @@ const TEST_CIDS = [ "bafytest-blob-dup", ]; +beforeAll(() => { + db.run( + "INSERT INTO wikis (slug, did, name, visibility, at_uri) VALUES (?, ?, ?, ?, ?)", + [WIKI_SLUG, TEST_DID, "Blob Test Wiki", "public", WIKI_AT_URI], + ); + db.run( + "INSERT INTO notes (slug, wiki_slug, title, did, at_uri) VALUES (?, ?, ?, ?, ?)", + ["note1", WIKI_SLUG, "Test Note", TEST_DID, NOTE_AT_URI], + ); + db.run( + "INSERT INTO revisions (note_at_uri, did, at_uri, diff) VALUES (?, ?, ?, ?)", + [NOTE_AT_URI, TEST_DID, REV_URI_1, ""], + ); + db.run( + "INSERT INTO revisions (note_at_uri, did, at_uri, diff) VALUES (?, ?, ?, ?)", + [NOTE_AT_URI, TEST_DID, REV_URI_2, ""], + ); +}); + afterAll(() => { for (const cid of TEST_CIDS) { db.run("DELETE FROM blobs WHERE cid = ?", [cid]); } + db.run("DELETE FROM wikis WHERE slug = ?", [WIKI_SLUG]); }); describe("insertBlob", () => { test("inserts a blob row", () => { insertBlob( "bafytest-blob-1", - "at://did:plc:test/wiki.lichen.noteRevision/abc", + REV_URI_1, "image/png", "data/blobs/test1.png", ); @@ -40,14 +67,14 @@ describe("insertBlob", () => { test("INSERT OR IGNORE on duplicate CID", () => { insertBlob( "bafytest-blob-dup", - "at://did:plc:test/wiki.lichen.noteRevision/first", + REV_URI_1, "image/jpeg", "data/blobs/first.jpg", ); - // Insert again with different values — should be ignored + // Insert again with different values -- should be ignored insertBlob( "bafytest-blob-dup", - "at://did:plc:test/wiki.lichen.noteRevision/second", + REV_URI_2, "image/png", "data/blobs/second.png", ); @@ -69,13 +96,13 @@ describe("getBlobsByCids", () => { test("returns matching blobs", () => { insertBlob( "bafytest-blob-2", - "at://did:plc:test/wiki.lichen.noteRevision/xyz", + REV_URI_1, "image/webp", "data/blobs/test2.webp", ); insertBlob( "bafytest-blob-3", - "at://did:plc:test/wiki.lichen.noteRevision/xyz", + REV_URI_1, "image/gif", "data/blobs/test3.gif", );