# 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, data_modifica DESC, percorso_completo)`. 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. La vista è ricreata (`DROP` + `CREATE`, non `CREATE IF NOT EXISTS`) a ogni "Aggiorna indice", così un DB già esistente prende la definizione corrente senza dover reindicizzare la share. 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). > **I criteri di pareggio non sono decorativi.** Lo stesso codice può esistere **nella stessa > cartella radice, con la stessa `versione`**, in due sottocartelle diverse (una copia in una > cartella di studio, di lavoro o temporanea). Senza `data_modifica` nell'`ORDER BY` il > pareggio veniva rotto in modo arbitrario, e poteva vincere la copia obsoleta — nascondendo > del tutto quella buona, che non compariva più nella vista. Il file che finiva in `raw/` era > allora un modello diverso da quello che l'assieme si aspetta: Creo lo carica, i vincoli di > accoppiamento non si risolvono e **l'assieme non si apre affatto**. > > Il criterio deterministico però non basta a scegliere *bene*: è il minimo per non sbagliare > a caso. Quale copia sia quella giusta dipende da chi la referenzia, non dalla data — vedi > [`formato-asm-record.md`](formato-asm-record.md) §7.4 e `Form1.ScegliPerCartella`. ## 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).