Documentation
¶
Overview ¶
Package sqlite provides a Go wrapper around SQLite with transaction management, automatic backups, and type-safe query helpers.
Features ¶
- WAL Mode: Uses Write-Ahead Logging for better concurrent read performance
- Single-Writer/Multi-Reader: Serialized writes with concurrent reads via separate connection pools
- Automatic Backups: Periodic background backups with smart retention policies
- Transaction Tracking: Context-based tracking to prevent nested transactions
- Type-Safe Queries: Generic helpers for scanning rows into typed values
- JSON Object Storage: Store and retrieve Go structs as JSON blobs
- Schema Generation: Code generators for SQL schemas from Go structs
Creating a Database ¶
Use New to create a database with background workers (periodic backups), or NewNoWorkers for tests and situations where background work is not desired:
db, err := sqlite.New("/path/to/db.sqlite", log.Printf)
if err != nil {
return err
}
defer db.Close()
Transactions ¶
All database operations require a context with a transaction tracker. Use NewContext to create one:
ctx := sqlite.NewContext()
// Read-only transaction
err := db.Read(ctx, func(tx *sqlite.Tx) error {
count, err := sqlite.QuerySingle[int](tx, "SELECT COUNT(*) FROM users")
return err
})
// Read/Write transaction
err := db.Write(ctx, "create-user", func(tx *sqlite.Tx) error {
_, err := tx.Exec("INSERT INTO users (name) VALUES (?)", "alice")
return err
})
Type-Safe Queries ¶
Generic helpers scan rows into typed values:
// Single value count, err := sqlite.QuerySingle[int](tx, "SELECT COUNT(*) FROM users") // Single row as JSON user, err := sqlite.QueryJSONRow[User](tx, "SELECT JSONObj FROM users WHERE id = ?", id) // Multiple rows ids, err := sqlite.QueryTypedRows[int64](tx, "SELECT id FROM users")
JSON Object Storage ¶
Store Go structs implementing ObjectWithMetadata using Put:
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
UpdatedAt time.Time `json:"updated_at"`
}
func (u User) GetID() int64 { return u.ID }
func (u *User) SetUpdatedAt(t time.Time) { u.UpdatedAt = t }
func (u User) TableName() string { return "users" }
err := sqlite.Put(tx, &user)
Schema Generation ¶
The schema subpackage provides tools for generating SQL schemas from Go structs and managing schema migrations. Use go:generate directives:
//go:generate go run pkg.maisem.dev/sqlite/schema/sqlgen -type User -output schema.sql //go:generate go run pkg.maisem.dev/sqlite/schema/embed -f schema.sql
The sqlgen tool generates:
- schema.sql: CREATE TABLE statements with JSONObj blob storage
- <pkg>_tables.go: TableName() methods for each type
Tables use a JSONObj BLOB column to store the struct as JSON, with generated columns for indexed fields. Use struct tags to control column generation:
type User struct {
ID int64 `json:"id"`
Email string `json:"email" sql:"stored,unique"`
TenantID int64 `json:"tenant_id" sql:"stored,index,fk:Tenant.ID"`
DeletedAt *time.Time `json:"deleted_at,omitempty" sql:"stored,omitempty"`
}
Supported sql tag options:
- stored: Create a STORED generated column
- virtual: Create a VIRTUAL generated column
- unique: Add a unique index on the column
- index: Add a non-unique index on the column
- omitempty: Allow NULL values (for optional fields)
- fk:<Type>.<Field>: Add a foreign key constraint
- inline: Embed metadata fields (for Metadata[ID] pattern)
For custom SQL statements (composite indexes, etc.), use //sqlgen: comments inside the struct definition:
type UserProject struct {
UserID UserID `sql:"stored"`
ProjectID ProjectID `sql:"stored"`
//sqlgen: CREATE UNIQUE INDEX user_project_unique ON user_projects (UserID, ProjectID);
}
The embed tool compresses schema.sql into schemas/v<N>.sql.gz for versioned migrations managed by [schema.Manager].
Backup Retention ¶
Automatic backups follow this retention policy:
- Keep all backups from the last hour
- Keep one backup per hour for the last 24 hours
- Keep one backup per day for the last 30 days
- Delete older backups (while respecting a minimum count)
Error Handling ¶
Use IsConstraintError to check for constraint violations (UNIQUE, etc.) and IsTableNotFoundError to check for missing tables.
Index ¶
- Variables
- func AttachTracker(ctx context.Context) context.Context
- func CreateWithSafeID[T any, ID ~int64](create func(id ID) (T, error)) (T, error)
- func InsertSafeID[T ~int64](tx *Tx, id T) error
- func InsertWithAutoID[T any](tx *Tx, table tableName, obj T) (int64, error)
- func IsConstraintError(err error) bool
- func IsTableNotFoundError(err error) bool
- func NewContext() context.Context
- func Put[O ObjectWithMetadata[ID], ID Key](tx *Tx, obj O) error
- func QueryJSONRow[T any](rx *Tx, query queryString, args ...any) (T, error)
- func QueryJSONRows[T any](rx *Tx, query queryString, args ...any) ([]T, error)
- func QuerySingle[T any](rx *Tx, query queryString, args ...any) (T, error)
- func QueryToCSV(tx *Tx, query queryString, args ...any) (string, error)
- func QueryTypedRow[T any](rx *Tx, query queryString, args ...any) (T, error)
- func QueryTypedRows[T any](rx *Tx, query queryString, args ...any) ([]T, error)
- func Read[T any](ctx context.Context, db *DB, fn func(*Tx) (T, error)) (T, error)
- func ReserveSafeIDTx[T ~int64](tx *Tx) (T, error)
- func ScanSingle[T any](row Scanner) (T, error)
- func ScanSingleJSON[T any](row Scanner) (T, error)
- func UnsafeQueryString(s string) queryString
- type DB
- func (db *DB) Backup(ctx context.Context, slug string) (string, error)
- func (db *DB) Close() error
- func (db *DB) CondWrite(ctx context.Context, why string, fn func(*Tx) error) error
- func (db *DB) Conn(ctx context.Context) (*sql.Conn, error)
- func (db *DB) InitSchema(schema string) error
- func (db *DB) Path() string
- func (db *DB) Read(ctx context.Context, fn func(*Tx) error) error
- func (db *DB) ReadTx(ctx context.Context) (_ *Tx, err error)
- func (db *DB) ReadTxWithWhy(ctx context.Context, why string) (_ *Tx, err error)
- func (db *DB) ReadWithWhy(ctx context.Context, why string, fn func(*Tx) error) error
- func (db *DB) RxManager(ctx context.Context) *RxManager
- func (db *DB) ScrubBackups(ctx context.Context, keepAtLeast int) error
- func (db *DB) Tx(ctx context.Context, why string) (_ *Tx, err error)
- func (db *DB) Write(ctx context.Context, why string, fn func(*Tx) error) error
- type Key
- type ObjectWithMetadata
- type Result
- type RxManager
- type Scanner
- type Stmt
- type Tx
- func (tx *Tx) Commit() error
- func (tx *Tx) Exec(query queryString, args ...any) (sql.Result, error)
- func (tx *Tx) ExecAndReturnLastInsertID(query queryString, args ...any) (int64, error)
- func (tx *Tx) ExecAndReturnRowsAffected(query queryString, args ...any) (int64, error)
- func (tx *Tx) OnCommit(fn func())
- func (tx *Tx) OnRollback(fn func())
- func (tx *Tx) Prepare(query queryString) (*Stmt, error)
- func (tx *Tx) Query(query queryString, args ...any) (*sql.Rows, error)
- func (tx *Tx) QueryRow(query queryString, args ...any) *sql.Row
- func (tx *Tx) Rollback() error
- func (tx *Tx) UTCNow() time.Time
- func (tx *Tx) Writable() bool
- type TxTracker
Constants ¶
This section is empty.
Variables ¶
var ErrNeedWriteTx = errors.New("need write tx")
var ErrSkipCommit = errors.New("skip commit")
Functions ¶
func AttachTracker ¶
AttachTracker attaches a TxTracker to the context.
func CreateWithSafeID ¶
CreateWithSafeID attempts to create an object with a unique ID by retrying with different IDs when constraint errors occur. It calls the create function with generated IDs until successful or max attempts reached.
func InsertSafeID ¶
InsertSafeID stores a SafeID in the database with its type using the provided transaction.
func IsConstraintError ¶
IsConstraintError reports whether err represents a SQLITE_CONSTRAINT error.
func IsTableNotFoundError ¶
func NewContext ¶
NewContext returns a new context with a TxTracker attached. It should only be used for root contexts, to attach a TxTracker to a child context use AttachTracker.
func Put ¶
func Put[O ObjectWithMetadata[ID], ID Key](tx *Tx, obj O) error
Put inserts or replaces an object in the table with the given ID.
func QueryToCSV ¶
QueryToCSV executes a SQL query and returns the results formatted as CSV. The first row contains column names, and subsequent rows contain the data. NULL values are represented as "NULL", and strings containing commas, quotes, newlines, tabs or spaces are properly quoted.
func QueryTypedRow ¶
QueryTypedRow executes a query and returns the result as a single value of the given type. The query must return a single column.
func QueryTypedRows ¶
QueryTypedRows executes a query and returns the results as a slice of the given type. The query must return a single column.
func ReserveSafeIDTx ¶
ReserveSafeIDTx generates a new SafeID of the specified type and stores it in the database using the provided transaction. It uses collision-resistant generation with retry logic.
func ScanSingle ¶
ScanSingle scans a single value from a row.
func ScanSingleJSON ¶
ScanSingleJSON scans a single JSON value from a row.
func UnsafeQueryString ¶
func UnsafeQueryString(s string) queryString
UnsafeQueryString creates a [queryString] from a string. This is unsafe because it allows the caller to pass constructed queries to the database which may lead to SQL injection if not used carefully.
Types ¶
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is a Read/Write wrapper around a sqlite database. It is safe to use DB from multiple goroutines.
func NewNoWorkers ¶
NewNoWorkers creates a new DB instance without starting the background backup worker.
func (*DB) Backup ¶
Backup creates a new backup of the database. It returns the path to the backup file.
If the database has not changed since the last backup, it skips the backup and returns an empty string.
func (*DB) InitSchema ¶
InitSchema initializes the database schema by executing the provided SQL script. This is typically called during database initialization to create necessary tables.
func (*DB) ReadTxWithWhy ¶
func (*DB) ReadWithWhy ¶
ReadWithWhy executes a function in a read-only transaction with the specified reason. The reason is used for monitoring and debugging purposes.
func (*DB) ScrubBackups ¶
ScrubBackups removes old backups according to retention policies: - Keep all backups from the last hour - Keep one backup per hour for the last 24 hours - Keep one backup per day for the last 30 days - Delete everything else
type ObjectWithMetadata ¶
type Stmt ¶
type Stmt struct {
// contains filtered or unexported fields
}
Stmt is a wrapper around sql.Stmt.
type Tx ¶
type Tx struct {
// contains filtered or unexported fields
}
Tx is a wrapper around sql.Tx.
func (*Tx) ExecAndReturnLastInsertID ¶
ExecAndReturnLastInsertID executes a query and returns the last insert ID.
func (*Tx) ExecAndReturnRowsAffected ¶
ExecAndReturnRowsAffected executes a query and returns the number of rows affected.
func (*Tx) OnCommit ¶
func (tx *Tx) OnCommit(fn func())
OnCommit registers a callback to be called when the transaction is committed, but before the application-level write lock is released.
func (*Tx) OnRollback ¶
func (tx *Tx) OnRollback(fn func())
OnRollback registers a callback to be called when the transaction is rolled back. Exactly one of OnCommit and OnRollback will be called, but not both.
func (*Tx) QueryRow ¶
QueryRow executes QueryRowContext on the transaction using the transaction's context.