# visualdb

> Manuale utente di visualdb: installazione, connessioni, sheet, form, report con raggruppamento e subtotali, viste salvate, snapshot, master-detail, gestione schema, webhook, RLS, assistente AI e packaging.

Pubblicato: 2026-08-24
Aggiornato: 2026-08-25
Ambito: software
Repository: <https://github.com/stefanofante/visualdb>

Pagina: <https://www.stline.it/wiki/visualdb/>

---

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 <a href="https://github.com/stefanofante/visualdb" target="_blank" rel="noopener">GitHub</a>.

## 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](https://nicegui.io/), 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:

```powershell
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.

```powershell
# 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:

```powershell
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:

```powershell
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`, `sheet` o `report`) 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:

1. **New connection** → scegli il dialetto (PostgreSQL, MySQL/MariaDB, SQL Server, Oracle, SQLite, DuckDB, SQLite cifrato).
2. Compila host, porta, database/percorso file, utente e password.
3. **Test** verifica la connessione; **Save** la salva.
4. **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 `.xlsx` con 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` (prefisso `SELECT`/`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 in `READ 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 via `platformdirs`), 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):

```powershell
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.
