Help us shape the future of Go! Take the 2026 Go Developer Survey

db

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package db is WebThings' small layer over SQL. It keeps one connection pool for SQLite, Postgres or MySQL, so queries are written once with `?` placeholders and work on all three. A transaction travels in the context (see DB.Tx), so code called inside one joins it without passing it along. Core and every module bring their own schema files, applied at start (see DB.ApplySchema).

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("not_found")

ErrNotFound is returned by Get when no row matches.

View Source
var ErrSchemaChanged = errors.New("schema file changed after it was applied")

ErrSchemaChanged is returned by ApplySchema for a schema file whose statements changed after it was applied to the database.

Functions

func Affected

func Affected(result sql.Result, err error) error

Affected checks an UPDATE or DELETE meant for rows that must exist, such as one record by its ID. It returns err when the statement failed, the error of RowsAffected when the count is unknown, and ErrNotFound when no row matched. The results of Exec go in directly:

return db.Affected(database.Exec(ctx, `DELETE FROM wt_note WHERE id=?`, id))

Rows count when they match, also when the statement leaves their values as they were: Open sets MySQL up to count like SQLite and Postgres.

func AfterCommit

func AfterCommit(ctx context.Context, fn func())

AfterCommit calls fn once what was written with ctx is committed: at once outside a transaction, and after the commit of the transaction ctx carries (see Tx), so nothing that reads committed data, such as a cache that is told to forget, sees the state before the write again. When the transaction rolls back, fn is not called.

func AfterWrite

func AfterWrite(ctx context.Context, err error, fn func()) error

AfterWrite calls fn after the commit of the write that err reports on (see AfterCommit), when the write succeeded and fn is set, and returns err. Stores use it to tell a cache that what it keeps changed, such as the output cache through webthings.Env.Changed.

func InTx

func InTx(ctx context.Context) bool

InTx reports whether ctx carries a transaction. Caches use it to avoid keeping data that is not committed yet.

func IsForeignKey

func IsForeignKey(err error) bool

IsForeignKey reports a foreign key violation on any supported driver.

func IsUnique

func IsUnique(err error) bool

IsUnique reports a unique or primary key violation on any supported driver.

func JSONValue

func JSONValue(value any) (driver.Value, error)

JSONValue encodes value as JSON text, for the Value method of a type stored as JSON.

func NewID

func NewID() string

NewID returns a new time-ordered identifier (UUIDv7) in its 36-character text form. IDs are stored as VARCHAR(36) on SQLite and MySQL and as the native UUID type on Postgres; they travel as plain strings in every dialect.

func ScanJSON

func ScanJSON(src, target any) error

ScanJSON decodes the JSON text of a column into target, a pointer, for the Scan method of a type stored as JSON. NULL and empty text leave target as it is, so they read as the zero value.

func WithActor

func WithActor(ctx context.Context, userID string) context.Context

WithActor returns ctx with the ID of the user on whose behalf statements run, for the *_by audit columns.

Types

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB is the site's database. It is safe for concurrent use; every method takes part in the transaction its context carries, if any (see Tx).

func Open

func Open(ctx context.Context, driver, dsn string) (*DB, error)

Open connects and pings. Driver is one of sqlite, pgx or mysql; "sqlite3" and "postgres" are accepted as aliases. SQLite uses the pure-Go modernc.org/sqlite driver, so no cgo is needed for any dialect.

func (*DB) ApplySchema

func (db *DB) ApplySchema(ctx context.Context, owner string, files fs.FS) error

Schema files sit in one folder, named <datetime>[_description].<flavour>.sql:

schema/20261004000000_init.sqlite.sql
schema/20261004000000_init.postgres.sql
schema/20261004000000_init.mysql.sql
schema/20261120093000_add_avatar.sqlite.sql
...

The datetime (YYYYMMDDHHMMSS) is the version. Every version must exist for every flavour. ApplySchema runs the current flavour's files that are newer than the version recorded for owner in wt_schema, each in a transaction.

