diff --git a/docs/db-cad-index.md b/docs/db-cad-index.md new file mode 100644 index 0000000..4f188e3 --- /dev/null +++ b/docs/db-cad-index.md @@ -0,0 +1,144 @@ +# Il database `cad_index.db` + +Documentazione tecnica del DB SQLite che indicizza i file CAD: schema, come viene +popolato/aggiornato (`Indexer.ScanAsync`), come l'app lo trova all'avvio e i problemi più +comuni quando i risultati sembrano "mancanti". + +## Schema + +Tabella `files` (schema SQL in `Indexer.Schema`, `PrepStep/Indexer.cs`): un record per ogni +file trovato sotto la radice del disco (default `X:\`, `Form1.RadiceDiscoDefault`). + +| Colonna | Contenuto | +|---|---| +| `id` | chiave primaria autoincrementale | +| `percorso_completo` | percorso assoluto del file — **chiave univoca**, usata per upsert | +| `percorso_relativo` | percorso relativo alla cartella di primo livello (`cartella_radice`) | +| `nome_file` | solo il nome del file | +| `cartella_radice` | prima cartella sotto la radice del disco (es. `__B.end`, `Commerciali`, ...) | +| `famiglia` | primo livello di sottocartella dentro `cartella_radice` (null se il file è direttamente dentro `cartella_radice`) | +| `codice` | estratto dal nome file col pattern Pro-E `..` | +| `tipo_file` | es. `prt`, `asm`, o `altro` se il nome non rispetta il pattern | +| `versione` | numero di versione Pro-E (intero), null se non rispetta il pattern | +| `dimensione_byte` | dimensione del file | +| `data_modifica` | `LastWriteTimeUtc` del file, formato ISO 8601 | +| `scansionato_il` | timestamp UTC dell'ultima scansione in cui il file è stato visto | + +Indici su `codice`, `tipo_file`, `cartella_radice` (le colonne usate nelle query di ricerca +componenti e nel filtro per cartella). + +Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con +`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER() +OVER (PARTITION BY ... ORDER BY versione DESC)`. Tutte le ricerche di componenti (`Form1. +TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano questa vista, non `files` +direttamente, così un componente con più versioni sulla share risolve sempre all'ultima. +Deliberatamente una `ROW_NUMBER()` invece di un self-join con `GROUP BY`: su ~200k righe un +self-join degenera in tempi quasi quadratici, `ROW_NUMBER()` resta lineare. + +Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste +in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per +ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per +`tipo_file` lato C# (vedi sotto). + +## Come viene popolato: `Indexer.ScanAsync` + +Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato. + +1. Crea lo schema se non esiste (`CREATE TABLE/VIEW IF NOT EXISTS`) — quindi funziona sia su + un DB vuoto (prima esecuzione) sia su uno esistente (aggiornamento incrementale). +2. Cancella subito tutte le righe con `cartella_radice LIKE 'temp\_%'`: le cartelle + `temp_paris`, `temp_davide`, `temp_studi esterni`, ecc. sono lavori personali/temporanei che + possono contenere copie datate con lo stesso codice numerico di file "ufficiali" altrove + sulla share, causando falsi positivi nella risoluzione dei sotto-componenti (vedi + `docs/formato-asm.md`). Vengono escluse a monte, non solo filtrate nelle singole query. +3. Elenca le cartelle di primo livello sotto la radice con `Directory.GetDirectories` (non un + elenco fisso in codice: una nuova cartella sulla share entra nell'indice al prossimo + aggiornamento senza bisogno di modificare `PrepStep`), escludendo quelle che iniziano per + `temp_`. +4. Per ogni cartella di primo livello, cammina l'albero con uno stack manuale (`Walk`, in + `Indexer.cs`) invece di `Directory.EnumerateFiles(..., SearchOption.AllDirectories)`: più + veloce su una share di rete con centinaia di migliaia di file, e ogni sottocartella + irraggiungibile (permessi, link rotti) viene saltata con un `try/catch` invece di far fallire + l'intera scansione. +5. Per ogni file trovato, estrae `codice`/`tipo`/`versione` dal nome (`ParseFilename`, vedi + sotto) e fa un **upsert riga per riga** dentro una transazione per cartella: confronta + `dimensione_byte` + `data_modifica` con quanto già salvato per lo stesso + `percorso_completo`— + - non esiste ancora → `INSERT` (contatore "nuovi") + - esiste ma dimensione/data sono cambiate → `UPDATE` (contatore "aggiornati") + - esiste e non è cambiato nulla → solo `scansionato_il` viene aggiornato (contatore + "invariati"), per non riscrivere l'intera riga inutilmente +6. Al termine di ogni cartella, cancella dal DB le righe di quella `cartella_radice` il cui + `scansionato_il` non è quello di questa scansione (`DELETE ... WHERE scansionato_il != + @now`): sono file rimossi dal disco dall'ultima scansione. Il conteggio finisce nel + contatore "rimossi". Questo avviene **per cartella**, non a scansione intera completata: se + annulli a metà, le cartelle già scansionate hanno comunque gli eliminati ripuliti. +7. Il progresso (`ScanProgress`, con conteggi nuovi/aggiornati/invariati/rimossi, file + fatti/previsti e tempo trascorso) viene riportato ogni 200 file tramite `IProgress`, + marshalato automaticamente sul thread UI, e mostrato in `ProgressForm` (che stima anche il + tempo rimanente dal ritmo file/secondo rispetto al totale atteso, preso dal conteggio della + scansione precedente salvato nel DB). + +### Estrazione codice/tipo/versione: `ParseFilename` + +Regex: `^(?.+)\.(?[A-Za-z0-9]+)\.(?\d+)$` applicata al nome file +(non al percorso). Esempio: `9251208030c.asm.5` → `codice="9251208030c"`, `tipo="asm"`, +`versione=5`. Il gruppo `codice` è **greedy**, quindi cattura tutto quello che precede l'ultimo +`.tipo.versione` — se un componente ha una lettera di revisione attaccata nel nome del file +(es. `9248908002a.asm.3`), quella lettera fa parte del `codice`, non è un gruppo separato. + +Se il nome file non rispetta il pattern (niente `.tipo.numero` finale), l'intero nome finisce +in `codice` e `tipo_file` diventa `"altro"` (usato per capire quanti file "strani" ci sono +sulla share, non tipicamente cercato dalla UI). + +## Come l'app trova il DB all'avvio + +`Form1.InizializzaDatabase` prova, in ordine, i seguenti candidati e usa il primo che esiste +su disco: + +1. l'ultimo DB caricato con successo, salvato in + `%AppData%/PrepStep/ultimo_db.txt` (`Form1.LeggiUltimoDbUsato`/`SalvaUltimoDbUsato`) +2. `cad_index.db` accanto all'eseguibile (`AppContext.BaseDirectory`) +3. `cad_index.db` nella working directory corrente + +Se nessuno dei tre esiste, la fase 1 resta bloccata su "Database non trovato" finché l'utente +non preme "Apri DB..." (`Form1.ScegliDatabase`, file picker libero) o "Aggiorna indice" +(`Form1.AggiornaIndice`, che crea `cad_index.db` da zero accanto all'eseguibile se non c'è +ancora un `_dbPath`). + +**Attenzione in sviluppo (dotnet run / IDE):** `AppContext.BaseDirectory` è la cartella di +build (`PrepStep/bin/Release/net10.0-windows` o `Debug/...`), non la radice del repo. Se in +quella cartella finisce per errore una copia di `cad_index.db` (es. creata lì da una vecchia +sessione), l'app la userà invece di quella nella radice del repo, e le due copie divergeranno +a ogni "Aggiorna indice" fatto nell'uno o nell'altro ambiente. Se i risultati di una ricerca +sembrano incoerenti con quello che ti aspetti di vedere sul DB "giusto", controlla con "Apri +DB..." quale file è effettivamente caricato (mostrato in fase 1 accanto al conteggio dei file +indicizzati) prima di sospettare un bug nella ricerca. + +## Query più usate + +- **Verifica completezza / crea raw** (`Form1.TrovaPercorsiCad`): per ogni codice del CSV, + `SELECT tipo_file, percorso_completo FROM files_ultima_versione WHERE LOWER(codice) = + LOWER(@codice) AND LOWER(tipo_file) IN ('prt','asm') ORDER BY tipo_file, data_modifica DESC`. + Il match sul `codice` è **esatto** (case-insensitive, non un LIKE): se il codice del CSV ha + una lettera di revisione in più o in meno rispetto al nome del file su disco, non c'è match e + la cella "Percorso CAD" resta vuota — non è un errore della query, è che il DB non ha nessuna + riga con quell'esatto `codice`. In presenza di duplicati su più `cartella_radice`, tiene solo + il primo per `tipo_file` (il più recente, dato l'`ORDER BY data_modifica DESC`) e scarta gli + altri. +- **Risoluzione codici noti per lo scanner assiemi** (`Form1.CaricaCodiciNoti`, usata sia dalla + GUI in fase 4 sia da `scripts/BomViaDb.cs`): carica in un'unica query tutti i codici + prt/asm noti con i relativi percorsi, invece di una query per codice — usata per validare i + token generici trovati nei binari `.asm` (vedi `docs/formato-asm.md`). + +## Debug ad-hoc + +Per interrogare il DB direttamente da riga di comando senza aprire un tool SQLite esterno: + +```powershell +dotnet run scripts/Query.cs -- cad_index.db "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione WHERE codice LIKE '925%' LIMIT 20" +``` + +Utile per capire in fretta se un codice mancante nella UI non è nell'indice affatto (nessuna +riga, nemmeno con un `LIKE` più permissivo) oppure c'è ma con un `codice` leggermente diverso +da quello atteso (es. lettera di revisione, maiuscole/minuscole, spazi).