Document WCX project split analysis

This commit is contained in:
2026-07-19 15:54:17 +02:00
parent 40008906ba
commit 2fcd9168b2
3 changed files with 1128 additions and 0 deletions

View File

@ -0,0 +1,462 @@
# Gap-analys för reproducerbart facitdatabasschema
## 1. Omfattning och metod
Analysen är avgränsad till om facitdatabasens struktur kan återskapas från
versionshanterade filer. Följande har inspekterats:
- `docs/project-split-analysis.md` och
`docs/project-split-analysis-summary.md`,
- `scripts/schema.sql`,
- databasberoendena i `import_site.py`, `migrate_csv.py`, `check_ocr.py`,
`process_pending_ocr.py`, `match_filenames.py` och
`test_duration_matching.py`,
- `sqlite_schema`, `PRAGMA table_xinfo`, `PRAGMA foreign_key_list`,
`PRAGMA index_list` och `PRAGMA index_xinfo` i
`/storage/disk1/WCX/database/wcx.db`, öppnad med `sqlite3 -readonly`, och
- resultatet av att läsa `scripts/schema.sql` i en separat SQLite-databas i
minnet.
**Verifierat faktum:** Produktionsdatabasen öppnades endast med
`sqlite3 -readonly`. Ingen kontroll eller verifiering skrev till den.
**Verifierat faktum:** `PRAGMA integrity_check` gav `ok` och
`PRAGMA foreign_key_check` gav inga rader. Både `PRAGMA user_version` och
`PRAGMA application_id` är `0`.
**Antagande:** Schemat som finns i den inspekterade `wcx.db` är den avsedda
produktionsmodellen. Analysen kan verifiera faktisk struktur och kodberoenden,
men inte om varje historiskt schemaingrepp var avsiktligt.
## 2. Fullständigt faktiskt produktionsschema
### 2.1 Tabeller
**Verifierat faktum:** Följande användardefinierade tabeller finns i
produktionen. SQL-definitionerna nedan är hämtade från `sqlite_schema`. Den
kompakta placeringen av OCR-kolumnerna på samma rad som `modified_at` är bara
hur SQLite lagrade den stegvis ändrade tabellens SQL-text; den ändrar inte
kolumnernas semantik.
```sql
CREATE TABLE movie (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
aka TEXT,
nationality TEXT,
age INTEGER,
shoot_location TEXT,
shoot_date TEXT,
duration_seconds INTEGER,
description TEXT,
rating REAL,
web_url TEXT,
thumbnail TEXT,
published TEXT,
updated TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
ocr_status TEXT NOT NULL DEFAULT 'pending',
ocr_raw_text TEXT,
ocr_error TEXT,
ocr_processed_at TEXT,
CHECK (age IS NULL OR age >= 0),
CHECK (rating IS NULL OR rating BETWEEN 0 AND 10),
CHECK (duration_seconds IS NULL OR duration_seconds >= 0)
);
CREATE TABLE movie_history (
history_id INTEGER PRIMARY KEY AUTOINCREMENT,
movie_id TEXT NOT NULL,
name TEXT NOT NULL,
aka TEXT,
nationality TEXT,
age INTEGER,
shoot_location TEXT,
shoot_date TEXT,
duration_seconds INTEGER,
description TEXT,
rating REAL,
web_url TEXT,
thumbnail TEXT,
published TEXT,
updated TEXT,
created_at TEXT,
modified_at TEXT,
ocr_status TEXT,
ocr_raw_text TEXT,
ocr_error TEXT,
ocr_processed_at TEXT,
archived_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
change_source TEXT NOT NULL DEFAULT 'site_import',
change_summary TEXT NOT NULL,
FOREIGN KEY (movie_id) REFERENCES movie(id)
);
CREATE TABLE movie_name_alias (
alias_id INTEGER PRIMARY KEY AUTOINCREMENT,
movie_id TEXT NOT NULL,
alias TEXT NOT NULL,
normalized_alias TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (movie_id) REFERENCES movie(id),
UNIQUE (movie_id, normalized_alias)
);
```
**Verifierat faktum:** SQLite har dessutom skapat den interna tabellen
`sqlite_sequence(name, seq)` eftersom `movie_history.history_id` och
`movie_name_alias.alias_id` använder `AUTOINCREMENT`. Den ska inte deklareras
manuellt i projektets schema.
### 2.2 Index
**Verifierat faktum:** Produktionen innehåller följande explicita index:
```sql
CREATE INDEX idx_movie_history_movie_id
ON movie_history(movie_id);
CREATE INDEX idx_movie_history_archived_at
ON movie_history(archived_at);
CREATE INDEX idx_movie_name_alias_movie_id
ON movie_name_alias(movie_id);
CREATE INDEX idx_movie_name_alias_normalized
ON movie_name_alias(normalized_alias);
```
**Verifierat faktum:** SQLite har också skapat två unika automatiska index:
| Index | Ursprung | Nyckelkolumner | Partiellt |
| --- | --- | --- | --- |
| `sqlite_autoindex_movie_1` | `PRIMARY KEY` | `movie.id` | nej |
| `sqlite_autoindex_movie_name_alias_1` | `UNIQUE` | `movie_id`, `normalized_alias` | nej |
Alla sex index använder stigande ordning och `BINARY`-kollation. De fyra
explicita indexen är icke-unika och icke-partiella.
### 2.3 Constraints
**Verifierat faktum:** Produktionsschemat deklarerar:
- primärnyckel på `movie.id`,
- primärnyckel med `AUTOINCREMENT``movie_history.history_id`,
- primärnyckel med `AUTOINCREMENT``movie_name_alias.alias_id`,
- `NOT NULL` enligt SQL-definitionerna ovan,
- standardvärdena `CURRENT_TIMESTAMP`, `'pending'`, `'site_import'` och
`'manual'` enligt SQL-definitionerna ovan,
- `CHECK` för icke-negativ `age` och `duration_seconds` samt `rating` i
intervallet 010,
- främmande nyckel från `movie_history.movie_id` till `movie.id`, med
`NO ACTION` för både update och delete,
- främmande nyckel från `movie_name_alias.movie_id` till `movie.id`, med
`NO ACTION` för både update och delete, och
- unikhet för kombinationen
`movie_name_alias(movie_id, normalized_alias)`.
**Verifierat faktum:** Det finns ingen `CHECK`-constraint som begränsar
`ocr_status` till de statusvärden som koden använder (`pending`, `completed`,
`manual_review` och `failed`).
**Verifierat faktum:** Främmande nycklar är del av schemat, men SQLite kräver
att `PRAGMA foreign_keys = ON` aktiveras per anslutning för enforcement.
`import_site.py` aktiverar detta. De övriga inspekterade Python-skripten gör
det inte. Detta är ett runtime-beteende, inte en strukturell skillnad mellan
produktionsschemat och Git-schemat.
### 2.4 Triggers och views
**Verifierat faktum:** Produktionsdatabasen innehåller inga triggers och inga
views. Historisering görs uttryckligen av `import_site.py`, inte av en trigger.
## 3. Schema som kan återskapas från Git i dag
**Verifierat faktum:** När `scripts/schema.sql` läses i en tom SQLite-databas
skapas `movie`, `movie_history`, `movie_name_alias`, den interna
`sqlite_sequence`, samma fyra explicita index och samma två automatiska index
som i produktionen. Inga triggers eller views skapas.
Det återskapade Git-schemats `movie` är:
```sql
CREATE TABLE IF NOT EXISTS movie (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
aka TEXT,
nationality TEXT,
age INTEGER,
shoot_location TEXT,
shoot_date TEXT,
duration_seconds INTEGER,
description TEXT,
rating REAL,
web_url TEXT,
thumbnail TEXT,
published TEXT,
updated TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (age IS NULL OR age >= 0),
CHECK (rating IS NULL OR rating BETWEEN 0 AND 10),
CHECK (duration_seconds IS NULL OR duration_seconds >= 0)
);
```
Git-schemats `movie_history` och `movie_name_alias` är semantiskt identiska
med definitionerna i avsnitt 2.1. De omfattar redan samtliga fyra
OCR-kolumner i `movie_history`. Git skapar även exakt de fyra explicita index
som visas i avsnitt 2.2.
**Verifierat faktum:** `IF NOT EXISTS` i Git-filen men inte i den lagrade
produktionsdefinitionen är ingen strukturell skillnad i den skapade databasen.
**Verifierat faktum:** Git innehåller ingen versionsmarkör eller
migrationshistorik. En nyskapad databas får `user_version = 0`, precis som den
inspekterade produktionsdatabasen.
## 4. Exakt jämförelse
### 4.1 Skillnader
**Verifierat faktum:** Den fullständiga strukturella skillnaden är fyra
kolumner som finns i produktionens `movie` men saknas i Git-schemats `movie`:
| Position i produktion | Kolumn | Typ | `NOT NULL` | Standardvärde | Saknas från Git |
| ---: | --- | --- | --- | --- | --- |
| 16 | `ocr_status` | `TEXT` | ja | `'pending'` | ja |
| 17 | `ocr_raw_text` | `TEXT` | nej | inget | ja |
| 18 | `ocr_error` | `TEXT` | nej | inget | ja |
| 19 | `ocr_processed_at` | `TEXT` | nej | inget | ja |
Konsekvenserna är konkreta:
- nya `movie`-rader från `import_site.py` förlitar sig på standardvärdet
`'pending'`,
- OCR-kön kan inte läsa `ocr_status` eller `ocr_error`,
- OCR-resultat kan inte lagras, och
- `import_site.py` kan inte läsa en komplett rad för arkivering till
`movie_history`.
### 4.2 Element som inte skiljer sig
**Verifierat faktum:** Följande matchar exakt semantiskt mellan produktion och
en tom databas skapad från Git:
- tabellerna `movie_history` och `movie_name_alias`, inklusive samtliga
kolumner, ordning, typer, nullbarhet och standardvärden,
- alla `movie`-kolumner som föregår OCR-kolumnerna,
- samtliga tre `CHECK`-constraints på `movie`,
- båda främmande nycklarna och deras `NO ACTION`-beteende,
- unikhetskravet på `(movie_id, normalized_alias)`,
- samtliga fyra explicita index,
- båda automatiska unika indexen,
- avsaknaden av triggers, och
- avsaknaden av views.
**Verifierat faktum:** Inga tabeller, index, constraints, triggers eller views
finns endast i Git-schemat. Den interna tabellen `sqlite_sequence` uppstår i
båda fallen och är inte ett gap.
## 5. Skriptberoenden per schemaelement
### 5.1 `movie`
| Skript | Lästa eller skrivna element | Förutsättning |
| --- | --- | --- |
| `import_site.py` | Alla 20 produktionskolumner läses för befintliga poster; webbägda fält och `modified_at` uppdateras; nya poster infogas. | Kräver de fyra OCR-kolumnerna för `SELECT` och historisering. Nya poster förlitar sig på `ocr_status DEFAULT 'pending'`. Förlitar sig även på `movie_history`, dess OCR-kolumner och obligatoriska `change_summary`. Aktiverar främmande nycklar. |
| `migrate_csv.py` | Läser `id`; infogar eller uppdaterar alla metadatafält samt `ocr_status`; uppdaterar `modified_at`. | Kräver `ocr_status`. Anger status explicit och förlitar sig därför inte på dess default i sina egna inserts. Skriptet aktiverar inte främmande nycklar. |
| `check_ocr.py` | Läser `name`, `thumbnail`; skriver `nationality`, `shoot_location`, `shoot_date`, samtliga fyra OCR-kolumner och `modified_at`. | Kräver alla fyra OCR-kolumnerna. Använder statusvärdena `completed`, `manual_review` och `failed`, men schemat validerar inte statusdomänen. |
| `process_pending_ocr.py` | Läser `id`, `name`, `published`, `ocr_status`, `ocr_error`. | Kräver `ocr_status` och `ocr_error`; förutsätter att nya obehandlade poster får status `pending`. |
| `match_filenames.py` | Läser `id`, `name`, `duration_seconds`. | Fungerar mot både produktions- och Git-schemat. |
| `test_duration_matching.py` | Läser `id`, `name`, `duration_seconds`. | Fungerar mot både produktions- och Git-schemat. |
### 5.2 `movie_history`
| Skript | Beroende |
| --- | --- |
| `import_site.py` | Infogar den föregående kompletta `movie`-raden, inklusive samtliga OCR-fält, samt `change_source` och `change_summary`. `archived_at` lämnas till standardvärdet. |
| `match_filenames.py` | Läser `movie_id`, `duration_seconds` och `archived_at`. |
| `test_duration_matching.py` | Läser `movie_id` och `duration_seconds`. |
**Verifierat faktum:** Inget skript förutsätter en historiseringstrigger;
`import_site.py` gör historik-insert och film-update i samma anslutningskontext.
### 5.3 `movie_name_alias`
| Skript | Beroende |
| --- | --- |
| `match_filenames.py` | Läser `movie_id`, `alias`, `normalized_alias` och `source`. |
**Verifierat faktum:** Inget inspekterat skript skriver alias. Constraints och
index på aliastabellen finns ändå likadant i produktion och Git.
## 6. Minsta säkra åtgärd
**Rekommendation:** Gör en enda avgränsad schemaändring: lägg till de fyra
verifierade produktionskolumnerna i `movie` i `scripts/schema.sql`, direkt
efter `modified_at`, med exakt följande definitioner:
```sql
ocr_status TEXT NOT NULL DEFAULT 'pending',
ocr_raw_text TEXT,
ocr_error TEXT,
ocr_processed_at TEXT,
```
Detta är den minsta åtgärden eftersom alla andra tabeller, index och
constraints redan reproduceras. Lägg inte samtidigt till triggers, views,
nya index, status-`CHECK`, ändrad främmande-nyckelpolicy eller andra
modellförbättringar; sådana ändringar skulle gå utöver att återskapa dagens
produktion.
**Rekommendation:** Behandla den korrigerade `schema.sql` som ett komplett
grundschema för helt nya databaser. Verifiera det först mot en ny temporär
databas. Kör inte den kompletta filen som en påstådd reparationsmigration mot
produktion: `CREATE TABLE IF NOT EXISTS` kompletterar inte kolumner i en redan
existerande tabell.
**Antagande:** Produktionsdatabasen behöver ingen strukturändring för detta
gap, eftersom den redan har exakt de fyra föreslagna kolumnerna. Den behöver
endast adopteras av en framtida migrationsmodell efter separat kontroll.
## 7. Enkel migrationsmodell för SQLite
**Rekommendation:** Inför en liten, versionshanterad modell med två delar:
1. `schema.sql` är aktuell baseline för nya, tomma databaser.
2. En katalog med ordnade migrationer, exempelvis
`migrations/0001_add_movie_ocr_columns.sql`, beskriver övergångar för äldre
databaser.
Använd en liten Python-runner baserad på standardbibliotekets `sqlite3` och en
tabell som exempelvis:
```sql
CREATE TABLE schema_migration (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
Runnern bör:
- öppna en explicit vald databas och aldrig ha produktionssökvägen som dold
teststandard,
- aktivera `PRAGMA foreign_keys = ON`,
- kontrollera befintliga kolumner med `PRAGMA table_xinfo` före migration,
- köra varje ej registrerad migration atomärt i en transaktion,
- registrera versionen först när hela migrationen lyckats,
- avbryta om databasen har en okänd eller partiell struktur, och
- köra `PRAGMA foreign_key_check` efteråt.
**Rekommendation:** Eftersom SQLite saknar portabelt
`ALTER TABLE ... ADD COLUMN IF NOT EXISTS` ska migration `0001` inte bara
exekveras blint. Runnern ska skilja mellan tre tillstånd:
- inga OCR-kolumner finns: lägg till alla fyra och registrera migrationen,
- alla fyra finns med exakt rätt typ, nullbarhet och default: registrera
migrationen som adopterad utan att ändra tabellen, och
- endast några finns eller definitionerna avviker: avbryt och kräv manuell
analys.
Exempel på de faktiska övergångssatserna för ett äldre Git-skapat schema är:
```sql
ALTER TABLE movie
ADD COLUMN ocr_status TEXT NOT NULL DEFAULT 'pending';
ALTER TABLE movie
ADD COLUMN ocr_raw_text TEXT;
ALTER TABLE movie
ADD COLUMN ocr_error TEXT;
ALTER TABLE movie
ADD COLUMN ocr_processed_at TEXT;
```
**Rekommendation:** Använd antingen `schema_migration` eller konsekvent
`PRAGMA user_version` som versionskälla, inte två oberoende sanningar. Tabellen
är enklare att granska eftersom den även lagrar migrationsnamn och tidpunkt.
Nuvarande `user_version = 0` visar att ingen versionsmodell används i dag.
## 8. Verifieringsplan utan produktionsskrivningar
Alla steg nedan ska använda en ny katalog skapad med `mktemp -d` och en ny
databasfil där. Ingen testparameter får peka på
`/storage/disk1/WCX/database/wcx.db`.
1. **Skydda referensen.** Registrera produktionsdatabasens filstorlek, mtime
och kryptografiska hash före testet. Använd därefter endast
`sqlite3 -readonly` om referensmetadata behöver läsas igen.
2. **Skapa baseline.** Skapa `${TEMP_DIR}/wcx.db` från den korrigerade
`schema.sql`.
3. **Jämför struktur maskinellt.** Jämför tabeller, kolumner, ordning, typer,
nullbarhet, defaults, primärnyckelflaggor, främmande nycklar, explicita och
automatiska index, indexkolumner, triggers och views mot den dokumenterade
produktionsmodellen. Ignorera endast harmlösa skillnader i SQL-format och
`IF NOT EXISTS`.
4. **Kontrollera constraints.** Verifiera att ogiltig negativ `age` och
`duration_seconds`, rating utanför 010, dubblett av
`(movie_id, normalized_alias)` samt främmande nyckelbrott med
`PRAGMA foreign_keys = ON` nekas i temporärdatabasen.
5. **Kontrollera defaults.** Infoga en minimal `movie(id, name)` och verifiera
att `created_at` och `modified_at` sätts samt att `ocr_status = 'pending'`
och övriga OCR-fält är `NULL`.
6. **Testa import.** Kör `import_site.py` med explicit `--input` till en liten
temporär JSON-fil och explicit `--database` till temporärdatabasen. Kör en
andra import med nyare uppdateringsdatum och verifiera att
`movie_history` får hela den föregående raden inklusive OCR-fälten.
7. **Testa CSV-operationer.** Kör först `migrate_csv.py --dry-run`. Om apply
behöver provas, använd endast explicit `--database` till temporärdatabasen
och en isolerad test-CSV.
8. **Testa OCR:s databasberoenden utan API-anrop.** Anropa de rena
databasfunktionerna i `check_ocr.py` mot temporärdatabasen för statusfallen
failed, manual review och completed. Verifiera alla skrivna metadata- och
OCR-kolumner. Testa `process_pending_ocr.py`-frågorna separat så att
`pending` kan väljas och `ocr_error` läsas. Gör inget Vision API-anrop.
9. **Testa konsumentfrågorna.** Kör matchningskodens databasladdning och
längddiagnostik mot temporärdatabasen efter att film, alias och historik har
lagts in.
10. **Testa migrationsvägen separat.** Skapa ytterligare en ny temporär
databas från dagens ofullständiga Git-schema, kör migration `0001`, och
upprepa strukturjämförelsen. Testa även adoption mot en ny databas som
redan har alla fyra korrekta kolumner samt avbrott mot ett medvetet
partiellt schema.
11. **Kör integritetskontroller.** Kräv `PRAGMA integrity_check = 'ok'` och
tomt resultat från `PRAGMA foreign_key_check` i varje temporärdatabas.
12. **Bekräfta produktionsskyddet.** Kontrollera att produktionens storlek,
mtime och hash är identiska med värdena från steg 1. Radera därefter endast
den explicit skapade temporärkatalogen.
**Rekommendation:** Godkänn reproducerbarheten först när både baseline-vägen
och migrationsvägen ger samma semantiska schema som avsnitt 2 och samtliga
databasberoende skript klarar sina isolerade operationer mot temporärdata.
## 9. Slutsats
**Verifierat faktum:** Facitdatabasen är nästan, men inte helt,
reproducerbar från Git. Gapet består exakt av fyra OCR-kolumner i `movie`.
Inga tabeller, index, constraints, triggers eller views saknas utöver dessa
kolumndefinitioner, och inga sådana objekt skiljer sig i övrigt.
**Rekommendation:** Komplettera baselineschemat med de fyra exakta
produktionsdefinitionerna och inför en liten, transaktionell migrationsrunner
som kan migrera äldre Git-skapade databaser och adoptera en redan korrekt
produktion utan att ändra den. All utveckling och verifiering ska ske mot nya
temporära databaser.