Aggiungi documentazione tecnica su cad_index.db
Schema, logica di scansione/upsert incrementale di Indexer.ScanAsync, come l'app individua il DB all'avvio (incluso il caveat sulla cartella di build in dev) e le query piu' usate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ea582e645
commit
3cfc040fb0
1 file changed
+144
@@ -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 `<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)`. 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<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` 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).
|
||||
Reference in new issue
Block a user