# Un'app locale per form e report sul database che hai già

> Tre pilastri presi da un prodotto commerciale, un modello di distribuzione ribaltato: un'app locale, senza cloud e senza account remoto, dove tutto nasce da una query-spec JSON e un solo compilatore.

Pubblicato: 2026-08-26
Categoria: software
Tag: visualdb, python, nicegui, sqlalchemy, postgresql, duckdb, low-code, open-source

Pagina: <https://www.stline.it/blog/dbvisual-form-sheet-report-in-locale/>

---

Chi ha un database in produzione e deve dare a qualcuno un modo controllato di inserire dati e tirare fuori report conosce il problema: il database c'è già, lo schema c'è già, e quello che manca è l'interfaccia. Scriverla a mano per ogni tabella è lavoro che non finisce; prendere un generatore di applicazioni significa quasi sempre spostare i dati, o almeno le credenziali, dentro un servizio che gira da un'altra parte.

[Visual DB](https://visualdb.com/) risolve la prima metà del problema in modo pulito, e la sua intuizione è quella che abbiamo preso: davanti a un database esistente servono tre cose, non trenta. Un **form** per lavorare su un record alla volta, uno **sheet** per lavorare su molti record come in un foglio di calcolo, un **report** in sola lettura per leggerli aggregati. Tre pilastri, e sopra quelli si costruisce quasi tutto il lavoro d'ufficio su dati.

Quello che non ci andava bene era il resto: il modello di distribuzione. Da qui è nato [dbvisual](https://github.com/stefanofante/visualdb).

## Cosa abbiamo tenuto e cosa abbiamo ribaltato

I tre pilastri sono rimasti quelli. Il modello di esecuzione è l'opposto: l'applicazione si installa e gira sulla macchina di chi la usa. Niente cloud, niente account remoto, niente multi-tenancy — e queste non sono funzioni ancora da fare, sono **non-obiettivi dichiarati** nella specifica, perché un vincolo scritto è l'unica cosa che regge alla pressione delle richieste successive.

La distinzione che conta è fra l'applicazione e i suoi bersagli. I database di destinazione possono stare dove vogliono, anche su un altro server: è normale puntare a un PostgreSQL aziendale. Ma l'applicazione e i suoi metadati — definizioni, viste salvate, segreti, allegati — restano sempre in locale. Il criterio è BYOD in senso letterale: *bring your own database*, ci si collega a quello che c'è.

Per un fornitore che lavora su applicazioni medicali, industriali e scientifiche questa non è una preferenza estetica. Quando un dato non può uscire dalla macchina che lo produce, uno strumento che per funzionare chiede di caricarlo altrove non è un'opzione da valutare: è fuori perimetro. Uno strumento locale, invece, si può portare dentro un ambiente controllato e discutere con chi lo deve autorizzare.

## Il centro dell'architettura: una specifica, un compilatore

La scelta strutturale che ha ripagato più di tutte è che non esistono due entità distinte "form" e "report". Esiste una **query-spec**, in JSON, che dichiara:

- `main_table` — la tabella principale, l'unica scrivibile in form e sheet;
- `related[]` — le tabelle collegate via chiave esterna, in sola lettura;
- `columns[]` — le colonne selezionate, con alias;
- `filters[]` — le condizioni, parametrizzate;
- `params[]` — i parametri, con supporto multi-valore e a cascata.

Un **solo compilatore** trasforma la query-spec in una `sqlalchemy.select()`. Form, sheet e report sono tre render diversi della stessa specifica; tutto il resto è interfaccia.

La conseguenza pratica si vede sulla sicurezza. Se la scrittura è consentita sulla sola `main_table` e le colonne di lookup sono per costruzione in sola lettura, questa non è una regola da ricordarsi in ogni schermata: è una proprietà del modello, e vale in tutti i render insieme. Stessa cosa per i parametri, che sono sempre *bound* — non c'è un punto del codice in cui si concatena una stringa SQL, quindi non c'è un punto in cui l'iniezione possa entrare.

## L'impianto

Applicazione monolitica: un unico codebase Python, un processo, un eseguibile. Interfaccia con [NiceGUI](https://nicegui.io/), motore con SQLAlchemy Core 2.0. Lo stesso codice gira come finestra desktop nativa o come web app locale su `127.0.0.1`; nessun frontend separato, nessuna build JavaScript.

Tre strati:

- **core** — indipendente dal dialetto: creazione dell'engine multi-dialetto e test di connessione, riflessione dello schema (tabelle, colonne, tipi, chiavi esterne), modelli Pydantic della query-spec, compilatore, CRUD generico con master-detail transazionale e locking ottimistico, composizione ed esecuzione del DDL;
- **meta** — persistenza locale: store dei metadati su SQLite via `platformdirs`, segreti cifrati con `keyring` e fallback su Fernet, archivio locale degli allegati;
- **app** — interfaccia e servizi.

I dialetti supportati sono PostgreSQL, MySQL/MariaDB, SQL Server, Oracle, SQLite e DuckDB; i driver sono extra opzionali, si installa solo quello che serve. Sono previsti anche i file locali cifrati con passphrase: SQLite via SQLCipher, e DuckDB con la cifratura nativa da 1.4 in poi. Se il driver SQLCipher non è installato, l'opzione si disabilita con un messaggio esplicito invece di far cadere l'applicazione — che è il comportamento giusto per una dipendenza opzionale.

Sul lato funzioni, quelle che spostano il lavoro reale: locking ottimistico e salvataggio in batch in una sola transazione, colonne calcolate e validazione per campo, master-detail atomico con propagazione della chiave del master ai dettagli nuovi, report con raggruppamento multi-livello e subtotali, viste salvate private/condivise/bloccate, snapshot *point-in-time* in HTML autoconsistente ed Excel, import/export CSV, webhook non bloccanti su create/update/delete, e row-level security delegata alle policy PostgreSQL — dbvisual passa l'identità corrente con `SET app.current_user_email` e non reimplementa l'autorizzazione, che è il posto giusto dove non essere creativi.

## Due scelte, e il perché

**Il DDL non passa dal canale in sola lettura.** La scheda Database permette di modificare lo schema, ma ogni modifica compone il DDL e lo mostra in un riquadro "rivedi ed esegui": l'esecuzione avviene solo dopo conferma esplicita, doppia per le operazioni distruttive. È un canale separato dal `ensure_readonly` che protegge i report, perché i due hanno requisiti opposti e confonderli è il modo di perdere entrambi.

**L'assistente AI è opzionale, spento per default e in sola lettura.** Traduce una richiesta in linguaggio naturale in SQL per i report, con la chiave API dell'utente conservata come segreto. L'SQL generato viene sempre mostrato per revisione e validato da `ensure_readonly`; non viene mai eseguito automaticamente. Un generatore di query non è un motivo per rinunciare al controllo su cosa tocca il database.

## Stato e verifica

I test del core girano su SQLite e DuckDB in memoria, quindi non richiedono alcun database esterno né credenziali. I test di integrazione sui dialetti server — PostgreSQL, MySQL/MariaDB, SQL Server, Oracle — sono opt-in: si attivano solo se le variabili d'ambiente puntano a un database reale, altrimenti vengono saltati. Ognuno esercita end-to-end le API che l'applicazione usa davvero: creazione tabella via DDL, riflessione, CRUD con locking ottimistico, rilettura compilata, drop finale.

Il pacchetto standalone si costruisce con `nicegui-pack`. Metadati, allegati, segreti e snapshot si risolvono sempre attraverso `platformdirs`, mai dentro la cartella temporanea di PyInstaller: è il difetto classico degli eseguibili confezionati, e si manifesta come dati che scompaiono alla chiusura.

Licenza MIT. La scheda del repository è nell'[Open Lab](/open-lab/visualdb/), con il manuale d'uso nella [wiki](/wiki/visualdb/).
