···139139140140export function deleteNoteByAtUri(atUri: string): void {
141141 const db = getDb();
142142+ // Cascades handle revisions, snapshots, current_note, and blobs.
143143+ // Backlinks use source_note_uri (not a FK), so delete explicitly.
142144 db.transaction(() => {
143143- db.run("DELETE FROM current_note WHERE note_at_uri = ?", [atUri]);
144144- db.run("DELETE FROM revisions WHERE note_at_uri = ?", [atUri]);
145145- db.run("DELETE FROM snapshots WHERE note_at_uri = ?", [atUri]);
146145 db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [atUri]);
147146 db.run("DELETE FROM notes WHERE at_uri = ?", [atUri]);
148147 })();
+6-6
src/server/db/queries/wiki.ts
···11import { escapeLikePattern } from "../../../lib/html.ts";
22import { getDb } from "../index.ts";
33import type { WikiRow } from "../types.ts";
44-import { deleteNoteByAtUri } from "./note.ts";
5465export interface WikiWithNoteCount extends WikiRow {
76 note_count: number;
···149148 .get(atUri) as { slug: string } | null;
150149 if (!wiki) return;
151150151151+ // Backlinks use source_note_uri (not a FK target), so clean up before cascade.
152152+ // Deleting the wiki cascades to notes, revisions, snapshots, current_note,
153153+ // blobs, memberships, requests, and backlinks (via wiki_slug FK).
152154 db.transaction(() => {
153153- const notes = db
155155+ const noteUris = db
154156 .query("SELECT at_uri FROM notes WHERE wiki_slug = ?")
155157 .all(wiki.slug) as { at_uri: string }[];
156156- for (const note of notes) {
157157- deleteNoteByAtUri(note.at_uri);
158158+ for (const n of noteUris) {
159159+ db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [n.at_uri]);
158160 }
159159- db.run("DELETE FROM memberships WHERE wiki_slug = ?", [wiki.slug]);
160160- db.run("DELETE FROM requests WHERE wiki_slug = ?", [wiki.slug]);
161161 db.run("DELETE FROM wikis WHERE slug = ?", [wiki.slug]);
162162 })();
163163}
+9-11
src/server/db/schema.ts
···33export function initSchema(db: Database): void {
44 db.run("PRAGMA journal_mode = WAL");
55 db.run("PRAGMA foreign_keys = ON");
66+ db.run("PRAGMA busy_timeout = 5000");
6778 db.run(`
89 CREATE TABLE IF NOT EXISTS wikis (
···2122 db.run(`
2223 CREATE TABLE IF NOT EXISTS notes (
2324 slug TEXT NOT NULL,
2424- wiki_slug TEXT NOT NULL REFERENCES wikis(slug),
2525+ wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE,
2526 title TEXT NOT NULL,
2627 did TEXT NOT NULL,
2728 at_uri TEXT NOT NULL UNIQUE,
···3334 db.run(`
3435 CREATE TABLE IF NOT EXISTS revisions (
3536 id INTEGER PRIMARY KEY AUTOINCREMENT,
3636- note_at_uri TEXT NOT NULL,
3737+ note_at_uri TEXT NOT NULL REFERENCES notes(at_uri) ON DELETE CASCADE,
3738 did TEXT NOT NULL,
3839 at_uri TEXT NOT NULL UNIQUE,
3940 parent_revision_uri TEXT,
···4748 db.run(`
4849 CREATE TABLE IF NOT EXISTS snapshots (
4950 id INTEGER PRIMARY KEY AUTOINCREMENT,
5050- note_at_uri TEXT NOT NULL,
5151+ note_at_uri TEXT NOT NULL REFERENCES notes(at_uri) ON DELETE CASCADE,
5152 revision_at_uri TEXT NOT NULL,
5253 content TEXT NOT NULL,
5354 created_at TEXT NOT NULL DEFAULT (datetime('now'))
···56575758 db.run(`
5859 CREATE TABLE IF NOT EXISTS current_note (
5959- note_at_uri TEXT PRIMARY KEY,
6060+ note_at_uri TEXT PRIMARY KEY REFERENCES notes(at_uri) ON DELETE CASCADE,
6061 content TEXT NOT NULL,
6162 latest_revision_uri TEXT NOT NULL,
6263 updated_at TEXT NOT NULL DEFAULT (datetime('now'))
···6667 db.run(`
6768 CREATE TABLE IF NOT EXISTS memberships (
6869 id INTEGER PRIMARY KEY AUTOINCREMENT,
6969- wiki_slug TEXT NOT NULL REFERENCES wikis(slug),
7070+ wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE,
7071 did TEXT NOT NULL,
7172 role TEXT NOT NULL CHECK (role IN ('admin', 'contributor', 'viewer')),
7273 at_uri TEXT NOT NULL UNIQUE,
···7879 db.run(`
7980 CREATE TABLE IF NOT EXISTS requests (
8081 id INTEGER PRIMARY KEY AUTOINCREMENT,
8181- wiki_slug TEXT NOT NULL REFERENCES wikis(slug),
8282+ wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE,
8283 did TEXT NOT NULL,
8384 at_uri TEXT NOT NULL UNIQUE,
8485 created_at TEXT NOT NULL DEFAULT (datetime('now')),
···101102 CREATE TABLE IF NOT EXISTS backlinks (
102103 source_note_uri TEXT NOT NULL,
103104 target_note_slug TEXT NOT NULL,
104104- wiki_slug TEXT NOT NULL,
105105+ wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE,
105106 PRIMARY KEY (source_note_uri, target_note_slug)
106107 )
107108 `);
···109110 db.run(`
110111 CREATE TABLE IF NOT EXISTS blobs (
111112 cid TEXT PRIMARY KEY,
112112- revision_at_uri TEXT NOT NULL,
113113+ revision_at_uri TEXT NOT NULL REFERENCES revisions(at_uri) ON DELETE CASCADE,
113114 mime_type TEXT NOT NULL,
114115 storage_key TEXT NOT NULL,
115116 created_at TEXT NOT NULL DEFAULT (datetime('now'))
···164165 db.run("CREATE INDEX IF NOT EXISTS idx_requests_wiki ON requests(wiki_slug)");
165166 db.run(
166167 "CREATE INDEX IF NOT EXISTS idx_backlinks_target ON backlinks(wiki_slug, target_note_slug)",
167167- );
168168- db.run(
169169- "CREATE INDEX IF NOT EXISTS idx_blobs_revision ON blobs(revision_at_uri)",
170168 );
171169 db.run("CREATE INDEX IF NOT EXISTS idx_bookmarks_did ON bookmarks(did)");
172170 db.run("CREATE INDEX IF NOT EXISTS idx_wikis_did ON wikis(did)");
+10-14
tests/helpers/cleanup.ts
···3344const db = getDb();
5566-function deleteNoteDependents(noteAtUris: { at_uri: string }[]): void {
77- for (const note of noteAtUris) {
88- db.run("DELETE FROM current_note WHERE note_at_uri = ?", [note.at_uri]);
99- db.run("DELETE FROM revisions WHERE note_at_uri = ?", [note.at_uri]);
1010- db.run("DELETE FROM snapshots WHERE note_at_uri = ?", [note.at_uri]);
1111- db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [note.at_uri]);
1212- }
1313-}
1414-156export function cleanupWikiAndDependents(slug: string): void {
77+ // Backlinks use source_note_uri which isn't a FK target, clean up first.
168 const notes = db
179 .query("SELECT at_uri FROM notes WHERE wiki_slug = ?")
1810 .all(slug) as { at_uri: string }[];
1919- deleteNoteDependents(notes);
2020- db.run("DELETE FROM notes WHERE wiki_slug = ?", [slug]);
2121- db.run("DELETE FROM memberships WHERE wiki_slug = ?", [slug]);
2222- db.run("DELETE FROM requests WHERE wiki_slug = ?", [slug]);
1111+ for (const note of notes) {
1212+ db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [note.at_uri]);
1313+ }
1414+ // Deleting the wiki cascades to notes, revisions, snapshots, current_note,
1515+ // blobs, memberships, requests, and backlinks (via wiki_slug FK).
2316 db.run("DELETE FROM wikis WHERE slug = ?", [slug]);
2417}
2518···2720 const notes = db
2821 .query("SELECT at_uri FROM notes WHERE wiki_slug = ? AND slug GLOB ?")
2922 .all(wikiSlug, slugGlob) as { at_uri: string }[];
3030- deleteNoteDependents(notes);
2323+ for (const note of notes) {
2424+ db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [note.at_uri]);
2525+ }
2626+ // Deleting notes cascades to revisions, snapshots, current_note, blobs.
3127 db.run("DELETE FROM notes WHERE wiki_slug = ? AND slug GLOB ?", [
3228 wikiSlug,
3329 slugGlob,