visualdb
App locale per costruire form, sheet e report su database esistenti, senza cloud.
Codice su GitHub ↗Manuale utente di visualdb: un’applicazione locale e self-contained per costruire form, sheet (griglie) e report su database esistenti, nello spirito di Visual DB. Gira interamente sulla macchina di installazione — senza dipendere da un servizio cloud per funzionare e senza account remoto. Due integrazioni sono però opzionali e disattivabili e, se attivate, escono dalla macchina: la generazione SQL da linguaggio naturale (provider LLM cloud, con API key dell’utente) e i webhook. Con quelle spente l’applicazione è interamente locale. I database target possono essere locali o remoti, ma l’app e i suoi metadati restano sempre in locale. Il riferimento autoritativo resta il repository GitHub.
Cos’è e a cosa serve
visualdb genera interfacce a partire da una query-spec (una specifica JSON): form, sheet e report sono render diversi della stessa specifica. È un’applicazione monolitica — un unico codebase Python (UI NiceGUI, engine SQLAlchemy Core 2.0), un solo processo, un solo eseguibile. Lo stesso codice gira come finestra desktop nativa oppure come web app locale su 127.0.0.1.
Serve a chi ha già un database e vuole dare rapidamente un’interfaccia di data-entry (form), un foglio editabile stile Excel (sheet) o una reportistica di sola lettura, senza scrivere un frontend. Database supportati: PostgreSQL, MySQL/MariaDB, SQL Server, Oracle, SQLite e DuckDB.
Installazione
Serve Python 3.11 o superiore. Si consiglia un ambiente virtuale:
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e .
I driver dei database sono extra opzionali: si installano solo quelli che servono.
# un driver specifico
.\.venv\Scripts\python.exe -m pip install -e ".[postgresql]"
# oppure tutti i driver
.\.venv\Scripts\python.exe -m pip install -e ".[all-drivers]"
Extra disponibili: postgresql, mysql, mssql, oracle, duckdb, all-drivers. SQLite è incluso in Python.
Avvio
Dall’entrypoint del progetto:
python main.py --mode desktop # finestra desktop nativa (default)
python main.py --mode web # web app su http://127.0.0.1:8080
Oppure tramite il comando installato:
dbvisual --mode desktop
dbvisual --mode web --host 127.0.0.1 --port 8080
In modalità web l’app resta legata a 127.0.0.1 (solo locale). La modalità di avvio preferita si imposta anche dalla pagina Impostazioni.
Concetti di base
- Connessione: i parametri per raggiungere un database target (le credenziali sono salvate come segreto, mai in chiaro).
- Query-spec: la specifica JSON che descrive tabella principale, colonne e relazioni; è la sorgente unica da cui derivano form, sheet e report.
- Applicazione: un raggruppamento logico di definizioni (form/sheet/report).
- Definizione: una singola vista salvata (
form,sheetoreport) legata a una connessione.
Regola trasversale: la scrittura avviene solo sulla main_table della query-spec; le colonne correlate (lookup) sono in sola lettura.
Connessioni
Nella pagina Connections crei e gestisci le connessioni:
- New connection → scegli il dialetto (PostgreSQL, MySQL/MariaDB, SQL Server, Oracle, SQLite, DuckDB, SQLite cifrato).
- Compila host, porta, database/percorso file, utente e password.
- Test verifica la connessione; Save la salva.
- Browse schema apre tabelle, colonne (tipo/nullable/PK) e foreign key.
La password è gestita come segreto (keyring di sistema, con fallback cifrato cryptography.Fernet). Per i database file cifrati (SQLCipher per SQLite, cifratura nativa per DuckDB) c’è un campo passphrase dedicato, anch’esso salvato come segreto e mai in chiaro.
Sheet (griglia editabile stile Excel)
Uno sheet rende una tabella come griglia editabile:
- Editing solo sulle colonne della
main_table; colonne correlate in sola lettura. - Locking ottimistico: se il record è stato modificato da altri nel frattempo, il salvataggio viene rifiutato e la griglia ricaricata.
- Validazione a livello di cella: gli errori bloccano il salvataggio.
- Colonne calcolate (formule) e totali live.
- Barra strumenti: ricerca rapida, aggiungi/elimina riga, copia/incolla TSV (da/verso Excel), export CSV, raggruppamento per colonna.
- Colonne allegato: una colonna di testo può diventare colonna di allegati (file su disco tramite lo store locale, metadati JSON nella cella); l’eliminazione della riga rimuove a cascata i file.
- Salvataggio batch in un’unica transazione.
Form (data-entry a record singolo)
Un form mostra un record alla volta con navigazione avanti/indietro:
- Tipi di input dedicati e valori disponibili (dove l’etichetta è diversa dal valore memorizzato).
- Default, validazione per campo, submit rules (regole cross-campo al salvataggio) e form rules condizionali (mostra/nascondi/abilita campi).
- Campi allegato come nello sheet.
- Salvataggio transazionale con locking ottimistico; l’eliminazione del record rimuove a cascata gli allegati.
Report (sola lettura)
Un report è progettato per non scrivere sul database. La sorgente dati è il query builder oppure una SQL custom, filtrata da ensure_readonly (prefisso SELECT/WITH e vincoli lessicali). Quel filtro va inteso come primo strato, non come garanzia: la sola lettura va imposta dai privilegi dell’account DB — vedi Sicurezza e cartella dati.
- Parametri multi-valore (
WHERE IN) e a cascata; filtri annidati AND/OR; ricerca full-text sui risultati. - Raggruppamento con subtotali: raggruppa su più livelli e calcola subtotali (sum/avg/count/min/max); i gruppi si ordinano per didascalia o per subtotale. La griglia mostra righe di intestazione gruppo, righe di dettaglio e righe di subtotale (in grassetto). Il raggruppamento resta coerente con la ricerca full-text.
- Grafici
ui.echart: colonna, linea/time-series con zoom, torta. - Export CSV e snapshot point-in-time (vedi sotto).
Snapshot (istantanee point-in-time)
Congela i dati correnti (già filtrati/raggruppati) in artefatti portabili salvati nella cartella snapshots/ dentro la directory dati utente:
- HTML — un singolo file autonomo (stile e dati inline, nessun asset esterno, nessun DB), che riflette esattamente ciò che è a schermo, subtotali inclusi.
- Excel — un
.xlsxcon le righe di dettaglio e, quando il raggruppamento è attivo, righe di intestazione gruppo e di subtotale.
Viste salvate
Sheet e report possono salvare viste con nome (ricerca, raggruppamento, configurazione colonne):
- Private (visibili solo all’identità corrente), condivise (visibili a tutti) o bloccate (immutabili).
- Si ricaricano o eliminano dal dialogo Views. Sono memorizzate nel metadata store locale.
Master-detail
Un master-detail unisce un form master a una o più griglie di dettaglio collegate. La query di dettaglio ha esattamente un parametro legato alla PK del master. Il salvataggio di master + tutti i dettagli avviene in un’unica transazione; la PK di un nuovo master viene propagata alle FK dei nuovi dettagli. Copre relazioni uno-a-molti e molti-a-molti.
Database (gestione schema / DDL)
La scheda Database è un browser ed editor dello schema. Ogni modifica compone il DDL e mostra l’SQL esatto in un dialogo “Review and execute”; l’esecuzione avviene solo dopo conferma esplicita (doppia conferma per le operazioni distruttive).
- Crea/elimina tabelle, aggiungi/rimuovi colonne, foreign key.
- Import/export CSV, diagramma delle relazioni (
ui.mermaid), DDL generato dall’AI (opzionale). - Il canale DDL (scrittura) è separato da
ensure_readonly. Serve un utente con privilegi DDL.
Automazioni / Webhook
Su create/update/delete è possibile inviare un HTTP POST (JSON) non bloccante verso URL configurati (Zapier/Slack/Discord/endpoint custom). Placeholder nel corpo: {{campo}} / {{campo:formatted}} / {{campo:bare}}. Gli URL dei webhook sono trattati come segreti. La configurazione è per sheet/form dal pulsante Webhook.
Row-Level Security (PostgreSQL)
La RLS è delegata a PostgreSQL (le policy SQL dell’utente): visualdb si limita a passare l’identità corrente con SET app.current_user_email. Si abilita per singola query-spec (solo Postgres), con un’email di identità locale. Attenzione: la connessione deve usare un ruolo NON superuser e NON owner della tabella, altrimenti la RLS viene bypassata.
Assistente AI (opzionale, disattivato di default)
Genera SQL di sola lettura per i Report a partire dal linguaggio naturale, tramite un provider LLM a scelta (Claude / OpenAI / Gemini / DeepSeek) con una API key dell’utente salvata come segreto. L’SQL generato è sempre mostrato per revisione e validato da ensure_readonly; non viene mai eseguito automaticamente. Usando l’AI, la struttura del DB (nomi di tabelle e colonne) e la richiesta vengono inviate al provider cloud scelto (costo per token a carico dell’utente).
Impostazioni
La pagina Settings è la fonte unica di configurazione:
- AI: abilita/disabilita (off di default), provider, modello e API key (salvata solo come segreto; mostrata come impostata / non impostata, mai il valore), con test e cancellazione.
- Identità / RLS: l’email usata come
app.current_user_email(vuota = RLS inattiva). - Generale: modalità di avvio preferita e la directory dati utente (mostrata in sola lettura).
Sicurezza e cartella dati
- I valori generati dall’applicazione passano sempre come parametri bind. È la difesa corretta contro l’injection sui valori, e va detto per quello che copre: non rende sicuro un frammento SQL arbitrario, un identificatore costruito dinamicamente o una query scritta a mano. Per la SQL custom vale il punto seguente.
- La sola lettura dei report non deve dipendere dal parser. Il controllo
ensure_readonly(prefissoSELECT/WITH, vincoli lessicali) è utile come defense in depth, ma un filtro sul testo non è una security boundary: quella si ottiene dando all’account DB usato per la reportistica privilegi realmente di sola lettura, e dove il DBMS lo supporta aprendo la transazione inREAD ONLY. Se serve garantire che un report non scriva, il presidio sta nei permessi, non nella grammatica. - Scrittura solo sulla
main_table; colonne di lookup in sola lettura. - I segreti (password, passphrase, API key, URL webhook) non sono mai salvati in chiaro nel metadata store o nei log.
- Metadata store, allegati, vault dei segreti e cartella
snapshots/risiedono nella directory dati utente (risolta viaplatformdirs), mostrata nelle Impostazioni.
Il modello di minaccia, in una tabella
I punti sopra si leggono meglio messi in fila, perché ognuno ha un confine preciso e la terza colonna è quella che conta:
| Superficie | Cosa la difende | Cosa la difesa non copre |
|---|---|---|
| Valori inseriti dall’utente nelle griglie e nei form | parametri bind, sempre | identificatori costruiti dinamicamente (nomi di tabella o colonna) e frammenti SQL scritti a mano |
| SQL custom dei Report | ensure_readonly + privilegi dell’account DB + transazione READ ONLY |
il solo ensure_readonly: è un filtro sul testo, non una frontiera. La frontiera sono i privilegi |
| Visibilità fra utenti sulle stesse tabelle | RLS di PostgreSQL, con le policy scritte dall’utente | niente su SQLite, che non ha RLS; e niente se il ruolo la bypassa (vedi sotto) |
| Credenziali e chiavi | vault dei segreti, mai in chiaro nel metadata store né nei log | la sicurezza del sistema operativo e del profilo utente sotto cui gira l’applicazione |
| Dati che escono dalla macchina | nulla, per costruzione: webhook e AI sono opt-in | una volta attivati sono trasferimenti a terzi a tutti gli effetti — vedi la sezione dedicata |
La riga da rileggere è la seconda. Un filtro sul testo dell’SQL è utile perché intercetta l’errore distratto, ma un attaccante che controlla il testo della query ha molte più forme grammaticali disponibili di quante un prefisso possa escludere: la garanzia che un report non scriva si ottiene togliendo all’account il permesso di scrivere, non guardando come inizia la stringa.
Le quattro condizioni che disattivano la RLS
L’avvertenza sul ruolo non superuser e non owner è corretta ma incompleta. In PostgreSQL la RLS non si applica in quattro casi, e i primi tre sono silenziosi — nessun errore, si vedono tutte le righe:
| Condizione | Effetto sulla RLS | Come verificarlo |
|---|---|---|
| il ruolo è superuser | bypassata sempre | SELECT rolsuper FROM pg_roles WHERE rolname = current_user; |
il ruolo ha l’attributo BYPASSRLS |
bypassata sempre | SELECT rolbypassrls FROM pg_roles WHERE rolname = current_user; |
| il ruolo è owner della tabella | bypassata, salvo FORCE ROW LEVEL SECURITY |
\dt in psql, colonna Owner |
| RLS abilitata ma nessuna policy definita | nega tutto: zero righe, non un errore | SELECT * FROM pg_policies WHERE tablename = '...'; |
L’ultimo caso è il più disorientante perché va nella direzione opposta: abilitare la RLS senza scrivere policy nega l’accesso a tutto, e l’applicazione mostra una tabella vuota invece di un messaggio d’errore. Chi ha appena eseguito ALTER TABLE ... ENABLE ROW LEVEL SECURITY e vede zero righe non ha un problema di connessione: ha una tabella senza policy.
FORCE ROW LEVEL SECURITY è l’unico modo di assoggettare anche il proprietario della tabella, e che serve quando l’account applicativo è anche l’owner dello schema — una configurazione comoda in sviluppo che disattiva la RLS senza dirlo.
Cosa esce dalla macchina, e quando
Due funzioni fanno uscire dati verso terzi. Sono entrambe disattivate di default e vanno abilitate esplicitamente, ma una volta attive sono trasferimenti a tutti gli effetti — e su dati di lavoro reali questo ha conseguenze che non sono tecniche.
I webhook inviano il contenuto delle righe. I placeholder {{campo}} nel corpo del POST vengono sostituiti coi valori effettivi, quindi il destinatario riceve dati: attivare un webhook su una tabella che contiene dati personali è una comunicazione a terzi, con la stessa base giuridica che servirebbe per qualunque altra. Il fatto che l’URL sia trattato come un segreto protegge il canale, non i dati che ci passano.
L’assistente AI invia lo schema e la richiesta, non i dati: nomi di tabelle e colonne. È una differenza sostanziale e va detta, ma non azzera il problema, perché uno schema può essere rivelatore per conto proprio. Una tabella chiamata pazienti_oncologia con una colonna data_diagnosi racconta la natura del trattamento anche restando completamente vuota; un nome cliente in un nome di tabella è un nome cliente comunicato. In un contesto regolato la valutazione va fatta sullo schema come si farebbe sui dati.
La conseguenza operativa è semplice, e sta nel modo in cui il progetto è costruito: entrambe restano off finché non si decide diversamente, e quella decisione è il punto in cui la valutazione va fatta — non dopo.
Packaging (eseguibile standalone)
Si può produrre un eseguibile autonomo con nicegui-pack (wrapper di PyInstaller):
nicegui-pack --onefile --name dbvisual main.py
Nel repository, docs/packaging.md elenca gli hidden imports necessari (driver DB, duckdb, keyring, cryptography, openpyxl, moduli AI) e una checklist di accettazione manuale in 8 punti. I dati utente si risolvono sempre via platformdirs, mai dentro la cartella temporanea di PyInstaller; se SQLCipher è assente, le connessioni SQLite cifrate degradano con un messaggio chiaro mentre tutto il resto continua a funzionare.
Un progetto simile?
Acustica, embedded, strumenti di calcolo: se hai un caso d’uso vicino, parliamone.