Before the first release every owner has only its _init files: edit them and recreate the development database. After a release, add new files instead of changing applied ones.

wt_schema_step keeps a checksum of each applied file's statements. A file whose statements changed after it was applied, also an _init file, makes ApplySchema return ErrSchemaChanged: the database does not have what the file now says. Changes of only whitespace or comments do not count. A database from before the checksums gets those of the files as they are.

func (*DB) Close

func (db *DB) Close() error

Close closes the connection pool. The App does this at Close.

func (*DB) Dialect

func (db *DB) Dialect() Dialect

Dialect returns the kind of database, for the queries that differ per database.

func (*DB) Exec

func (db *DB) Exec(ctx context.Context, query string, args ...any) (sql.Result, error)

Exec runs a statement that returns no rows, such as an INSERT, UPDATE or DELETE. Errors for values a column type rejects become ErrNotFound (see Get); see Affected to check that a row was there to change.

func (*DB) Get

func (db *DB) Get(ctx context.Context, dest any, query string, args ...any) error

Get scans one row into dest and returns ErrNotFound for no rows, and for values a column type rejects, such as text that is not a UUID in Postgres.

func (*DB) In

func (db *DB) In(query string, args ...any) (string, []any, error)

In expands `IN (?)` for a slice argument; Get, Select and Exec rebind it.

func (*DB) Ping

func (db *DB) Ping(ctx context.Context) error

Ping checks that the database answers, for readiness checks.

func (*DB) Query

func (db *DB) Query(ctx context.Context, query string, args ...any) (*sqlx.Rows, error)

Query runs a query and returns its rows to be read one at a time, for results too large to scan at once with Select. The caller closes the rows. Errors for values a column type rejects become ErrNotFound (see Get).

func (*DB) SchemaVersion

func (db *DB) SchemaVersion(files fs.FS) (int64, error)

SchemaVersion is the newest version among the schema files of this database's dialect, the version ApplySchema brings an owner to; 0 when there are none.

func (*DB) SchemaVersions

func (db *DB) SchemaVersions(ctx context.Context) (map[string]int64, error)

SchemaVersions returns the applied schema version of every owner, as recorded in wt_schema, in one query; readiness checks compare them with SchemaVersion.

func (*DB) Select

func (db *DB) Select(ctx context.Context, dest any, query string, args ...any) error

Select scans all rows into dest. Errors for values a column type rejects, such as an invalid UUID, become ErrNotFound (see Get).

func (*DB) Statements

func (db *DB) Statements() uint64

Statements is the number of statements this DB has sent so far. Tests use it to check that cached work needs no queries; it is cheap enough to keep for diagnostics.

func (*DB) Stats

func (db *DB) Stats() sql.DBStats

Stats are the connection pool's statistics, for metrics.

func (*DB) Tx

func (db *DB) Tx(ctx context.Context, fn func(context.Context) error) error

Tx runs fn in a transaction. Calls made with the supplied context join it; a nested Tx simply joins the outer transaction.

type Dialect

type Dialect string

Dialect is the kind of database behind a DB. It decides which schema files apply, and lets the few queries that differ per database choose their SQL.

const (
	// SQLite is the default: one file in the state directory, no server.
	SQLite Dialect = "sqlite"
	// Postgres is PostgreSQL, through the pgx driver.
	Postgres Dialect = "postgres"
	// MySQL is MySQL or MariaDB.
	MySQL Dialect = "mysql"
)

type Ref

type Ref string

Ref is an optional reference to a row, such as a created_by user ID. It is stored as NULL when empty and reads NULL as empty, so structs can use it for nullable ID columns.

func Actor

func Actor(ctx context.Context) Ref

Actor is the user ID WithActor put in ctx, as a Ref: empty, and so NULL in the database, for visitors and the system.

func Stamp

func Stamp(ctx context.Context) (time.Time, Ref)

Stamp returns the time and the author of a change, for the audit columns: the current time in UTC, cut to the microsecond that every supported database keeps, and Actor(ctx). A store takes one stamp per change and writes it to every row the change touches, so rows changed together carry equal times:

