SQLite è una libreria C serverless, zero-configuration, transazionale che implementa un motore SQL embedded. L'intero database risiede in un singolo file cross-platform. Conforme ad ACID anche in presenza di crash e power loss.
| Caratteristica | Dettaglio |
|---|---|
| Tipo | Libreria in-process, no daemon, no IPC |
| File DB | Singolo file portabile (little-endian, cross-OS) |
| Tipi SQL | NULL, INTEGER, REAL, TEXT, BLOB (type affinity) |
| Concorrenza | Multi-reader / single-writer (WAL: reader non bloccati) |
| Limite DB | 281 TB (teorico) — tipicamente < qualche GB |
| Licenza | Public Domain |
| Oggetto | Ruolo |
|---|---|
sqlite3 | Handle di connessione al database |
sqlite3_stmt | Statement preparato (SQL compilato) |
sqlite3_value | Valore dinamico (usato nelle funzioni custom) |
sqlite3_blob | Handle per I/O BLOB incrementale |
sqlite3_backup | Handle per operazioni di backup online |
sqlite3_context | Contesto esecuzione di funzioni SQL utente |
Linux/macOS forniscono tipicamente libsqlite3 come libreria di sistema. L'header è sqlite3.h, la libreria è -lsqlite3.
# Compilazione base gcc -Wall -Wextra -std=c11 main.c -o app -lsqlite3 # Con pkg-config (se disponibile) gcc main.c -o app $(pkg-config --cflags --libs sqlite3) # Verifica presenza libreria pkg-config --modversion sqlite3 ldconfig -p | grep sqlite3
SQLite distribuisce una amalgamation (file unico sqlite3.c + sqlite3.h) per embedding diretto. Nessuna dipendenza esterna richiesta.
# Download da sqlite.org/download.html # Compila sqlite3.c insieme al tuo codice gcc -O2 -DSQLITE_THREADSAFE=1 \ main.c sqlite3.c -o app \ -lpthread -ldl -lm
| Macro | Effetto |
|---|---|
SQLITE_THREADSAFE=0|1|2 | 0=single, 1=serialized, 2=multi-thread |
SQLITE_ENABLE_FTS5 | Abilita full-text search v5 |
SQLITE_ENABLE_JSON1 | Funzioni JSON (integrato dal 3.38) |
SQLITE_ENABLE_RTREE | Indici R-tree (spatial) |
SQLITE_ENABLE_COLUMN_METADATA | Attiva sqlite3_column_database_name() ecc. |
SQLITE_OMIT_DEPRECATED | Rimuove API obsolete |
SQLITE_DEFAULT_MEMSTATUS=0 | Disabilita tracking memoria (performance) |
SQLITE_USE_URI=1 | Abilita parsing URI nei filename |
#include <sqlite3.h> #include <stdio.h> int main(void) { /* Compile-time: macro #define */ printf("Compile: %s (%d)\n", SQLITE_VERSION, SQLITE_VERSION_NUMBER); /* Runtime: funzioni della libreria dinamicamente linkata */ printf("Runtime: %s (%d)\n", sqlite3_libversion(), sqlite3_libversion_number()); printf("Source: %s\n", sqlite3_sourceid()); return 0; }
sqlite3_libversion_number() per controlli condizionali a runtime.#include <sqlite3.h> // tutta l'API pubblica
Un singolo header espone tutte le funzioni, macro e tipi opachi. Nessun sotto-header.
sqlite3 *db; // handle di connessione sqlite3_stmt *stmt; // statement preparato sqlite3_blob *blob; // handle BLOB per I/O incrementale sqlite3_backup *bkp; // sessione di backup sqlite3_value *val; // valore dinamico (in funzioni custom) sqlite3_context *ctx; // contesto di esecuzione UDF /* Intero a 64 bit definito dalla libreria */ sqlite3_int64 id = 123456789012; sqlite3_uint64 sz;
Codici di ritorno principali:
• SQLITE_OK (0) — successo
• SQLITE_ROW (100) — step ha prodotto riga
• SQLITE_DONE (101) — step completato
• SQLITE_BUSY (5) — DB lock
• SQLITE_ERROR (1) — errore generico
• SQLITE_MISUSE (21) — API usata scorrettamente
• SQLITE_CONSTRAINT (19) — violazione vincolo
Tipi di storage (column_type):
• SQLITE_INTEGER (1)
• SQLITE_FLOAT (2)
• SQLITE_TEXT (3)
• SQLITE_BLOB (4)
• SQLITE_NULL (5)
sqlite3*sqlite3_stmt*?, ?N, :nomeSQLITE_ROW per ogni riga, poi SQLITE_DONESQLITE_ROW)prepare → bind → step → reset può essere ripetuto N volte: compila una sola volta, esegui molte. È il pattern più efficiente e sicuro (no SQL injection).int sqlite3_open(const char *filename, sqlite3 **ppDb);
Apre (o crea) un database. filename è una stringa UTF-8. Ritorna SQLITE_OK in caso di successo. Anche in caso di errore *ppDb è valido e deve essere chiuso con sqlite3_close().
| Filename | Significato |
|---|---|
"file.db" | Database su disco (relativo al cwd) |
":memory:" | Database in RAM, distrutto alla chiusura |
"" (stringa vuota) | Database temporaneo su disco, cancellato alla chiusura |
"file:foo.db?mode=ro" | URI (richiede flag o SQLITE_USE_URI) |
#include <stdio.h> #include <sqlite3.h> int main(void) { sqlite3 *db = NULL; int rc = sqlite3_open("test.db", &db); if (rc != SQLITE_OK) { fprintf(stderr, "open: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); // va chiamata anche in errore return 1; } /* ... uso del DB ... */ sqlite3_close(db); return 0; }
int sqlite3_open_v2( const char *filename, // UTF-8 path o URI sqlite3 **ppDb, // out: handle int flags, // SQLITE_OPEN_* const char *zVfs // VFS name (NULL = default) );
| Flag | Significato |
|---|---|
SQLITE_OPEN_READONLY | Sola lettura; errore se il file non esiste |
SQLITE_OPEN_READWRITE | Lettura/scrittura; errore se non esiste |
SQLITE_OPEN_CREATE | Crea se non esiste (con READWRITE) |
SQLITE_OPEN_URI | Interpreta filename come URI |
SQLITE_OPEN_MEMORY | DB in memoria puro |
SQLITE_OPEN_NOMUTEX | Multi-thread mode (no serial. interna) |
SQLITE_OPEN_FULLMUTEX | Serialized mode (più protezione, più lento) |
SQLITE_OPEN_SHAREDCACHE | Shared cache (deprecato in molti casi) |
SQLITE_OPEN_PRIVATECACHE | Cache privata (default in molti build) |
SQLITE_OPEN_NOFOLLOW | Rifiuta symlink |
sqlite3 *db; int rc = sqlite3_open_v2( "data.db", &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL); if (rc != SQLITE_OK) { fprintf(stderr, "open_v2: %s\n", sqlite3_errmsg(db)); sqlite3_close_v2(db); return 1; }
sqlite3_open() equivale a open_v2 con flag READWRITE | CREATE e nessun URI. Per controllo fine preferisci sempre open_v2.int sqlite3_close (sqlite3 *db); // fallisce se ci sono stmt/blob non finalizzati int sqlite3_close_v2(sqlite3 *db); // chiusura "zombie": chiude quando tutti gli stmt finalizzati
close() ritorna SQLITE_BUSY se esistono statement preparati ancora vivi. close_v2() marca la connessione per chiusura deferita e non fallisce mai per quel motivo.
/* Enumerare e finalizzare tutti gli stmt rimasti */ sqlite3_stmt *s = NULL; while ((s = sqlite3_next_stmt(db, s)) != NULL) sqlite3_finalize(s); sqlite3_close(db);
Con flag SQLITE_OPEN_URI (o macro di compile SQLITE_USE_URI=1), il filename diventa un URI con query string.
file:data.db // relativo file:/var/lib/app.db // assoluto file:data.db?mode=ro // read-only file:data.db?mode=rwc&cache=shared // rw+create, cache condivisa file::memory:?cache=shared // memoria condivisa tra connessioni file:/tmp/t.db?psow=0&nolock=1
| Parametro | Valori |
|---|---|
mode | ro / rw / rwc / memory |
cache | shared / private |
psow | Powersafe overwrite (0/1) |
nolock | Disabilita file locking (solo lettura) |
immutable | Il file non sarà mai modificato |
vfs | Nome VFS da usare |
int sqlite3_exec( sqlite3 *db, const char *sql, // SQL (può contenere più statement) int (*callback)(void*, int, char**, char**), void *arg, // passato al callback come 1° arg char **errmsg // out: msg errore (da liberare) );
Wrapper che fa prepare → step → finalize per ogni statement nella stringa SQL. Ideale per DDL e statement "fire and forget" senza parametri.
sqlite3_exec() con SQL costruito concatenando input utente — SQL injection. Usa prepared statement + bind.char *err = NULL; const char *ddl = "CREATE TABLE IF NOT EXISTS utenti (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " nome TEXT NOT NULL UNIQUE," " eta INTEGER CHECK(eta >= 0)" ");"; int rc = sqlite3_exec(db, ddl, NULL, NULL, &err); if (rc != SQLITE_OK) { fprintf(stderr, "DDL: %s\n", err); sqlite3_free(err); // errmsg allocato internamente }
static int dump_row(void *ud, int ncol, char **vals, char **cols) { (void)ud; for (int i = 0; i < ncol; i++) printf("%s=%s ", cols[i], vals[i] ? vals[i] : "NULL"); putchar('\n'); return 0; // !=0 interrompe l'esecuzione con SQLITE_ABORT } char *err = NULL; sqlite3_exec(db, "SELECT id,nome FROM utenti;", dump_row, NULL, &err);
int callback( void *user_data, // 4° arg di sqlite3_exec() int ncol, // numero colonne della riga char **values, // array di stringhe (valori come TEXT, NULL possibile) char **column_names // array di stringhe (nomi colonne) ); // return 0 = continua; return !=0 = abort (sqlite3_exec ritorna SQLITE_ABORT)
| Aspetto | Nota |
|---|---|
| Memoria | Tutti i puntatori sono validi solo durante la chiamata. Copia se serve tenerli. |
| Tipi | Tutti i valori arrivano come char* (stringa). Tipi nativi solo con prepared statement. |
| NULL | values[i] == NULL quando la colonna è NULL SQL. |
| Errori | Non chiamare altre API SQLite sulla stessa db dal callback. |
typedef struct { int count; long sum; } agg_t; static int aggregate(void *ud, int nc, char **v, char **cn) { (void)nc; (void)cn; agg_t *a = (agg_t *)ud; a->count++; if (v[0]) a->sum += atol(v[0]); return 0; } agg_t a = {0,0}; sqlite3_exec(db, "SELECT eta FROM utenti;", aggregate, &a, NULL); printf("%d righe, somma=%ld\n", a.count, a.sum);
sqlite3_column_*.int sqlite3_prepare_v2( sqlite3 *db, const char *zSql, // SQL UTF-8 int nByte, // lunghezza in byte, o -1 per strlen sqlite3_stmt **ppStmt, // out: stmt compilato const char **pzTail // out: primo byte dopo il primo stmt );
Compila un solo statement SQL. Se zSql contiene più statement, *pzTail punta all'inizio del successivo: loop su prepare_v2 per eseguirli tutti.
| Variante | Differenza |
|---|---|
sqlite3_prepare() | LEGACY: gestione errori peggiore, non usare. |
sqlite3_prepare_v2() | Versione standard raccomandata. |
sqlite3_prepare_v3() | Aggiunge flag (SQLITE_PREPARE_PERSISTENT, ecc.). |
sqlite3_prepare16_v2() | Input UTF-16. |
sqlite3_stmt *stmt = NULL; const char *sql = "INSERT INTO utenti(nome, eta) VALUES (?, ?);"; int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf(stderr, "prepare: %s\n", sqlite3_errmsg(db)); return -1; }
int sqlite3_prepare_v3(db, sql, -1, flags, &stmt, NULL);
| Flag | Significato |
|---|---|
SQLITE_PREPARE_PERSISTENT | Stmt riusato a lungo: hint per il planner (evita cache eviction). |
SQLITE_PREPARE_NORMALIZE | Abilita sqlite3_normalized_sql(). |
SQLITE_PREPARE_NO_VTAB | Non usare virtual table. |
int sqlite3_step(sqlite3_stmt *stmt);
| Ritorno | Significato |
|---|---|
SQLITE_ROW (100) | Riga disponibile: leggi con column_*, poi richiama step |
SQLITE_DONE (101) | Esecuzione completata |
SQLITE_BUSY (5) | DB lock, retry o abort |
SQLITE_CONSTRAINT | Violazione di vincolo (UNIQUE, CHECK, NOT NULL, FK) |
SQLITE_ERROR | Errore generico (controlla errmsg) |
SQLITE_MISUSE | Uso scorretto (es. step dopo finalize) |
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { int id = sqlite3_column_int (stmt, 0); const unsigned char *nome = sqlite3_column_text(stmt, 1); printf("%d %s\n", id, nome); } if (rc != SQLITE_DONE) fprintf(stderr, "step: %s\n", sqlite3_errmsg(db));
if (sqlite3_step(stmt) != SQLITE_DONE) { fprintf(stderr, "insert: %s\n", sqlite3_errmsg(db)); } sqlite3_int64 last_id = sqlite3_last_insert_rowid(db); int changes = sqlite3_changes(db);
int sqlite3_reset (sqlite3_stmt *stmt); int sqlite3_clear_bindings(sqlite3_stmt *stmt);
reset() riporta lo stato di esecuzione allo stadio "pronto per il primo step", senza cancellare i bind. clear_bindings() azzera tutti i parametri a NULL.
SQLITE_DONE, non è obbligatorio chiamare reset() prima di finalize(). Il motivo di usarlo è riusare lo stesso statement con nuovi bind.const char *sql = "INSERT INTO utenti(nome, eta) VALUES (?,?);"; sqlite3_stmt *s; sqlite3_prepare_v2(db, sql, -1, &s, NULL); for (int i = 0; i < n; i++) { sqlite3_bind_text(s, 1, names[i], -1, SQLITE_TRANSIENT); sqlite3_bind_int (s, 2, ages[i]); if (sqlite3_step(s) != SQLITE_DONE) fprintf(stderr, "err: %s\n", sqlite3_errmsg(db)); sqlite3_reset(s); // pronto per il prossimo sqlite3_clear_bindings(s); // opzionale (nuovi bind li sovrascrivono) } sqlite3_finalize(s);
int sqlite3_finalize(sqlite3_stmt *stmt);
Distrugge lo statement e libera tutte le risorse associate. Sicura da chiamare su NULL (noop). Va chiamata sempre prima di sqlite3_close() (ma non prima di close_v2()).
sqlite3_close() ritorna SQLITE_BUSY e il database rimane aperto.#define SQL_FINALIZE(s) do { \ if (s) { sqlite3_finalize(s); s = NULL; } \ } while(0) sqlite3_stmt *stmt = NULL; /* ... prepare, bind, step ... */ SQL_FINALIZE(stmt);
int sqlite3_bind_int (sqlite3_stmt*, int i, int v); int sqlite3_bind_int64 (sqlite3_stmt*, int i, sqlite3_int64 v); int sqlite3_bind_double (sqlite3_stmt*, int i, double v); int sqlite3_bind_text (sqlite3_stmt*, int i, const char* v, int n, void(*fin)(void*)); int sqlite3_bind_text16 (sqlite3_stmt*, int i, const void* v, int n, void(*fin)(void*)); int sqlite3_bind_blob (sqlite3_stmt*, int i, const void* v, int n, void(*fin)(void*)); int sqlite3_bind_zeroblob(sqlite3_stmt*, int i, int n); int sqlite3_bind_null (sqlite3_stmt*, int i); int sqlite3_bind_value (sqlite3_stmt*, int i, const sqlite3_value*); int sqlite3_bind_pointer(sqlite3_stmt*, int i, void*, const char* type, void(*fin)(void*));
sqlite3_column_* parte invece da 0. Facile fonte di bug.L'ultimo parametro di bind_text/blob indica a SQLite come gestire il buffer passato.
| Valore | Semantica |
|---|---|
SQLITE_STATIC | Il buffer è valido per tutta la vita dello statement (no copia). |
SQLITE_TRANSIENT | SQLite copia subito il dato; il buffer originale può essere liberato. |
| Puntatore a funzione | Chiamata da SQLite quando il dato non serve più (free, custom, ecc.). |
char buf[64]; snprintf(buf, sizeof buf, "Mario-%d", i); // STATIC: buf è stabile per tutta la vita dello stmt? No, è temporaneo → TRANSIENT sqlite3_bind_text(s, 1, buf, -1, SQLITE_TRANSIENT); // Stringa letterale: TEXT segment immortale → STATIC sqlite3_bind_text(s, 2, "admin", -1, SQLITE_STATIC); // Buffer malloc-ato che vogliamo cedere: passa free char *heap = strdup("xxxx"); sqlite3_bind_text(s, 3, heap, -1, free);
SQLITE_STATIC con un buffer automatico/di stack che esce di scope è undefined behavior. In dubbio: SQLITE_TRANSIENT.Parametro n | Significato in bind_text |
|---|---|
n = -1 | Usa strlen per determinare la lunghezza (stringa C terminata) |
n >= 0 | Numero di byte (non include \0 terminatore) |
/* Bind di un BLOB */ unsigned char data[256]; sqlite3_bind_blob(s, 1, data, sizeof data, SQLITE_TRANSIENT); /* Bind di BLOB vuoto di N byte (da riempire con incremental I/O) */ sqlite3_bind_zeroblob(s, 1, 1024); /* Bind di NULL esplicito */ sqlite3_bind_null(s, 3);
| Forma | Descrizione |
|---|---|
? | Anonimo, posizione automatica |
?NNN | Posizionale esplicito (NNN = 1..999) |
:nome | Nominato (colon-prefix) |
@nome | Nominato (at-prefix, stile T-SQL) |
$nome | Nominato (dollar-prefix, stile TCL) |
int sqlite3_bind_parameter_index(sqlite3_stmt*, const char *zName); const char* sqlite3_bind_parameter_name(sqlite3_stmt*, int i); int sqlite3_bind_parameter_count(sqlite3_stmt*);
Il nome include il prefisso (es. ":nome", non "nome").
const char *sql = "UPDATE utenti SET eta = :eta WHERE nome = :nome;"; sqlite3_stmt *s; sqlite3_prepare_v2(db, sql, -1, &s, NULL); int iEta = sqlite3_bind_parameter_index(s, ":eta"); int iNome = sqlite3_bind_parameter_index(s, ":nome"); sqlite3_bind_int (s, iEta, 30); sqlite3_bind_text(s, iNome, "Mario", -1, SQLITE_STATIC); sqlite3_step(s); sqlite3_finalize(s);
const char* sqlite3_sql (sqlite3_stmt*); // testo originale char* sqlite3_expanded_sql (sqlite3_stmt*); // con parametri bind-ati (free con sqlite3_free) const char* sqlite3_normalized_sql(sqlite3_stmt*); // forma normalizzata (prepare_v3) int sqlite3_stmt_readonly (sqlite3_stmt*); // != 0 se non modifica il DB int sqlite3_stmt_busy (sqlite3_stmt*); // != 0 se in corso di esecuzione int sqlite3_data_count (sqlite3_stmt*); // colonne dopo SQLITE_ROW int sqlite3_column_count (sqlite3_stmt*); // colonne della query
int sqlite3_column_int (sqlite3_stmt*, int i); sqlite3_int64 sqlite3_column_int64 (sqlite3_stmt*, int i); double sqlite3_column_double (sqlite3_stmt*, int i); const unsigned char* sqlite3_column_text (sqlite3_stmt*, int i); const void* sqlite3_column_text16 (sqlite3_stmt*, int i); const void* sqlite3_column_blob (sqlite3_stmt*, int i); int sqlite3_column_bytes (sqlite3_stmt*, int i); int sqlite3_column_bytes16(sqlite3_stmt*, int i); int sqlite3_column_type (sqlite3_stmt*, int i); sqlite3_value* sqlite3_column_value (sqlite3_stmt*, int i);
| Funzione | Nota |
|---|---|
column_text() | Ritorna const unsigned char* UTF-8, terminata da \0. |
column_blob() | NON terminata; usa column_bytes() per la dimensione. |
column_bytes() | Numero di byte del valore BLOB/TEXT (escluso \0). |
column_type() | Ritorna SQLITE_INTEGER, _FLOAT, _TEXT, _BLOB, _NULL. |
column_text/blob sono validi solo fino alla prossima sqlite3_step/reset/finalize sullo stesso stmt, o a una qualsiasi conversione di tipo su quella colonna. Copia subito se serve persistere.const char *sql = "SELECT id, nome, eta FROM utenti WHERE eta > ?;"; sqlite3_stmt *s; if (sqlite3_prepare_v2(db, sql, -1, &s, NULL) != SQLITE_OK) goto err; sqlite3_bind_int(s, 1, 18); int rc; while ((rc = sqlite3_step(s)) == SQLITE_ROW) { int id = sqlite3_column_int (s, 0); const unsigned char *n = sqlite3_column_text (s, 1); int eta = sqlite3_column_int (s, 2); if (sqlite3_column_type(s, 1) == SQLITE_NULL) n = (const unsigned char*)"(null)"; printf("%3d | %-20s | %d\n", id, n, eta); } if (rc != SQLITE_DONE) fprintf(stderr, "step: %s\n", sqlite3_errmsg(db)); err: sqlite3_finalize(s);
const char* sqlite3_column_name (sqlite3_stmt*, int N); const char* sqlite3_column_decltype (sqlite3_stmt*, int N); /* Richiedono SQLITE_ENABLE_COLUMN_METADATA */ const char* sqlite3_column_database_name(sqlite3_stmt*, int N); const char* sqlite3_column_table_name (sqlite3_stmt*, int N); const char* sqlite3_column_origin_name (sqlite3_stmt*, int N);
column_decltype ritorna il tipo dichiarato nella CREATE TABLE (es. "INTEGER", "VARCHAR(50)"), non il tipo storage corrente. SQLite è typeless: usa type affinity.
int nc = sqlite3_column_count(s); for (int i = 0; i < nc; i++) printf("%-15s ", sqlite3_column_name(s, i)); putchar('\n');
In SQLite i valori hanno tipo, le colonne hanno affinità. Una colonna INTEGER può contenere un TEXT (se non c'è STRICT).
| Tipo Dichiarato | Affinità |
|---|---|
| INT, INTEGER, BIGINT, SMALLINT... | INTEGER |
| TEXT, CHAR, VARCHAR(n), CLOB | TEXT |
| BLOB, (nessun tipo) | BLOB |
| REAL, DOUBLE, FLOAT | REAL |
| NUMERIC, DECIMAL, BOOLEAN, DATE | NUMERIC |
STRICT rifiutano tipi non conformi. Dal 3.38: tipo ANY esplicito per colonne senza conversione.SQLite è ACID. Per default opera in autocommit: ogni statement è una transazione. BEGIN apre una transazione esplicita, COMMIT la conferma, ROLLBACK la annulla.
sqlite3_exec(db, "BEGIN TRANSACTION;", 0, 0, 0); /* ... molte INSERT/UPDATE ... */ sqlite3_exec(db, "COMMIT;", 0, 0, 0); // oppure, su errore: sqlite3_exec(db, "ROLLBACK;", 0, 0, 0);
int bulk_insert(sqlite3 *db, const char **names, int n) { char *err = NULL; int rc; if (sqlite3_exec(db, "BEGIN IMMEDIATE;", 0, 0, &err) != SQLITE_OK) goto fail; sqlite3_stmt *s; sqlite3_prepare_v2(db, "INSERT INTO utenti(nome) VALUES(?);", -1, &s, NULL); for (int i = 0; i < n; i++) { sqlite3_bind_text(s, 1, names[i], -1, SQLITE_STATIC); if (sqlite3_step(s) != SQLITE_DONE) { sqlite3_finalize(s); goto rollback; } sqlite3_reset(s); } sqlite3_finalize(s); return sqlite3_exec(db, "COMMIT;", 0, 0, 0); rollback: sqlite3_exec(db, "ROLLBACK;", 0, 0, 0); fail: sqlite3_free(err); return -1; }
SAVEPOINT sp1; -- crea punto di salvataggio -- ... statement ... RELEASE sp1; -- "commit" del savepoint ROLLBACK TO sp1; -- rollback parziale, la tx principale resta attiva
I savepoint sono annidabili, si comportano come mini-transazioni dentro una transazione. Utili per parsing/import a stadi con recupero.
sqlite3_exec(db, "BEGIN;", 0,0,0); sqlite3_exec(db, "SAVEPOINT chunk;", 0,0,0); if (processing_failed) sqlite3_exec(db, "ROLLBACK TO chunk;", 0,0,0); else sqlite3_exec(db, "RELEASE chunk;", 0,0,0); sqlite3_exec(db, "COMMIT;", 0,0,0);
| Sintassi | Lock acquisito | Quando |
|---|---|---|
BEGIN / BEGIN DEFERRED | Nessuno inizialmente; SHARED sul primo SELECT, RESERVED sul primo WRITE | Default. Rischio SQLITE_BUSY su upgrade |
BEGIN IMMEDIATE | RESERVED subito | Per transazioni che sicuramente scriveranno |
BEGIN EXCLUSIVE | EXCLUSIVE subito (no altri reader, non-WAL) | Operazioni esclusive (rare) |
BEGIN IMMEDIATE è la scelta sicura per evitare deadlock di upgrade.PRAGMA journal_mode = WAL; -- Write-Ahead Logging (concorrenza ottima) PRAGMA journal_mode = DELETE; -- default classico PRAGMA journal_mode = TRUNCATE; PRAGMA journal_mode = PERSIST; PRAGMA journal_mode = MEMORY; -- non durabile PRAGMA journal_mode = OFF; -- NO rollback, NO crash safety PRAGMA synchronous = FULL; -- default, max durabilità PRAGMA synchronous = NORMAL; -- OK con WAL, 2–3× più veloce PRAGMA synchronous = OFF; -- rischio corruzione su crash
int sqlite3_get_autocommit(sqlite3 *db); // 1 = autocommit attivo (nessuna tx aperta) // 0 = tx in corso int sqlite3_txn_state(sqlite3 *db, const char *zSchema); // SQLITE_TXN_NONE (0) : nessuna tx // SQLITE_TXN_READ (1) : tx di sola lettura // SQLITE_TXN_WRITE (2) : tx di scrittura
Utile per wrapping difensivo: se la routine ritorna con get_autocommit() == 0, c'è una transazione dimenticata.
| Codice | Val | Significato |
|---|---|---|
SQLITE_OK | 0 | Successo |
SQLITE_ERROR | 1 | Errore SQL / generico |
SQLITE_INTERNAL | 2 | Bug interno (non dovresti vederlo) |
SQLITE_PERM | 3 | Permessi insufficienti |
SQLITE_ABORT | 4 | Abort (callback ha ritornato !=0) |
SQLITE_BUSY | 5 | File lock; ritenta |
SQLITE_LOCKED | 6 | Deadlock sulla stessa connessione |
SQLITE_NOMEM | 7 | malloc fallito |
SQLITE_READONLY | 8 | Scrittura su DB read-only |
SQLITE_INTERRUPT | 9 | sqlite3_interrupt() |
SQLITE_IOERR | 10 | Errore I/O sul VFS |
SQLITE_CORRUPT | 11 | File corrotto |
SQLITE_NOTFOUND | 12 | Non trovato |
SQLITE_FULL | 13 | DB pieno (disco) |
SQLITE_CANTOPEN | 14 | Impossibile aprire il file |
SQLITE_SCHEMA | 17 | Schema cambiato (stmt va ricompilato) |
SQLITE_CONSTRAINT | 19 | Violazione vincolo |
SQLITE_MISMATCH | 20 | Tipo incompatibile |
SQLITE_MISUSE | 21 | API usata male |
SQLITE_RANGE | 25 | Indice bind fuori range |
SQLITE_NOTADB | 26 | File non è un DB SQLite |
SQLITE_ROW | 100 | Riga disponibile |
SQLITE_DONE | 101 | Esecuzione completata |
int sqlite3_errcode (sqlite3*); // codice primario int sqlite3_extended_errcode(sqlite3*); // codice esteso const char* sqlite3_errmsg (sqlite3*); // messaggio, NUL-term const char* sqlite3_errmsg16 (sqlite3*); const char* sqlite3_errstr (int rc); // descrizione statica int sqlite3_error_offset (sqlite3*); // offset token errore int sqlite3_extended_result_codes(sqlite3*, int onoff); // attiva extended
Ogni codice base può essere esteso come CODICE_BASE | (sub_code << 8). Permette diagnosi fine.
| Extended | Significato |
|---|---|
SQLITE_CONSTRAINT_PRIMARYKEY | Violazione PK |
SQLITE_CONSTRAINT_UNIQUE | UNIQUE fallita |
SQLITE_CONSTRAINT_NOTNULL | NOT NULL violato |
SQLITE_CONSTRAINT_CHECK | CHECK fallita |
SQLITE_CONSTRAINT_FOREIGNKEY | FK violata |
SQLITE_IOERR_WRITE | I/O in scrittura |
SQLITE_IOERR_FSYNC | fsync fallito |
SQLITE_READONLY_RECOVERY | Serve recupero WAL |
SQLITE_BUSY_RECOVERY | Recovery WAL in corso |
SQLITE_BUSY_SNAPSHOT | Snapshot troppo vecchio (WAL) |
static void sql_die(sqlite3 *db, const char *ctx) { fprintf(stderr, "[%s] rc=%d ext=%d off=%d: %s\n", ctx, sqlite3_errcode(db), sqlite3_extended_errcode(db), sqlite3_error_offset(db), sqlite3_errmsg(db)); exit(1); } /* Abilita extended codes all'inizio */ sqlite3_extended_result_codes(db, 1);
int sqlite3_busy_timeout(sqlite3*, int ms); int sqlite3_busy_handler(sqlite3*, int(*cb)(void*,int), void*);
busy_timeout(ms) imposta un handler built-in che effettua sleep-retry per fino a ms millisecondi prima di ritornare SQLITE_BUSY. Zero lo disabilita.
static int my_busy(void *ud, int n) { (void)ud; if (n > 10) return 0; // ritorna 0 → SQLite dà SQLITE_BUSY usleep(100000); // 100 ms return 1; // ritorna !=0 → ritenta } sqlite3_busy_handler(db, my_busy, NULL);
busy_timeout(5000) è sufficiente.PRAGMA foreign_keys = ON; -- attiva enforcement FK (default OFF) PRAGMA journal_mode = WAL; -- concorrenza read/write PRAGMA synchronous = NORMAL; -- bilanciato con WAL PRAGMA temp_store = MEMORY; -- tmp in RAM PRAGMA cache_size = -20000; -- 20 MB di cache (negativo = KB) PRAGMA mmap_size = 268435456; -- 256 MB di mmap PRAGMA page_size = 4096; -- solo prima del primo CREATE PRAGMA integrity_check; -- verifica integrità PRAGMA quick_check; -- verifica veloce PRAGMA wal_checkpoint(TRUNCATE); -- checkpoint WAL PRAGMA optimize; -- ANALYZE selettivo (3.18+)
PRAGMA table_info('utenti'); PRAGMA table_list; -- 3.37+ PRAGMA index_list('utenti'); PRAGMA index_info('idx_nome'); PRAGMA foreign_key_list('ordini'); PRAGMA database_list; PRAGMA user_version = 3; -- int 32 libero per schema versioning
int sqlite3_blob_open( sqlite3 *db, const char *zDb, const char *zTable, const char *zColumn, sqlite3_int64 iRow, int flags, sqlite3_blob **ppBlob); int sqlite3_blob_bytes (sqlite3_blob*); int sqlite3_blob_read (sqlite3_blob*, void *buf, int n, int off); int sqlite3_blob_write (sqlite3_blob*, const void*, int n, int off); int sqlite3_blob_reopen (sqlite3_blob*, sqlite3_int64 newRow); int sqlite3_blob_close (sqlite3_blob*);
Permette lettura/scrittura di un BLOB a chunk, senza caricarlo tutto in memoria. La dimensione è fissa (non può crescere): prima allocala con zeroblob().
/* Pre-allocare 1 MB */ sqlite3_exec(db, "INSERT INTO files(name,data) VALUES('foo.bin', zeroblob(1048576));", 0,0,0); sqlite3_int64 rowid = sqlite3_last_insert_rowid(db); sqlite3_blob *b; sqlite3_blob_open(db, "main", "files", "data", rowid, 1, &b); char chunk[4096]; for (int off = 0; off < 1048576; off += sizeof chunk) { /* ... riempi chunk ... */ sqlite3_blob_write(b, chunk, sizeof chunk, off); } sqlite3_blob_close(b);
sqlite3 *src, *dst; sqlite3_open("live.db", &src); sqlite3_open("backup.db", &dst); sqlite3_backup *bkp = sqlite3_backup_init(dst, "main", src, "main"); if (bkp) { /* copia tutte le pagine in una volta (-1) oppure in step */ do { int rc = sqlite3_backup_step(bkp, 100); // 100 pagine per step if (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED) { sqlite3_sleep(50); continue; } if (rc == SQLITE_DONE) break; fprintf(stderr, "backup: %s\n", sqlite3_errmsg(dst)); break; } while (1); sqlite3_backup_finish(bkp); } sqlite3_close(src); sqlite3_close(dst);
sqlite3_backup_remaining() e sqlite3_backup_pagecount().| Funzione | Trigger |
|---|---|
sqlite3_commit_hook() | Prima di ogni COMMIT |
sqlite3_rollback_hook() | Prima di ogni ROLLBACK |
sqlite3_update_hook() | Dopo INSERT/UPDATE/DELETE su tabella |
sqlite3_trace_v2() | Tracing SQL (statement/profile/row/close) |
sqlite3_progress_handler() | Ogni N VM opcode (abort di query lunghe) |
sqlite3_wal_hook() | Dopo COMMIT in WAL mode |
sqlite3_authorizer() | Controllo permessi a compile-time di prepare |
static int progress(void *ud) { return cancel_requested ? 1 : 0; // !=0 → SQLITE_INTERRUPT } sqlite3_progress_handler(db, 1000, progress, NULL); /* Interruzione immediata da altro thread: */ sqlite3_interrupt(db);
static void audit(void *ud, int op, const char *db_name, const char *tbl, sqlite3_int64 rowid) { const char *o = (op==SQLITE_INSERT) ? "INS" : (op==SQLITE_UPDATE) ? "UPD" : "DEL"; fprintf(stderr, "[%s] %s.%s rowid=%lld\n", o, db_name, tbl, rowid); } sqlite3_update_hook(db, audit, NULL);
int sqlite3_create_function( sqlite3 *db, const char *zName, // nome SQL della funzione int nArg, // numero argomenti (-1 = variadic) int eTextRep, // SQLITE_UTF8 | SQLITE_DETERMINISTIC void *pApp, void (*xFunc)(sqlite3_context*, int, sqlite3_value**), void (*xStep)(sqlite3_context*, int, sqlite3_value**), // per aggregati void (*xFinal)(sqlite3_context*) );
static void sq_fn(sqlite3_context *ctx, int n, sqlite3_value **argv) { if (n < 1) { sqlite3_result_null(ctx); return; } double x = sqlite3_value_double(argv[0]); sqlite3_result_double(ctx, x * x); } sqlite3_create_function(db, "sq", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, sq_fn, NULL, NULL); /* Ora da SQL: SELECT sq(5.0); → 25.0 */
| Set di result | Uso |
|---|---|
sqlite3_result_int / _int64 | Intero |
sqlite3_result_double | Float |
sqlite3_result_text | Stringa (con TRANSIENT/STATIC) |
sqlite3_result_blob | Buffer binario |
sqlite3_result_null | NULL SQL |
sqlite3_result_error | Propaga errore |
void* sqlite3_malloc (int n); // int: max 2 GB void* sqlite3_malloc64(sqlite3_uint64 n); void* sqlite3_realloc(void*, int); void sqlite3_free (void*); char* sqlite3_mprintf(const char*, ...); // printf-like, libera con sqlite3_free char* sqlite3_vmprintf(const char*, va_list);
Stringhe e messaggi ritornati/richiesti da API SQLite (es. errmsg di exec, expanded_sql) devono essere liberati con sqlite3_free, non con free().
/* %q : escape di singola quote in stringa SQL (no quote esterne) %Q : come %q, ma aggiunge le quote (o NULL se pointer == NULL) %w : escape per identificatori (nomi tabella/colonna) */ char *sql = sqlite3_mprintf( "INSERT INTO log VALUES(%Q);", user_input); sqlite3_exec(db, sql, 0,0,0); sqlite3_free(sql);
sprintf/strcat per costruire SQL con dati utente. Preferisci sempre prepared statement + bind. Se proprio devi, usa %Q.| Modalità | Descrizione |
|---|---|
Single-thread (THREADSAFE=0) | No locking. DB accessibile da un solo thread. |
Multi-thread (THREADSAFE=2) | Thread diversi possono usare connessioni diverse. |
Serialized (THREADSAFE=1, default) | Qualsiasi thread, qualsiasi connessione. Lock interno. |
int sqlite3_config(SQLITE_CONFIG_SINGLETHREAD); int sqlite3_config(SQLITE_CONFIG_MULTITHREAD); int sqlite3_config(SQLITE_CONFIG_SERIALIZED); int sqlite3_threadsafe(void); // 0/1/2
sqlite3_stmt* tra thread.#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sqlite3.h> #define SQL_OK(db, rc) \ do { if ((rc) != SQLITE_OK && (rc) != SQLITE_DONE \ && (rc) != SQLITE_ROW) { \ fprintf(stderr, "[%s:%d] %s\n", __FILE__, __LINE__, \ sqlite3_errmsg(db)); exit(1); } } while(0) static sqlite3 *db_open(const char *path) { sqlite3 *db; int rc = sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); SQL_OK(db, rc); sqlite3_extended_result_codes(db, 1); sqlite3_busy_timeout(db, 5000); sqlite3_exec(db, "PRAGMA journal_mode=WAL;", 0, 0, 0); sqlite3_exec(db, "PRAGMA foreign_keys=ON;", 0, 0, 0); return db; } static void db_init(sqlite3 *db) { const char *ddl = "CREATE TABLE IF NOT EXISTS utenti (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " nome TEXT NOT NULL UNIQUE," " eta INTEGER CHECK(eta >= 0));"; char *err = NULL; if (sqlite3_exec(db, ddl, 0, 0, &err) != SQLITE_OK) { fprintf(stderr, "ddl: %s\n", err); sqlite3_free(err); exit(1); } } static sqlite3_int64 user_insert(sqlite3 *db, const char *nome, int eta) { sqlite3_stmt *s; SQL_OK(db, sqlite3_prepare_v2(db, "INSERT INTO utenti(nome,eta) VALUES(?1,?2);", -1, &s, NULL)); sqlite3_bind_text(s, 1, nome, -1, SQLITE_TRANSIENT); sqlite3_bind_int (s, 2, eta); int rc = sqlite3_step(s); sqlite3_finalize(s); if (rc != SQLITE_DONE) { fprintf(stderr, "ins: %s\n", sqlite3_errmsg(db)); return -1; } return sqlite3_last_insert_rowid(db); } static int user_find_by_name(sqlite3 *db, const char *nome, int *out_id, int *out_eta) { sqlite3_stmt *s; SQL_OK(db, sqlite3_prepare_v2(db, "SELECT id,eta FROM utenti WHERE nome=?1;", -1, &s, NULL)); sqlite3_bind_text(s, 1, nome, -1, SQLITE_STATIC); int rc = sqlite3_step(s), found = 0; if (rc == SQLITE_ROW) { *out_id = sqlite3_column_int(s, 0); *out_eta = sqlite3_column_int(s, 1); found = 1; } sqlite3_finalize(s); return found; } int main(void) { sqlite3 *db = db_open("app.db"); db_init(db); sqlite3_exec(db, "BEGIN;", 0, 0, 0); user_insert(db, "Mario", 30); user_insert(db, "Luisa", 25); sqlite3_exec(db, "COMMIT;", 0, 0, 0); int id, eta; if (user_find_by_name(db, "Mario", &id, &eta)) printf("Mario id=%d eta=%d\n", id, eta); sqlite3_close(db); return 0; }
Do:
• Una transazione per N operazioni batch
• journal_mode=WAL per concorrenza
• synchronous=NORMAL con WAL
• Riusa prepared statement (prepare+reset)
• Indici su colonne WHERE/JOIN
• PRAGMA optimize a chiusura connessione
• EXPLAIN QUERY PLAN ... per diagnosi
• mmap_size su DB read-heavy
Don't:
• Concatenare stringhe per costruire SQL
• Una transazione per ogni INSERT
• synchronous=OFF senza accettare data loss
• Tenere transazioni aperte a lungo
• Aprire/chiudere connessione per ogni query
• Ignorare SQLITE_BUSY (imposta timeout)
• SELECT * quando servono 2 colonne
• Condividere sqlite3* tra thread senza serializzare
sqlite3_stmt *s; sqlite3_prepare_v2(db, "EXPLAIN QUERY PLAN SELECT * FROM utenti WHERE nome=?;", -1, &s, NULL); sqlite3_bind_text(s, 1, "Mario", -1, SQLITE_STATIC); while (sqlite3_step(s) == SQLITE_ROW) printf("%s\n", sqlite3_column_text(s, 3)); sqlite3_finalize(s); /* Output atteso (con indice): SEARCH utenti USING INDEX sqlite_autoindex_utenti_1 (nome=?) Senza indice: SCAN utenti ← attenzione, full table scan */
| Scenario | INSERT/sec (tipico SSD) |
|---|---|
| 1 INSERT per transazione (autocommit) | ∼ 100–500 |
| Batch da 1000 in una BEGIN/COMMIT | ∼ 50.000–200.000 |
WAL + synchronous=NORMAL | +30–50% |
| Prepared statement riusato | +20–40% |
.timer on in sqlite3 CLI).