# 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` på `movie_history.history_id`, - primärnyckel med `AUTOINCREMENT` på `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 0–10, - 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 0–10, 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.