at, by := db.Stamp(ctx)

_, err := database.Exec(ctx, `
	UPDATE wt_note
	SET body=?, updated_at=?, updated_by=?
	WHERE id=? AND deleted_at IS NULL`,
	body, at, by, id,
)

func (*Ref) Scan

func (r *Ref) Scan(value any) error

Scan reads NULL as the empty Ref.

func (Ref) String

func (r Ref) String() string

String returns the ID.

func (Ref) Value

func (r Ref) Value() (driver.Value, error)

Value stores the empty Ref as NULL.

type SoftTable

type SoftTable struct {
	// DB is the database the table is in.
	DB *DB
	// Table is the name of the table, such as "wt_redirect". It goes into
	// the SQL as it is, so it never comes from input.
	Table string
	// Scope limits every statement to a part of the table, such as the rows
	// of one module instance: an SQL condition with ? placeholders, such as
	// "instance=?", that Args fill. Empty covers the whole table.
	Scope string
	// Args are the values of Scope's placeholders, in order.
	Args []any
}

SoftTable is a table whose rows go to the trash before they are removed for good. Every row has an id column and the deleted_at and deleted_by audit columns (see audit.go): NULL while the row is live, set while it is in the trash. Queries for live rows add "deleted_at IS NULL" themselves.

SoftTable writes the statements that every such table needs, so a store keeps only its own rules:

notes := db.SoftTable{DB: database, Table: "wt_note", Scope: "instance=?", Args: []any{key}}

err := notes.Delete(ctx, id)
err = notes.Undelete(ctx, id, func(ctx context.Context) error {
	// Refuse a note whose title another note took meanwhile.
	return checkTitle(ctx, id)
})
err = notes.Purge(ctx, id)

Every method returns ErrNotFound for an ID that is not where it should be: not live for Delete, not in the trash for the others. The methods join a transaction in ctx (see DB.Tx).

func (SoftTable) Delete

func (t SoftTable) Delete(ctx context.Context, id string) error

Delete moves the live row with id to the trash, recording when and by whom in deleted_at and deleted_by (see Stamp).

func (SoftTable) DeleteWith

func (t SoftTable) DeleteWith(ctx context.Context, id, set string, args ...any) error

DeleteWith is Delete with more assignments in the same statement, such as "publish_at=NULL" to cancel what was planned for the row. The ? placeholders of set take args.

func (SoftTable) GetTrashed

func (t SoftTable) GetTrashed(ctx context.Context, dest any, columns, id string) error

GetTrashed reads the row with id from the trash into dest, a pointer to a struct, with the given columns, such as "id, name, deleted_at".

func (SoftTable) Purge

func (t SoftTable) Purge(ctx context.Context, id string, steps ...func(ctx context.Context) error) error

Purge removes the row with id from the trash for good. Steps run first, in one transaction with the delete and only for a row in the trash, for what goes with the row, such as releasing the media it uses or removing rows that refer to it; the first error of a step is returned as it is.

func (SoftTable) Trashed

func (t SoftTable) Trashed(ctx context.Context, dest any, columns string, limit, offset int) (int, error)

Trashed reads rows in the trash into dest, a pointer to a slice, with the given columns: the most recently deleted first, at most limit of them from offset. A limit of 0 or less reads them all. It returns how many rows the trash holds in all, for paging.

func (SoftTable) Undelete

func (t SoftTable) Undelete(ctx context.Context, id string, checks ...func(ctx context.Context) error) error

Undelete brings the row with id back from the trash, recording when and by whom in updated_at and updated_by. Checks run first, in one transaction with the change, so a store can keep a row in the trash that may not come back, such as a redirect whose path another redirect uses now; the first error of a check is returned as it is.

A unique index may still refuse the row; db.IsUnique reports that error.

Directories

Path Synopsis
Package dbtest opens a fresh database per test for every configured dialect.
Package dbtest opens a fresh database per test for every configured dialect.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL