# Data, transacties en versiebeheer ## PostgreSQL als bron van waarheid De database bevat zowel de huidige toestand als de auditgeschiedenis. De browsercache, Undo/Redo-stacks en ingesloten CMaps zijn afgeleide of tijdelijke weergaven en mogen nooit als enige bron van inhoud gelden. | Tabel | Betekenis | | --- | --- | | `users` | Identiteit, profiel, rol, status en wachtwoordhash | | `sessions` | Gehashte sessietokens, CSRF-token en vervaltijd | | `pages` | Actuele pagina, namespace, slug, tags, versieteller en zoekvector | | `page_versions` | Volledige onveranderlijke snapshots van titel, Markdown en tags | | `page_aliases` | Oude adressen die naar dezelfde pagina-id blijven verwijzen | | `todo_items` | Afgeleide index van huidige `todo(...)`-markeringen | | `bookmarks` | Gebruikersspecifieke verwijzingen naar pagina-id's | | `attachments` | Metadata en volledige binaire inhoud van uploads | | `attachment_references` | Huidige en historische verwijzingen vanuit paginaversies | | `concept_maps` | Actuele titel, JSONB-document en versieteller per CMap | | `concept_map_versions` | Expliciete CMap-snapshots en maximaal vijf handmatige opslagversies per CMap | | `password_reset_tokens` | Gehashte, tijdelijke en eenmalige herstelcodes | | `wiki_settings` | Beheerinstellingen, momenteel vooral e-mail | | `wiki_schema` | Geïnstalleerde migratieversie | ## Pagina-identiteit Een pagina wordt intern door `pages.id` geïdentificeerd. Het externe adres is `namespace:slug`, of alleen `slug` in de rootnamespace. Namespace en slug zijn afzonderlijke kolommen en samen uniek. Titelwijziging verandert de slug niet automatisch. Bij een echte adreswijziging blijft het oude adres als alias naar dezelfde id bestaan. Deze scheiding voorkomt dat links bij iedere redactionele titelwijziging breken. Code die relaties bewaart, gebruikt waar mogelijk de id; Markdown en CMaps gebruiken het externe, leesbare paginareferentieformaat. ## Schrijftransactie van een pagina Een paginaopslag is één atomaire transactie: 1. Zoek en vergrendel de actuele pagina met `FOR UPDATE`. 2. Vergelijk `current_version` met de basisversie van de editor. 3. Verhoog de versie en werk de actuele projectie bij. 4. Voeg dezelfde inhoud toe aan `page_versions`. 5. Bouw de todo-index voor deze pagina opnieuw op. 6. Vervang huidige bijlageverwijzingen en leg historische verwijzingen voor de nieuwe versie vast. 7. Commit alles, of niets. Een CMap-opslag volgt voor de actuele projectie hetzelfde kernpatroon: rij vergrendelen, versienummer vergelijken en actuele JSONB bijwerken. Autosave verhoogt wel het optimistische versienummer, maar maakt geen historieregel. Een expliciete opslag voegt een volledige toestand aan `concept_map_versions` toe en verwijdert oudere handmatige versies boven de grens van vijf. Benoemde snapshots vallen niet onder die grens. ## Waarom volledige snapshots Volledige snapshots zijn eenvoudig te begrijpen, herstellen en vergelijken. Er is geen keten van patches nodig om versie 37 te reconstrueren en een defecte diff kan de historie niet onleesbaar maken. Voor de verwachte wikiomvang is deze eenvoud belangrijker dan maximale opslagcompactheid. De prijs is lineaire databasegroei met het aantal versies maal de documentgrootte. Voor pagina's en benoemde CMap-snapshots is dat bewust; CMap-autosaves zijn daarom uitgesloten van historie en handmatige CMap-opslag is begrensd op vijf versies. Zie [Verwachte performance](racket-wiki:performance). ## Afgeleide gegevens `pages.search_document`, `todo_items` en `attachment_references.current_reference` zijn afgeleide gegevens. Zij moeten in dezelfde transactie als hun bron worden aangepast. Een los herstelcommando mag ze opnieuw kunnen opbouwen, maar gewone reads mogen niet afhankelijk zijn van een toevallig later achtergrondproces. De gecombineerde navigatiegraaf is momenteel volledig afgeleid in de browser. Zij wordt niet als databasegraaf bewaard. ## Bijlagen Een upload hoort bij de id van de eigenaarpagina en krijgt een veilige opgeslagen naam. PostgreSQL bewaart zowel metadata als bytes. Markdown verwijst via de pagina en opgeslagen naam. Huidige en historische referenties worden afzonderlijk gevolgd, zodat beheer kan zien of een bestand alleen nog in oude versies voorkomt. Het huidige downloadpad leest de volledige `BYTEA` in geheugen voordat de response wordt gemaakt. Dit is eenvoudig en correct voor gebruikelijke wiki-afbeeldingen en documenten, maar vraagt begrenzing of streaming wanneer grote bestanden een doel worden. ## Migraties `private/migrations.rkt` bevat een oplopende keten. Iedere stap moet herhaalbare SQL gebruiken waar dat nodig is, de nieuwe schemaversie pas na succesvolle omzetting registreren en binnen de omvattende transactie blijven. Oude stappen worden niet achteraf inhoudelijk herschreven, omdat bestaande installaties ze al uitgevoerd kunnen hebben. Een nieuw schema vereist: 1. een nieuwe migratiestap; 2. aanpassing van `migrate-database!`; 3. controles voor een verse database én een upgrade vanaf de vorige versie; 4. documentatie van nieuwe tabellen, kolommen, indexen en herstelgedrag; 5. een back-up- en terugrolnotitie wanneer de omzetting niet triviaal omkeerbaar is. ## Invarianten De volgende regels mogen niet alleen in de browser staan: - iedere actuele paginaversie heeft een overeenkomstige onveranderlijke snapshot; - CMap-autosave wijzigt alleen de actuele projectie; CMap-historie bevat alleen snapshots en maximaal vijf handmatige versies; - versienummers nemen per object strikt toe; - een stale editor overschrijft geen nieuwere toestand; - e-mailadressen zijn hoofdletterongevoelig uniek wanneer ingevuld; - sessies en herstelcodes worden alleen gehasht opgeslagen; - een bijlage die nog vanuit huidige inhoud wordt gebruikt, wordt niet als orphan verwijderd; - een gearchiveerde pagina of CMap verschijnt niet in normale lees- en lijstoperaties. Zie [Beveiliging en vertrouwen](racket-wiki:beveiliging) voor geheimen en [Testbaarheid](racket-wiki:testbaarheid) voor de tests die deze invarianten moeten afdekken.