2026-09-21 14:37:08 +02:00
|
|
|
# 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()
|
2026-09-22 11:52:16 +02:00
|
|
|
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.
|
2026-09-21 14:37:08 +02:00
|
|
|
|
|
|
|
|
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).
|
|
|
|
|
|
2026-09-22 11:52:16 +02:00
|
|
|
> **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`.
|
|
|
|
|
|
2026-09-21 14:37:08 +02:00
|
|
|
## 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
|
|
|
|
|
|
2026-09-23 08:29:03 +02:00
|
|
|
`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.
|
2026-09-21 14:37:08 +02:00
|
|
|
|
2026-09-23 08:29:03 +02:00
|
|
|
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).
|
2026-09-21 14:37:08 +02:00
|
|
|
|
|
|
|
|
**Attenzione in sviluppo (dotnet run / IDE):** `AppContext.BaseDirectory` è la cartella di
|
2026-09-23 08:29:03 +02:00
|
|
|
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
|
2026-09-21 14:37:08 +02:00
|
|
|
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).
|