Files
Aether/backend/db/migrations/000014_document_text.up.sql
HoneyandClaude Opus 5 31a027c93a feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA.
Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb,
Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming,
Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office
(Collabora/OnlyOffice).

Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort)
sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 09:25:36 +02:00

64 lines
3.8 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Volltext über den *Inhalt* – die zweite Hälfte der Suche.
--
-- Migration 000013 machte Datei*namen* durchsuchbar. Das reicht, solange
-- Dateien sprechende Namen haben. Ein Scanner nennt seine Ablage aber
-- "scan_0042.pdf", und genau die Belege sollen auffindbar werden.
-- ── Der erkannte Text ─────────────────────────────────────────────────────────
-- Eigene Tabelle statt einer Spalte an files: Der Text ist um Größenordnungen
-- länger als der Rest der Zeile, und files wird bei jedem Auflisten gelesen.
-- Postgres lagert lange Werte zwar aus (TOAST), aber die tsvector-Spalte und
-- ihr GIN-Index gehörten dann trotzdem an eine Tabelle, die ständig in
-- Bewegung ist.
CREATE TABLE file_text (
file_id UUID PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
content TEXT NOT NULL,
-- Woher der Text stammt. 'layer' = das PDF hatte bereits eine Textebene
-- (pdftotext, verlustfrei); 'ocr' = aus dem Bild erkannt, kann Fehler
-- enthalten. Der Unterschied ist für die Fehlersuche wichtig: Findet die
-- Suche ein Wort nicht, sagt die Herkunft, ob OCR es verlesen hat.
source TEXT NOT NULL CHECK (source IN ('layer', 'ocr')),
-- Die Sprachen, mit denen erkannt wurde ("deu+eng"). Bei 'layer' leer.
lang TEXT NOT NULL DEFAULT '',
-- Zeichenzahl des Originals vor dem Kürzen – siehe maxTextBytes im Worker.
chars INTEGER NOT NULL DEFAULT 0,
pages INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Berechnete Spalte statt eines Triggers: Sie kann nie mit dem Inhalt
-- auseinanderlaufen, weil Postgres sie bei jedem Schreiben selbst füllt.
--
-- 'german' fest verdrahtet, nicht 'simple': Ohne Stammformen fände
-- "Rechnung" die "Rechnungen" nicht. Deutsch ist die Sprache der Belege,
-- um die es hier geht; englische Wörter darin verlieren nur ihre
-- Stammformbildung, sie bleiben wörtlich auffindbar. Eine Änderung wäre
-- eine Migration – der Preis dafür, dass der Ausdruck IMMUTABLE sein muss.
tsv tsvector GENERATED ALWAYS AS (to_tsvector('german', content)) STORED
);
CREATE INDEX idx_file_text_tsv ON file_text USING gin (tsv);
-- ── Die Warteschlange ─────────────────────────────────────────────────────────
-- Warum eine Tabelle und nicht einfach ein Goroutine-Aufruf wie bei den
-- Vorschaubildern: OCR dauert Sekunden bis Minuten. Startet der Server in
-- dieser Zeit neu – Update, Absturz, Neustart des Wirts –, ist ein Auftrag im
-- Speicher verloren, und die Datei bleibt für immer unauffindbar, ohne dass es
-- jemand merkt. Bei einem Vorschaubild fällt das auf; bei fehlendem Text nicht.
CREATE TABLE ocr_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Ein Auftrag je Datei. Wird dieselbe Datei erneut hochgeladen, ersetzt der
-- neue Auftrag den alten (ON CONFLICT), statt sich daneben zu stapeln.
file_id UUID NOT NULL UNIQUE REFERENCES files(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'running', 'done', 'failed')),
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Der Arbeiter fragt nur nach offenen Aufträgen. Ein Teilindex bleibt klein,
-- auch wenn sich zehntausend erledigte Zeilen ansammeln.
CREATE INDEX idx_ocr_jobs_pending ON ocr_jobs(created_at)
WHERE status IN ('pending', 'running');