Files
prep-step/docs/db-cad-index.md
T
davideandClaude Opus 5 54f267b508 Copia in raw i file che servono a Creo, non solo quelli della distinta
Creo Distributed Batch falliva su 8 dei 22 assiemi con "[-4] Dati di
input non completi", un messaggio che non nomina il file mancante. Erano
due cause distinte, entrambe emerse aprendo gli assiemi da raw/ e
cercando nell'albero il componente con l'icona rossa.

Copia sbagliata del componente. Lo stesso codice esiste spesso in piu'
cartelle della share con pari versione e contenuto diverso (13% dei
codici): non sono revisioni, sono istantanee parallele lasciate da studi,
allestimenti fiera e cartelle temporanee. La vista dell'indice rompeva il
pareggio in modo arbitrario, e poteva vincere una copia obsoleta -
nascondendo del tutto quella buona. Creo poi caricava un modello diverso
da quello atteso, non risolveva i vincoli e non apriva l'assieme. Il
.asm non dice quale copia usa (ne' percorso, ne' UOBJ_ID del figlio, ne'
revisione attesa), quindi si replica la regola di Creo: vince la copia
nella stessa cartella dell'assieme che la referenzia. La vista ora rompe
il pareggio in modo deterministico e viene ricreata a ogni scan, cosi' un
DB esistente prende la definizione nuova senza reindicizzare.

Componenti soppressi. Spariscono da "members" ma restano citati dalla
feature che li posiziona, e il loro file serve comunque per aprire
l'assieme. Da quando la lettura usa il parser esatto - che legge solo
"members", ed e' la scelta giusta per la BOM - non venivano piu' copiati.
Ora si copiano a parte, senza entrare nel conteggio dei "non trovati":
non sono componenti della distinta, segnalarli come mancanti sarebbe un
falso allarme.

Documentate entrambe in docs/formato-asm-record.md §7.3 e §7.4, insieme
alla distinzione che le accomuna: la lista della distinta e la lista dei
file necessari a Creo non coincidono, e la seconda e' piu' lunga.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:52:16 +02:00

10 KiB

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). 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 §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<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:

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).