Files
prep-step/docs/db-cad-index.md
T

160 lines
10 KiB
Markdown
Raw Permalink Normal View History

# 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 `<codice>.<tipo>.<versione>` |
| `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)`. Le
ricerche per codice (`Form1.TrovaPercorsiCad` in fase 3, `Form1.CaricaCodiciNoti`)
interrogano questa vista, non `files` direttamente, così un componente con più versioni sulla
share risolve sempre all'ultima. Fa eccezione la fase 4 (`Form1.CaricaCandidati`), che legge
`files` per avere tutte le copie di un codice in cartelle diverse e scegliere quella giusta
(`ScegliPerCartella`). 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/INDEX IF NOT EXISTS`; la vista invece è
`DROP`+`CREATE` a ogni scan) — 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<T>`,
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: `^(?<codice>.+)\.(?<tipo>[A-Za-z0-9]+)\.(?<versione>\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` carica sempre e solo `cad_index.db` accanto all'eseguibile
(`AppContext.BaseDirectory`: con il setup è `%LOCALAPPDATA%\Programs\PrepStep`). Nessun
percorso viene ricordato fra un avvio e l'altro.
Se non esiste, la fase 1 resta bloccata su "Database non trovato" finché l'utente non preme
"Aggiorna indice" (`Form1.AggiornaIndice`, che crea `cad_index.db` da zero accanto
all'eseguibile) o "Apri DB..." (`Form1.ScegliDatabase`, file picker libero: il DB scelto vale
solo per la sessione corrente, e "Aggiorna indice" aggiorna quello).
**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. L'app usa
quindi la copia di `cad_index.db` in quella cartella (creandola lì al primo "Aggiorna
indice"), non quella nella radice del repo né quella installata: per usare un altro DB in
sviluppo va aperto con "Apri DB..." a ogni avvio. 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.
- **Codici noti per lo scanner assiemi** (`Form1.CaricaCodiciNoti`, fase 4;
`scripts/BomViaDb.cs` fa una query equivalente per conto suo): carica in un'unica query tutti
i codici prt/asm noti, invece di una query per codice — usata solo per validare i token
generici trovati nei binari `.asm` (vedi `docs/formato-asm.md`). I percorsi da copiare
vengono invece da `Form1.CaricaCandidati` (tabella `files`, vedi sopra).
## 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).