db

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: Apache-2.0 Imports: 37 Imported by: 0

Documentation

Overview

Package db is Anetos's data layer: connections, a typed query builder, model CRUD with timestamps, soft deletes and hooks, transactions, raw SQL scanned into structs, and pagination. It is built on database/sql and works with PostgreSQL, MySQL/MariaDB and SQLite through the driver modules (anetos.dev/anetos/drivers/postgres, …/mysql, …/sqlite).

Connect once at startup; the connection is then available in every context the app creates (handlers, jobs, app.Go tasks):

database, err := db.Connect(ctx, app, sqlite.Driver(), postgres.Driver())

Models are plain structs:

type Post struct {
	db.Model        // id, created_at, updated_at
	db.SoftDeletes  // deleted_at
	Title    string `db:"title" json:"title"`
	AuthorID int64  `db:"author_id" json:"author_id"`
}

err := db.Create(ctx, &post)
post, err := db.Find[Post](ctx, id)          // ErrNotFound → 404
posts, err := db.Query[Post](ctx).
	Where(db.C("author_id").Eq(id)).
	Latest().
	Paginate(page, 20)

Columns are the db tags, or the snake_case field names; the table is the snake_case plural of the type name unless a TableName method says otherwise. Struct, pointer-to-struct and slice-of-struct fields without a db tag are ignored (they are reserved for relations).

Transactions

Tx runs a function in a transaction carried by its context, so everything called with that context joins it; nested calls use savepoints.

Raw SQL

Raw scans any query into structs or values, and Exec runs statements. Placeholders are written ? on every database, or :name with Named.

Validation

Importing db registers the unique and exists validation rules, which query the database in the request's context.

Index

Constants

View Source
const (
	TLSVerify     = "verify"      // encrypt, and check the server's certificate and name
	TLSSkipVerify = "skip-verify" // encrypt only: open to a man in the middle
	TLSNone       = "none"        // plain text
)

The values of Config.TLS.

View Source
const DefaultPerPage = 15

DefaultPerPage is used when Paginate or CursorPaginate get perPage < 1.

View Source
const SQLiteCosineFunction = "anetos_vec_distance_cosine"

SQLiteCosineFunction is the SQL function SQLite queries use for CosineDistanceBlobs; the SQLite driver registers it.

View Source
const SQLiteTimeFormat = "2006-01-02 15:04:05.999999999"

SQLiteTimeFormat is how times are stored in SQLite: UTC text in the format of SQLite's own CURRENT_TIMESTAMP and datetime(), with fractional seconds when present. Using one format makes text comparison and sorting match time order, also against values written by SQL defaults.

View Source
const SearchIndexesTable = "search_indexes"

SearchIndexesTable is the table where migrations record the search indexes they make.

View Source
const SimilarCandidates = 200

SimilarCandidates is how many of the nearest chunks Q.Similar and Q.Hybrid consider (and how many keyword matches Hybrid ranks): conditions of the query (Where) apply to the records they come from, so a query whose conditions exclude most records finds fewer.

Variables

View Source
var ErrInvalidCursor error = invalidCursor{}

ErrInvalidCursor is returned by CursorPaginate for a cursor it didn't produce. It reports status 400 to the web package.

View Source
var ErrNoDB = errors.New("db: no database in context (use db.Connect, or db.WithDB in tests and scripts)")

ErrNoDB is returned when a query's context has no database. Use Connect (which adds the database to every context the app creates) or WithDB.

View Source
var ErrNotFound error = notFound{}

ErrNotFound is returned by First, Find and similar methods when no row matches. It reports status 404 to the web package, so a handler can return it directly:

post, err := db.Find[Post](c, in.ID)
if err != nil {
	return nil, err // 404 if the post doesn't exist
}

Functions

func AfterCommit

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

AfterCommit runs fn once the transaction in ctx commits, or right away if ctx has no transaction (or only a test's, from WithTestTx). Use it for work that must only happen if the data was saved, such as sending an email or dispatching a job. If the transaction (or the nested transaction fn was registered in) rolls back, fn never runs. fn gets a context outside the transaction (in a test's transaction from WithTestTx, the test's context).

func Attach added in v0.1.1

func Attach[T, R any](ctx context.Context, row *T, rel Rel[T, R], ids ...any) error

Attach links row to the related rows with the given primary keys through the pivot table of a many_to_many relation. Keys already linked are skipped, also when another transaction links them at the same time, so Attach can be repeated; this needs a primary key or unique index on the pivot's two columns.

err := db.Attach(ctx, &post, PostRels.Tags, goTag.ID, webTag.ID)

func Avg

func Avg[V, T any](q *Q[T], col Column[V]) (float64, error)

Avg returns the average of col over the matching rows (zero if none).

func Columns

func Columns[T any]() ([]string, error)

Columns returns the column names of model T in field order, with the columns of embedded structs where they are embedded. It is the list `anetos gen` writes typed columns for, and handy for raw SQL:

cols, err := db.Columns[Post]()
sql := "SELECT " + strings.Join(cols, ", ") + " FROM posts WHERE …"

func CosineDistance added in v0.3.0

func CosineDistance(a, b Vector) (float64, error)

CosineDistance returns 1 minus the cosine of the angle between a and b: 0 for the same direction, 1 for unrelated, 2 for opposite. It is the distance Q.Similar orders by; SQLite's driver computes it with this function. Vectors of different lengths are an error; a zero vector is at distance 1 from every other.

func CosineDistanceBlobs added in v0.3.0

func CosineDistanceBlobs(a, b []byte) (float64, error)

CosineDistanceBlobs is CosineDistance of two vectors stored as little-endian float32s, as SQLite stores them: the SQL function anetos_vec_distance_cosine, which the SQLite driver registers.

func Create

func Create[T any](ctx context.Context, row *T) error

Create inserts row. A zero integer primary key is generated by the database and set on row. Models with Timestamps get created_at and updated_at set (unless already set).

func CreateMany

func CreateMany[T any](ctx context.Context, rows []T) error

CreateMany inserts rows in as few statements as possible, running the create hooks for each row. Generated primary keys are set on the rows on PostgreSQL and SQLite, but not on MySQL, which can't report them for a multi-row insert.

func Delete

func Delete[T any](ctx context.Context, row *T) error

Delete deletes row, found by primary key. Models with SoftDeletes are soft-deleted: deleted_at is set in the database and on row. It returns ErrNotFound if no (non-deleted) row matched.

func Detach added in v0.1.1

func Detach[T, R any](ctx context.Context, row *T, rel Rel[T, R], ids ...any) error

Detach unlinks row from the related rows with the given primary keys (none: nothing to do). The related rows stay.

func DetachAll added in v0.1.1

func DetachAll[T, R any](ctx context.Context, row *T, rel Rel[T, R]) error

DetachAll unlinks row from all its related rows.

func EmbeddingsTable added in v0.3.0

func EmbeddingsTable(table string) string

EmbeddingsTable returns the table holding table's embeddings: "<table>_embeddings" (migrate.Schema.CreateEmbeddings makes it).

func EscapeLike added in v0.3.0

func EscapeLike(s string) string

EscapeLike escapes LIKE's wildcards (% and _) and its escape character (!) in s, for a pattern used with ESCAPE '!':

db.SQL("title LIKE ? ESCAPE '!'", "%"+db.EscapeLike(q)+"%")

func Exec

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

Exec runs a SQL statement that returns no rows, with the same parameter forms as Raw.

func Find

func Find[T any](ctx context.Context, id any) (T, error)

Find returns the row of model T whose primary key is id, or ErrNotFound.

func ForceDelete

func ForceDelete[T any](ctx context.Context, row *T) error

ForceDelete deletes row even if the model uses SoftDeletes.

func InTx

func InTx(ctx context.Context) bool

InTx reports whether ctx has a transaction on its database.

func KeyOf added in v0.3.0

func KeyOf(row any) (table string, key any, err error)

KeyOf returns the table and primary key of row, a pointer to a model (or a model), as the db package writes them; for audit entries and links to a row.

func Load added in v0.1.1

func Load[T any](ctx context.Context, row *T, rels ...Relation[T]) error

Load loads relations of one row:

err := db.Load(ctx, &post, PostRels.Comments)

func LoadMany added in v0.1.1

func LoadMany[T any](ctx context.Context, rows []T, rels ...Relation[T]) error

LoadMany loads relations of rows, in place, with one query per relation:

err := db.LoadMany(ctx, posts, PostRels.Author)

func LocalHost added in v0.3.0

func LocalHost(host string) bool

LocalHost reports whether host is this machine: localhost, a loopback address, or a Unix socket path.

func Max

func Max[V, T any](q *Q[T], col Column[V]) (V, error)

Max returns the largest value of col (the zero value if no rows match).

func Min

func Min[V, T any](q *Q[T], col Column[V]) (V, error)

Min returns the smallest value of col (the zero value if no rows match).

func Pluck

func Pluck[V, T any](q *Q[T], col Column[V]) ([]V, error)

Pluck returns the values of one column of the matching rows. Columns made with JSONCol are decoded from JSON.

emails, err := db.Pluck(db.Query[User](ctx).Where(active), db.Col[string]("email"))

func Plural

func Plural(name string) string

Plural returns the plural the db package uses for table names: it pluralizes the last word of a snake_case name (blog_post → blog_posts, category → categories, person → people). The migrate package uses it to guess referenced tables.

func PruneChunks added in v0.3.0

func PruneChunks[T any](ctx context.Context) (int64, error)

PruneChunks deletes the chunks of T's records that no longer exist, and returns how many: those a cascading delete left on MariaDB, where the embeddings table has no foreign key (see migrate.Schema.CreateEmbeddings).

func PruneTrashed added in v0.3.0

func PruneTrashed[T any](app *anetos.App, after time.Duration) error

PruneTrashed registers model T, which must embed SoftDeletes: its rows soft-deleted more than after ago are deleted for good by the db:prune-trashed command, or by PruneAllTrashed (in a scheduled task, say). The first call adds the command to the app.

err := db.PruneTrashed[models.Post](app, 90*24*time.Hour)

Rows are deleted in batches of 1,000, each in its own transaction, as Q.ForceDelete does: a table watched by the audit log gets a bulk entry per batch. Related rows go only where foreign keys cascade.

func Raw

func Raw[T any](ctx context.Context, query string, args ...any) ([]T, error)

Raw runs a SQL query and scans every row into T: a struct (columns match db tags or snake_case field names; extra columns are ignored) or a single value such as int64 or string for one-column results.

type row struct {
	Name  string `db:"name"`
	Total int64  `db:"total"`
}
rows, err := db.Raw[row](ctx, `
	SELECT u.name, COUNT(p.id) AS total
	FROM users u JOIN posts p ON p.author_id = u.id
	WHERE p.created_at > ?
	GROUP BY u.name`, since)

Write parameters as ? on every database (?? for a literal ?), or as :name with a single Named argument. Native placeholders ($1) also work when the query has no ?.

func RawFirst

func RawFirst[T any](ctx context.Context, query string, args ...any) (T, error)

RawFirst is like Raw but returns only the first row, or ErrNotFound.

func RecordID added in v0.3.0

func RecordID[T any](row T) (int64, error)

RecordID returns row's primary key as an int64, the key of its embeddings: an integer primary key that isn't zero.

func ReplaceChunks added in v0.3.0

func ReplaceChunks[T any](ctx context.Context, recordID int64, chunks []Chunk) error

ReplaceChunks makes chunks the record's chunks, in one transaction: its other chunks are deleted. The chunks are stored for recordID, at their positions in the slice (their RecordID and Position are ignored).

func Restore

func Restore[T any](ctx context.Context, row *T) error

Restore undeletes a soft-deleted row.

func Save

func Save[T any](ctx context.Context, row *T) error

Save creates row if its generated primary key is zero, and updates it otherwise. It is for models with an integer primary key; use Create and Update for others.

func Select

func Select[R, T any](q *Q[T], terms ...string) ([]R, error)

Select runs the query with the given SELECT terms (written in SQL) and scans the rows into R, matching columns to R's db tags (or snake_case field names). Use it for aggregates per group:

type authorStats struct {
	AuthorID int64 `db:"author_id"`
	Posts    int64 `db:"posts"`
}
stats, err := db.Select[authorStats](
	db.Query[Post](ctx).GroupBy("author_id"),
	"author_id", "COUNT(*) AS posts")

func SoftDeleting added in v0.3.0

func SoftDeleting[T any]() (bool, error)

SoftDeleting reports whether model T embeds SoftDeletes, so that Delete sets deleted_at and queries skip deleted rows.

func Sum

func Sum[V, T any](q *Q[T], col Column[V]) (V, error)

Sum returns the sum of col over the matching rows (zero if none), respecting Limit and Offset. Distinct is ignored (use Select with "SUM(DISTINCT col)"), and GroupBy is an error (use Select for per-group totals). The same holds for Avg, Min and Max.

total, err := db.Sum(db.Query[Order](ctx).Where(paid), db.Col[int64]("amount"))

func Sync added in v0.1.1

func Sync[T, R any](ctx context.Context, row *T, rel Rel[T, R], ids ...any) error

Sync makes the given primary keys row's exact set of related rows: missing links are added and the others removed, in one transaction.

func TableOf added in v0.3.0

func TableOf[T any]() (string, error)

TableOf returns the table of model T (its Tabler name, or the type's name in snake case, plural).

func Tx

func Tx(ctx context.Context, fn func(ctx context.Context) error) error

Tx runs fn in a transaction on the database in ctx. Queries made with the ctx passed to fn run in the transaction. If fn returns an error or panics, the transaction is rolled back; otherwise it is committed.

err := db.Tx(ctx, func(ctx context.Context) error {
	if err := db.Create(ctx, &order); err != nil {
		return err
	}
	return db.Create(ctx, &payment)
})

Calling Tx inside a transaction starts a nested one using a savepoint: an error rolls back only the nested part.

Don't run queries of one transaction from several goroutines at once.

func TxWith

func TxWith(ctx context.Context, opts *sql.TxOptions, fn func(ctx context.Context) error) (err error)

TxWith is like Tx with transaction options (isolation level, read-only). Options are ignored for nested transactions, which inherit the outer one.

func Untracked added in v0.2.0

func Untracked(ctx context.Context) context.Context

Untracked returns ctx with its queries left out of repeated-query detection (DB.Track), for code whose repeated queries are by design: the framework's database stores use it.

func Update

func Update[T any](ctx context.Context, row *T) error

Update writes every column of row (except the primary key, created_at and deleted_at) to its database row, found by primary key, and returns ErrNotFound if there is no such row. Models with Timestamps get updated_at set. Soft-deleted rows can be updated; use Restore to undelete one.

func Upsert

func Upsert[T any](ctx context.Context, rows []T, conflict []string, update ...string) error

Upsert inserts rows, or updates the columns in update of rows that conflict with an existing one on the conflict columns (which need a unique index). With no update columns, conflicting rows are left as they are. Models with Timestamps also update updated_at. Hooks don't run, and generated keys aren't set on rows.

err := db.Upsert(ctx, prices, []string{"sku"}, "price")

func WithDB

func WithDB(ctx context.Context, d *DB) context.Context

WithDB returns ctx with d as the database for queries made with it. Use it in tests and for additional connections:

ctx = db.WithDB(ctx, analytics)
rows, err := db.Query[Event](ctx).Get()

A transaction started on another DB in ctx is not used for d.

func WithTestTx added in v0.2.0

func WithTestTx(ctx context.Context, tx *sql.Tx) (context.Context, error)

WithTestTx is WithTx for test helpers, whose transaction is rolled back at the end of the test instead of committed: work done at its level counts as committed. AfterCommit callbacks registered directly in it run at once, and those of a Tx directly inside it run when that Tx commits (as they would in the app, without the test's transaction). anetostest uses it.

func WithTx

func WithTx(ctx context.Context, tx *sql.Tx) (context.Context, error)

WithTx returns ctx with tx, a transaction on the database in ctx that the caller began and will commit or roll back itself. Queries made with the returned context run in tx, and Tx on it uses savepoints. Use it to share a transaction with code that works with *sql.Tx directly. AfterCommit callbacks registered on the returned context never run, since the db package doesn't see the commit: neither do what relies on them (queue.AfterCommit dispatches, async event listeners, and the recording of jobs the database queue driver writes in tx, which anetostest and queue.Queue.Observe see).

func WithoutTx added in v0.2.0

func WithoutTx(ctx context.Context) context.Context

WithoutTx returns ctx without its transaction on the database in ctx: queries made with it use their own connections, and their writes stay if the transaction rolls back. Use it for records that must outlive a failed transaction, such as an audit log of the attempt. It needs a second connection while the transaction holds one. Don't use it with SQLite, whose transactions hold the write lock from the start: a write made this way waits for the transaction and fails after the busy timeout.

Types

type AfterCreateHook

type AfterCreateHook interface {
	// AfterCreate runs after the insert; an error is returned by Create.
	AfterCreate(ctx context.Context) error
}

AfterCreateHook runs after a row is inserted.

type AfterDeleteHook

type AfterDeleteHook interface {
	// AfterDelete runs after the delete; an error is returned by Delete.
	AfterDelete(ctx context.Context) error
}

AfterDeleteHook runs after a row is deleted or soft-deleted.

type AfterSaveHook

type AfterSaveHook interface {
	// AfterSave runs after an insert or update; an error is returned.
	AfterSave(ctx context.Context) error
}

AfterSaveHook runs after a row is inserted or updated.

type AfterUpdateHook

type AfterUpdateHook interface {
	// AfterUpdate runs after the update; an error is returned by Update.
	AfterUpdate(ctx context.Context) error
}

AfterUpdateHook runs after a row is updated.

type Assignment

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

Assignment sets a column in Q.Update, from Column.Set or Column.SetRaw.

type BeforeCreateHook

type BeforeCreateHook interface {
	// BeforeCreate runs in the insert's context; an error stops the insert.
	BeforeCreate(ctx context.Context) error
}

BeforeCreateHook runs before a row is inserted.

type BeforeDeleteHook

type BeforeDeleteHook interface {
	// BeforeDelete runs in the delete's context; an error stops the delete.
	BeforeDelete(ctx context.Context) error
}

BeforeDeleteHook runs before a row is deleted or soft-deleted.

type BeforeSaveHook

type BeforeSaveHook interface {
	// BeforeSave runs before an insert or update; an error stops it.
	BeforeSave(ctx context.Context) error
}

BeforeSaveHook runs before a row is inserted or updated.

type BeforeUpdateHook

type BeforeUpdateHook interface {
	// BeforeUpdate runs in the update's context; an error stops the update.
	BeforeUpdate(ctx context.Context) error
}

BeforeUpdateHook runs before a row is updated.

type Bulk added in v0.3.0

type Bulk struct {
	// Where is the condition the rows were chosen by, in the database's
	// SQL ("" for CreateMany and Upsert), and Args its arguments.
	Where string
	// Args are the arguments of Where.
	Args []any
	// Set are a bulk update's assignments by column: values, or SetExpr
	// for expressions. updated_at is left out.
	Set map[string]any
	// Keys are the primary keys of every row written.
	Keys []any
	// Before are rows' values before the write, for as many rows as the
	// watchers asked for ([DB.Watch]): the assigned columns of an update,
	// every column of a force delete, the updated columns of rows an
	// upsert updated. Empty for creates, deletes and restores.
	Before []RowValues
	// After are rows' values after an upsert or a create, within the same
	// limit.
	After []RowValues
	// Complete reports whether Before and After hold every row's values.
	Complete bool
}

Bulk describes a write to many rows at once: a query's Update, Delete, ForceDelete or Restore, CreateMany or Upsert.

type Capability added in v0.3.0

type Capability string

Capability is something a database server may or may not be able to do, depending on the database, its version and its extensions.

const (
	// FullText is full-text search: [Q.Search] and search indexes. Every
	// supported database has it.
	FullText Capability = "full-text search"
	// BM25 is BM25 ranking of search results: SQLite's FTS5 has it, and
	// PostgreSQL 17+ with the pg_textsearch extension.
	BM25 Capability = "BM25 ranking"
)

The capabilities features can require (DB.Require).

const VectorSearch Capability = "vector search"

VectorSearch is the vector search capability: Q.Similar, Q.Hybrid and embeddings tables. PostgreSQL has it with the pgvector extension, MariaDB from 11.7, and SQLite by comparing every vector (fine for tens of thousands of chunks); MySQL Community doesn't.

type Chunk added in v0.3.0

type Chunk struct {
	// RecordID is the record's primary key.
	RecordID int64 `db:"record_id"`
	// Position is the passage's position in the record, from 0.
	Position int `db:"chunk"`
	// Content is the passage's text.
	Content string `db:"content"`
	// ContentHash identifies the text, so unchanged text isn't embedded
	// again.
	ContentHash string `db:"content_hash"`
	// Model is the embedding model's name.
	Model string `db:"model"`
	// Embedding is the passage's vector.
	Embedding Vector `db:"embedding"`
}

Chunk is a row of an embeddings table (EmbeddingsTable): one passage of a record, with its embedding. Package ai reads and writes them.

func Chunks added in v0.3.0

func Chunks[T any](ctx context.Context, recordIDs ...int64) ([]Chunk, error)

Chunks returns the chunks of T's records with the IDs (all records' when there are none), by record and position.

type ChunkMatch added in v0.3.0

type ChunkMatch struct {
	Chunk
	// Distance is the cosine distance to the vector: 0 is the same
	// direction.
	Distance float64 `db:"distance"`
}

ChunkMatch is a chunk and its distance to a vector (NearestChunks).

func NearestChunks added in v0.3.0

func NearestChunks[T any](ctx context.Context, model string, v Vector, recordIDs ...int64) ([]ChunkMatch, error)

NearestChunks returns the chunks of the records with the IDs (made by the embedding model named model; "" for any) by their distance to v, nearest first: the passages to show for the records Q.Similar or Q.Hybrid found.

type Column

type Column[T any] struct {
	// contains filtered or unexported fields
}

Column is a typed reference to a column, used to build conditions, orderings and assignments that the compiler checks:

posts, err := db.Query[Post](ctx).
	Where(models.PostCols.Title.Like("%go%")).
	OrderBy(models.PostCols.Title.Asc()).
	Get()

`anetos gen` declares the columns of every model (see the model code generation guide). Declare others with Col or JSONCol, or use C for an untyped column. T is the type of the model field, so a nullable column is a Column[*time.Time] and compares with new(t).

func C

func C(name string) Column[any]

C returns an untyped column reference, for quick queries:

db.Query[Post](ctx).Where(db.C("author_id").Eq(id))

func Col

func Col[T any](name string) Column[T]

Col returns a column reference. name may be qualified: "posts.title".

func JSONCol

func JSONCol[T any](name string) Column[T]

JSONCol returns a reference to a column stored as JSON (a `db:",json"` field): values given to its methods are encoded as JSON, and Pluck decodes them.

func (Column[T]) Asc

func (c Column[T]) Asc() Order

Asc orders by the column, ascending.

func (Column[T]) Between

func (c Column[T]) Between(lo, hi T) Expr

Between is column BETWEEN lo AND hi (inclusive).

func (Column[T]) Contains added in v0.3.0

func (c Column[T]) Contains(s string) Expr

Contains is column LIKE '%s%' with s taken literally: its % and _ match themselves, so text from a search box can't widen the match. Case sensitivity follows the database, as for Column.Like.

func (Column[T]) Desc

func (c Column[T]) Desc() Order

Desc orders by the column, descending.

func (Column[T]) Eq

func (c Column[T]) Eq(v T) Expr

Eq is column = v.

func (Column[T]) Gt

func (c Column[T]) Gt(v T) Expr

Gt is column > v.

func (Column[T]) Gte

func (c Column[T]) Gte(v T) Expr

Gte is column >= v.

func (Column[T]) In

func (c Column[T]) In(vs ...T) Expr

In is column IN (vs…). An empty list matches nothing.

func (Column[T]) IsNull

func (c Column[T]) IsNull() Expr

IsNull is column IS NULL.

func (Column[T]) Like

func (c Column[T]) Like(pattern string) Expr

Like is column LIKE pattern. Case sensitivity follows the database (PostgreSQL is case-sensitive; MySQL and SQLite usually aren't for ASCII).

func (Column[T]) Lt

func (c Column[T]) Lt(v T) Expr

Lt is column < v.

func (Column[T]) Lte

func (c Column[T]) Lte(v T) Expr

Lte is column <= v.

func (Column[T]) Name

func (c Column[T]) Name() string

Name returns the column name.

func (Column[T]) Ne

func (c Column[T]) Ne(v T) Expr

Ne is column <> v.

func (Column[T]) NotIn

func (c Column[T]) NotIn(vs ...T) Expr

NotIn is column NOT IN (vs…). An empty list matches everything.

func (Column[T]) NotLike

func (c Column[T]) NotLike(pattern string) Expr

NotLike is column NOT LIKE pattern.

func (Column[T]) NotNull

func (c Column[T]) NotNull() Expr

NotNull is column IS NOT NULL.

func (Column[T]) Of

func (c Column[T]) Of(table string) Column[T]

Of returns the column qualified with table (replacing any qualifier; "" removes it), for queries with joins:

db.Query[Post](ctx).Join("JOIN authors ON authors.id = posts.author_id").
	Where(models.PostCols.ID.Of("posts").Gt(100))

func (Column[T]) Set

func (c Column[T]) Set(v T) Assignment

Set assigns v to the column in Q.Update. A nil pointer, map or slice sets NULL.

func (Column[T]) SetRaw

func (c Column[T]) SetRaw(sql string, args ...any) Assignment

SetRaw assigns a SQL expression (with ? placeholders) to the column:

q.Update(db.C("views").SetRaw("views + ?", 1))

func (Column[T]) StartsWith added in v0.3.0

func (c Column[T]) StartsWith(s string) Expr

StartsWith is column LIKE 's%' with s taken literally (see Column.Contains).

type Config

type Config struct {
	// Connection selects the driver: sqlite, postgres or mysql.
	Connection string `env:"DB_CONNECTION" default:"sqlite"`
	// URL is a complete connection string in the driver's format. When set,
	// Host, Port, Database, Username and Password are ignored.
	URL      string `env:"DB_URL"`
	Host     string `env:"DB_HOST" default:"127.0.0.1"` // server host (PostgreSQL, MySQL)
	Port     int    `env:"DB_PORT"`                     // 0: the driver's default port
	Database string `env:"DB_DATABASE"`                 // database name; for SQLite, the file (default database/app.db)
	Username string `env:"DB_USERNAME"`                 // user to connect as
	Password string `env:"DB_PASSWORD"`                 // that user's password
	// TLS secures connections built from DB_HOST (PostgreSQL, MySQL):
	// "verify" (encrypt, and check the server's certificate and name),
	// "skip-verify" (encrypt only), or "none". Empty: "none" for a local
	// host (localhost, a loopback address, a Unix socket), "verify" for
	// any other, so a remote database is never reached in plain text by
	// default. With DB_URL, put the driver's own options in the URL.
	// DB_TLS (v0.3).
	TLS string `env:"DB_TLS"`
	// TLSCA is a PEM file of the certificate authorities that sign the
	// server's certificate, for "verify" when they aren't the system's
	// (a managed database's own CA). Setting it means "verify" unless
	// DB_TLS says otherwise, also for a local host (a tunnel); it can't
	// be combined with "skip-verify" or "none". DB_TLS_CA.
	TLSCA string `env:"DB_TLS_CA"`

	// Pool settings, as for database/sql's DB.SetMaxOpenConns and friends.
	MaxOpenConns    int           `env:"DB_MAX_OPEN_CONNS" default:"25"`     // connections open at most
	MaxIdleConns    int           `env:"DB_MAX_IDLE_CONNS" default:"25"`     // idle connections kept
	ConnMaxLifetime time.Duration `env:"DB_CONN_MAX_LIFETIME" default:"30m"` // a connection is closed after this long
	ConnMaxIdleTime time.Duration `env:"DB_CONN_MAX_IDLE_TIME" default:"5m"` // an idle connection is closed after this long

	// LogQueries logs every query with its duration at debug level. Unset
	// means on in development and off elsewhere.
	LogQueries *bool `env:"DB_LOG_QUERIES"`
	// SlowQuery logs queries that take at least this long as warnings.
	// Zero disables it.
	SlowQuery time.Duration `env:"DB_SLOW_QUERY" default:"500ms"`
	// RepeatedQueries logs a warning when a unit of work (a request, a
	// job…) runs the same query this many times or more: an N+1. Unset
	// means 5 in development and testing, off elsewhere; 0 disables it.
	RepeatedQueries *int `env:"DB_REPEATED_QUERIES"`

	// AllowLocalTimeZone lets the database session run in a time zone
	// other than UTC, which [DB.Check] otherwise refuses: for a legacy
	// database whose times are local. The app still writes UTC.
	// DB_ALLOW_LOCAL_TIMEZONE, default false.
	AllowLocalTimeZone bool `env:"DB_ALLOW_LOCAL_TIMEZONE"`
}

Config is a database connection's configuration. Connect reads it from the DB_* keys; LoadConfig reads it with another prefix for extra connections.

func LoadConfig

func LoadConfig(src config.Source, prefix string) (Config, error)

LoadConfig reads a Config from src with every key prefixed by prefix, so LoadConfig(src, "ANALYTICS_") reads ANALYTICS_DB_CONNECTION, ANALYTICS_DB_HOST and so on. An empty prefix reads the DB_* keys.

func (Config) GoString

func (c Config) GoString() string

GoString masks secrets for %#v too.

func (Config) LogValue

func (c Config) LogValue() slog.Value

LogValue logs the Config as Config.String does, for every slog handler (JSON included).

func (Config) String

func (c Config) String() string

String describes the connection without secrets: the password, and passwords inside URL, are masked, so a printed or logged Config leaks nothing (fmt's %v, %+v and %#v, and slog through Config.LogValue).

func (Config) TLSMode added in v0.3.0

func (c Config) TLSMode() string

TLSMode returns the TLS the connection built from Host uses: TLS if set, else TLSVerify with TLSCA set or a host other than this machine, and TLSNone for a local host (localhost, a loopback address, a Unix socket path). Drivers use it; with URL set, the URL decides and it returns "".

func (Config) Validate

func (c Config) Validate() error

Validate implements config.Validator.

type CursorPage

type CursorPage[T any] struct {
	Data       []T    `json:"data"`        // the page's rows
	PerPage    int    `json:"per_page"`    // rows per page
	NextCursor string `json:"next_cursor"` // "" on the last page
	PrevCursor string `json:"prev_cursor"` // "" on the first page
}

CursorPage is one page of results from Q.CursorPaginate.

type DB

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

DB is a database connection pool with its dialect. It is safe for concurrent use; create one per database and share it.

Queries find their DB in the context: Connect adds it to every context the app creates, and WithDB adds it to others.

func Connect

func Connect(ctx context.Context, app *anetos.App, drivers ...Driver) (*DB, error)

Connect opens the app's default database from the DB_* configuration and makes it available to the app:

database, err := db.Connect(ctx, app, sqlite.Driver(), postgres.Driver())

DB_CONNECTION picks one of the given drivers, so an app can use SQLite in development and PostgreSQL in production with both compiled in. Connect opens the connection pool, adds the DB to every context the app creates (see anetos.App.AddContextValue), provides it as a *db.DB service, and closes it in a shutdown hook. It pings the database when the app boots (right away if it already has), so the app and every command that boots it fail fast when the database is unreachable, while `help` doesn't need one; then it runs DB.Check, so the app doesn't start with settings or features the database can't serve (SEARCH_*, DB.Require).

Queries are logged at debug level in development unless DB_LOG_QUERIES says otherwise.

func From

func From(ctx context.Context) (*DB, error)

From returns the database in ctx, or ErrNoDB.

func New

func New(sqlDB *sql.DB, d Dialect, opts ...Option) *DB

New wraps an existing *sql.DB, for example one opened by other code or a test helper.

func Open

func Open(drv Driver, cfg Config, opts ...Option) (*DB, error)

Open opens a connection pool with drv, configured by cfg. Zero pool settings keep database/sql's defaults. It doesn't connect; call DB.Ping to check the connection.

func (*DB) Check added in v0.3.0

func (d *DB) Check(ctx context.Context) error

Check runs the checks Connect runs when the app boots: the session's time zone (DB.CheckTimeZone), the search settings (DB.CheckSearch), the features' requirements (DB.Require), and that the search indexes were built for the current settings (unless the command being run changes the schema, such as migrate and search:reindex; see cmd.Command.ChangesSchema). Call it after Open to check a DB the same way.

func (*DB) CheckCapabilities added in v0.3.0

func (d *DB) CheckCapabilities(ctx context.Context, feature string, caps ...Capability) error

CheckCapabilities returns nil if the database provides caps, else the error DB.Require would stop the app with, naming feature: for code that checks before it does something, such as a migration.

func (*DB) CheckSearch added in v0.3.0

func (d *DB) CheckSearch(ctx context.Context) error

CheckSearch checks that the database supports the search settings: SEARCH_LANGUAGE (on PostgreSQL, a text search configuration the server has) and SEARCH_RANKING. Migrations creating search indexes check it too.

func (*DB) CheckTimeZone added in v0.3.0

func (d *DB) CheckTimeZone(ctx context.Context) error

CheckTimeZone returns an error unless the database session's time zone is UTC, so times the database writes itself (CURRENT_TIMESTAMP, NOW(), MySQL's TIMESTAMP columns) are UTC like the ones the app writes. The drivers open sessions in UTC, so a zone comes from the connection string (timezone= or time_zone= in DB_URL); DB.Check runs it at boot unless DB_ALLOW_LOCAL_TIMEZONE is set. SQLite has no session zone.

func (*DB) Close

func (d *DB) Close() error

Close closes the connection pool.

func (*DB) Dialect

func (d *DB) Dialect() Dialect

Dialect returns the database's dialect.

func (*DB) OnRepeatedQuery added in v0.2.0

func (d *DB) OnRepeatedQuery(fn func(ctx context.Context, r RepeatedQuery))

OnRepeatedQuery calls fn, besides logging a warning, for each repeated query DB.Track finds from now on: anetostest records them. fn must be safe for concurrent use.

func (*DB) Ping

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

Ping checks that the database is reachable.

func (*DB) Require added in v0.3.0

func (d *DB) Require(feature string, caps ...Capability) error

Require records that feature needs the database to provide caps. The app checks every requirement when it boots (Connect, or DB.Check), and refuses to start if one isn't met, naming the feature, the database and how to get the capability. Once the DB is checked, Require checks at once and returns the error.

err := database.Require("relevance ranking", db.BM25)

func (*DB) SQL

func (d *DB) SQL() *sql.DB

SQL returns the underlying *sql.DB.

func (*DB) SearchConfig added in v0.3.0

func (d *DB) SearchConfig() SearchConfig

SearchConfig returns the DB's search settings.

func (*DB) Supports added in v0.3.0

func (d *DB) Supports(ctx context.Context, c Capability) (bool, error)

Supports reports whether the database can provide c, asking the server when it depends on it (its version, its extensions).

func (*DB) Track added in v0.2.0

func (d *DB) Track(ctx context.Context, u anetos.Unit) (context.Context, func())

Track counts the queries made on d with the returned context, until the returned function is called; then it logs a warning (and calls the DB.OnRepeatedQuery functions) for each query run at least the threshold number of times (WithRepeatedQueries). Connect has every unit of work of the app tracked this way (anetos.App.AroundUnits). Without a threshold, it returns ctx and a no-op.

func (*DB) Watch added in v0.3.0

func (d *DB) Watch(table string, w Watcher, bulkValues int) error

Watch makes w watch the writes the db package makes to table: Create, CreateMany, Upsert, Update, Save, Delete, ForceDelete, Restore, and a query's Update, Delete, ForceDelete and Restore. For a watched table, each of these runs in a transaction (a savepoint inside one already open, so model hooks run inside it too) and calls w after writing:

  • an update first reads the row's values FOR UPDATE, so the watcher gets the values before and after;
  • a force delete (or a delete without SoftDeletes) reads the values the row had;
  • a bulk write first selects the matching rows' keys (and the values of up to bulkValues rows) FOR UPDATE, then writes in chunks of 1,000 keys, with the condition and the keys, and fails if a chunk changes fewer rows than it selected, so what the watcher is told is exactly what was written;
  • CreateMany on a database that can't report a multi-row insert's keys (MySQL) inserts the rows one by one.

A watched table's model needs a primary key. Raw SQL (Exec), pivot writes (Attach, Sync…) and the database's own cascades aren't seen. Call Watch before the app writes, typically at setup.

func (*DB) Watchers added in v0.3.0

func (d *DB) Watchers(table string) []Watcher

Watchers returns the watchers of table (DB.Watch), in the order they were added.

type Dialect

type Dialect interface {
	// Name identifies the dialect: "postgres", "mysql" or "sqlite".
	Name() string
	// Placeholder returns the n-th (1-based) parameter placeholder.
	Placeholder(n int) string
	// QuoteIdent quotes one identifier (no dots).
	QuoteIdent(name string) string
	// Returning reports whether INSERT … RETURNING is supported.
	Returning() bool
	// DefaultValues is the INSERT clause for a row with no explicit columns.
	DefaultValues() string
	// Upsert returns the clause that follows INSERT … VALUES to update the
	// columns in update (or do nothing if it is empty) when a row conflicts
	// on the conflict columns. Column names are already quoted.
	Upsert(conflict, update []string) string
	// LockClause returns the row-locking suffix for SELECT … FOR UPDATE
	// (share=false) or FOR SHARE, or "" if the database has no row locks.
	LockClause(share bool) string
	// Arg converts a query argument before it is sent to the driver.
	Arg(v any) any
}

Dialect is the SQL flavor of a database: placeholders, identifier quoting and the few statements that differ between databases. The db package ships dialects for PostgreSQL, MySQL/MariaDB and SQLite; driver modules pair one with a database/sql driver in a Driver.

The interface may grow before v1.0; implement it outside this package only if you are prepared to follow those changes.

func MySQL

func MySQL() Dialect

MySQL returns the MySQL and MariaDB dialect.

func Postgres

func Postgres() Dialect

Postgres returns the PostgreSQL dialect.

func SQLite

func SQLite() Dialect

SQLite returns the SQLite dialect.

type Driver

type Driver struct {
	// Name is the value of DB_CONNECTION that selects this driver.
	Name string
	// Dialect writes the database's SQL.
	Dialect Dialect
	// Tune, if set, adjusts cfg before the pool is opened and configured,
	// for settings a database needs (an in-memory SQLite database must use
	// a single connection, for example).
	Tune func(cfg *Config)
	// Open opens a connection pool for cfg. It should not connect yet.
	Open func(cfg Config) (*sql.DB, error)
	// InspectURL, if set, reads a DB_URL in the driver's format: the
	// server's host ("" or a path for a Unix socket) and how connections
	// are secured, [TLSVerify], [TLSSkipVerify] or [TLSNone] (TLSNone
	// when they may fall back to plain text). The doctor command uses it
	// (v0.3).
	InspectURL func(url string) (host, tls string, err error)
}

Driver pairs a Dialect with a way to open connections. Driver modules provide one each: sqlite.Driver(), postgres.Driver(), mysql.Driver().

type Expr

type Expr interface {
	// contains filtered or unexported methods
}

Expr is a condition or value expression, built with Column methods, And, Or, Not and SQL.

func And

func And(conds ...Expr) Expr

And is true when every condition is. And() with no conditions is true.

func Not

func Not(cond Expr) Expr

Not negates a condition.

func Or

func Or(conds ...Expr) Expr

Or is true when any condition is. Or() with no conditions is false.

func SQL

func SQL(sql string, args ...any) Expr

SQL is a condition or value written in SQL, with ? placeholders (?? for a literal ?), or :name placeholders with a single Named argument:

q.Where(db.SQL("lower(email) = ?", email))

type Model

type Model struct {
	ID int64 `db:"id,pk" json:"id"` // primary key, set by Create when zero
	Timestamps
}

Model gives a struct an auto-increment ID and timestamps:

type Post struct {
	db.Model
	Title string `db:"title" json:"title"`
}

type Named

type Named map[string]any

Named holds named arguments for raw SQL written with :name parameters:

db.Raw[User](ctx, "SELECT * FROM users WHERE email = :email", db.Named{"email": e})

type Op added in v0.3.0

type Op string

Op is the kind of a watched write.

const (
	// OpCreate inserts rows: Create, CreateMany.
	OpCreate Op = "create"
	// OpUpdate changes rows: Update, Save, and Update on a query.
	OpUpdate Op = "update"
	// OpDelete soft-deletes rows (models with SoftDeletes): the rows stay.
	OpDelete Op = "delete"
	// OpRestore undeletes soft-deleted rows.
	OpRestore Op = "restore"
	// OpForceDelete removes rows from the database: ForceDelete, and
	// Delete on a model without SoftDeletes.
	OpForceDelete Op = "force_delete"
	// OpUpsert inserts rows or updates those that conflict (Upsert).
	OpUpsert Op = "upsert"
)

The kinds of watched writes.

type Option

type Option func(*DB)

Option configures a DB created with Open or New.

func WithLocalTimeZone added in v0.3.0

func WithLocalTimeZone(allow bool) Option

WithLocalTimeZone makes DB.Check accept, or not, a session time zone other than UTC (DB_ALLOW_LOCAL_TIMEZONE).

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the logger for query logs. The default discards them.

func WithQueryLog

func WithQueryLog(on bool) Option

WithQueryLog turns logging of every query on or off.

func WithRepeatedQueries added in v0.2.0

func WithRepeatedQueries(n int) Option

WithRepeatedQueries makes DB.Track report queries a unit of work runs n or more times; n below 2 disables it. Connect sets it from DB_REPEATED_QUERIES (with a default in development and testing), and Open from Config.RepeatedQueries when set. For a database opened with Open, have the app's units tracked with app.AroundUnits(d.Track).

func WithSearch added in v0.3.0

func WithSearch(cfg SearchConfig) Option

WithSearch sets the search settings of a DB from Open or New (Connect reads them from SEARCH_*). Default: simple, default.

func WithSlowQuery

func WithSlowQuery(threshold time.Duration) Option

WithSlowQuery sets the duration from which queries are logged as slow; zero disables it.

type Order

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

Order is an ORDER BY term, from Column.Asc, Column.Desc or OrderRaw.

func OrderRaw

func OrderRaw(sql string, args ...any) Order

OrderRaw is an ORDER BY term written in SQL, e.g. "lower(title) ASC".

type Page

type Page[T any] struct {
	Data        []T   `json:"data"`         // the page's rows; empty, not nil, past the end
	CurrentPage int   `json:"current_page"` // 1-based
	PerPage     int   `json:"per_page"`     // rows per page
	Total       int64 `json:"total"`        // rows on all pages
	LastPage    int   `json:"last_page"`    // number of the last page; 1 when there are no rows
}

Page is one page of results from Q.Paginate. Its JSON form follows Laravel's paginator: data, current_page, per_page, total, last_page.

func (Page[T]) HasMore

func (p Page[T]) HasMore() bool

HasMore reports whether there are pages after this one.

func (Page[T]) HasPrev

func (p Page[T]) HasPrev() bool

HasPrev reports whether there are pages before this one.

type Pruned added in v0.3.0

type Pruned struct {
	// Table is the model's table.
	Table string
	// Rows is how many rows were deleted (or, in a dry run, would be).
	Rows int64
}

Pruned is what PruneAllTrashed did to one table.

func PruneAllTrashed added in v0.3.0

func PruneAllTrashed(ctx context.Context) ([]Pruned, error)

PruneAllTrashed deletes for good the rows of every model registered with PruneTrashed that were soft-deleted longer ago than the model's duration, and reports how many per table. Run it from a scheduled task:

err := sched.Add(schedule.Daily(), "db:prune-trashed", func(ctx context.Context) error {
	_, err := db.PruneAllTrashed(ctx)
	return err
})

It stops at the first error, returning what it did so far.

type Q

type Q[T any] struct {
	// contains filtered or unexported fields
}

Q is a query on the table of model T. Build one with Query; each method returns a new Q, so a base query can be reused:

published := db.Query[Post](ctx).Where(db.C("published").Eq(true))
recent, err := published.Latest().Limit(10).Get()
total, err := published.Count()

Models with SoftDeletes exclude deleted rows unless Q.WithTrashed or Q.OnlyTrashed is used.

func Query

func Query[T any](ctx context.Context) *Q[T]

Query starts a query on the table of model T, using the database (and transaction) in ctx.

func (*Q[T]) All

func (q *Q[T]) All() iter.Seq2[T, error]

All streams the matching rows, for results too large to hold at once:

for post, err := range db.Query[Post](ctx).All() {
	if err != nil {
		return err
	}
	…
}

The connection stays busy until the loop ends, so don't run other queries in the same transaction inside the loop.

func (*Q[T]) Count

func (q *Q[T]) Count() (int64, error)

Count returns the number of matching rows.

func (*Q[T]) CursorPaginate

func (q *Q[T]) CursorPaginate(cursor string, perPage int) (CursorPage[T], error)

CursorPaginate returns perPage rows after (or before) cursor, which is "" for the first page or a NextCursor/PrevCursor from a previous page. Unlike Paginate it stays fast deep into large tables and doesn't skip or repeat rows when rows are added, but it can't jump to a page number.

The query's OrderBy terms must be plain columns (not OrderRaw) whose values aren't NULL; the primary key is added as a tie-breaker, and used alone when there is no OrderBy.

func (*Q[T]) Delete

func (q *Q[T]) Delete() (int64, error)

Delete deletes every matching row and returns how many. Models with SoftDeletes are soft-deleted: deleted_at is set on matching rows that aren't already deleted. Model hooks don't run for mass deletes.

func (*Q[T]) Distinct

func (q *Q[T]) Distinct() *Q[T]

Distinct selects distinct rows.

func (*Q[T]) Exists

func (q *Q[T]) Exists() (bool, error)

Exists reports whether any row matches.

func (*Q[T]) Find

func (q *Q[T]) Find(id any) (T, error)

Find returns the row whose primary key is id, or ErrNotFound.

func (*Q[T]) First

func (q *Q[T]) First() (T, error)

First returns the first matching row, or ErrNotFound. Without OrderBy, which row is first is up to the database.

func (*Q[T]) ForShare

func (q *Q[T]) ForShare() *Q[T]

ForShare locks the selected rows against changes until the transaction ends.

func (*Q[T]) ForUpdate

func (q *Q[T]) ForUpdate() *Q[T]

ForUpdate locks the selected rows until the transaction ends (SELECT … FOR UPDATE). Use it inside Tx. On SQLite it does nothing: SQLite transactions lock the whole database.

func (*Q[T]) ForceDelete

func (q *Q[T]) ForceDelete() (int64, error)

ForceDelete deletes every matching row, even for models with SoftDeletes. Soft-deleted rows only match with WithTrashed or OnlyTrashed.

func (*Q[T]) Get

func (q *Q[T]) Get() ([]T, error)

Get returns every matching row. It returns an empty (non-nil) slice if there are none.

func (*Q[T]) GroupBy

func (q *Q[T]) GroupBy(cols ...string) *Q[T]

GroupBy adds GROUP BY columns. Use it with Select to read the groups.

func (*Q[T]) Having

func (q *Q[T]) Having(conds ...Expr) *Q[T]

Having adds HAVING conditions.

func (*Q[T]) Hybrid added in v0.3.0

func (q *Q[T]) Hybrid(text, model string, v Vector) *Q[T]

Hybrid ranks the rows by both full-text search of text (Q.Search) and similarity to v (Q.Similar), merged by reciprocal rank fusion: a row's score adds 1/(60 + its rank) for each of the two lists it's in (the best SimilarCandidates of each), so rows that both find come first, without comparing their scores. It needs a search index and an embeddings table. Text without words makes it Q.Similar.

func (*Q[T]) Join

func (q *Q[T]) Join(sql string, args ...any) *Q[T]

Join adds a join clause written in SQL:

q.Join("JOIN users ON users.id = posts.author_id").Where(db.C("users.active").Eq(true))

The query still selects only T's columns; qualify column names that exist in both tables.

func (*Q[T]) Latest

func (q *Q[T]) Latest() *Q[T]

Latest orders by created_at, newest first.

func (*Q[T]) Limit

func (q *Q[T]) Limit(n int) *Q[T]

Limit returns at most n rows.

func (*Q[T]) Offset

func (q *Q[T]) Offset(n int) *Q[T]

Offset skips the first n rows.

func (*Q[T]) Oldest

func (q *Q[T]) Oldest() *Q[T]

Oldest orders by created_at, oldest first.

func (*Q[T]) OnlyTrashed

func (q *Q[T]) OnlyTrashed() *Q[T]

OnlyTrashed returns only soft-deleted rows.

func (*Q[T]) OrderBy

func (q *Q[T]) OrderBy(orders ...Order) *Q[T]

OrderBy adds ORDER BY terms.

func (*Q[T]) Paginate

func (q *Q[T]) Paginate(page, perPage int) (Page[T], error)

Paginate returns page (1-based) of the results, perPage rows each, with the total count. Pages past the end are empty. Limit and Offset set on the query are replaced.

perPage often comes from the request; bound it (for example with validate:"max:100") so a client can't ask for a million rows.

func (*Q[T]) Restore

func (q *Q[T]) Restore() (int64, error)

Restore undeletes the matching soft-deleted rows (live rows are left alone, whatever WithTrashed says).

func (*Q[T]) Scope

func (q *Q[T]) Scope(scopes ...func(*Q[T]) *Q[T]) *Q[T]

Scope applies reusable query modifiers:

func Published(q *db.Q[Post]) *db.Q[Post] { return q.Where(db.C("published").Eq(true)) }
posts, err := db.Query[Post](ctx).Scope(Published).Get()

func (*Q[T]) Search added in v0.3.0

func (q *Q[T]) Search(text string) *Q[T]

Search keeps the rows matching text and orders them by relevance, best first; OrderBy terms, before or after, break ties:

posts, err := db.Query[Post](ctx).Where(published).Search(q).Paginate(page, 20)

The model's table needs a search index (migrate.Table.SearchIndex). Every word of text must match in an indexed column: as a prefix for words of three letters or more ("generic" finds "generics"), whole for shorter ones; at most ten words count; with a SEARCH_LANGUAGE other than simple, words are also stemmed. Punctuation is ignored, and text without words leaves the query as it is. A second Search replaces the first. Databases differ in which words they index: MySQL and MariaDB skip words shorter than three letters and common English words ("the"), so there those only match the longer words they start, and are optional next to other words. Update, Delete and CursorPaginate refuse a query with Search.

func (*Q[T]) Similar added in v0.3.0

func (q *Q[T]) Similar(model string, v Vector) *Q[T]

Similar keeps the rows that have embeddings (in the table EmbeddingsTable names, made by migrate.Schema.CreateEmbeddings) and orders them by how close their nearest chunk is to v, nearest first: the cosine distance of vectors made by the embedding model named model (other models' vectors are ignored; "" compares every one). It considers the SimilarCandidates nearest chunks. Most apps call it through package ai, which embeds the query text:

posts, err := db.Query[Post](ctx).Where(published).Similar("text-embedding-3-small", queryVector).Limit(10).Get()

The database needs the VectorSearch capability. Search and Hybrid replace it; Update, Delete and CursorPaginate refuse it.

func (*Q[T]) Update

func (q *Q[T]) Update(assignments ...Assignment) (int64, error)

Update sets columns on every matching row and returns how many rows matched. Columns may be qualified with the model's own table. Models with Timestamps also get updated_at set. Model hooks don't run for mass updates.

n, err := db.Query[Post](ctx).Where(author.Eq(id)).Update(published.Set(false))

func (*Q[T]) Where

func (q *Q[T]) Where(conds ...Expr) *Q[T]

Where adds conditions, all of which must hold (AND). Build them with Column methods, And, Or, Not and SQL.

func (*Q[T]) WhereDoesntHave added in v0.1.1

func (q *Q[T]) WhereDoesntHave(rel Relation[T], conds ...Expr) *Q[T]

WhereDoesntHave keeps the rows with no related row matching conds.

func (*Q[T]) WhereHas added in v0.1.1

func (q *Q[T]) WhereHas(rel Relation[T], conds ...Expr) *Q[T]

WhereHas keeps the rows that have at least one related row matching conds (and the relation's own Where conditions):

// posts with an approved comment
db.Query[Post](ctx).WhereHas(PostRels.Comments, CommentCols.Approved.Eq(true))

It is an EXISTS subquery; conds use the related model's columns.

func (*Q[T]) WhereKeys added in v0.3.0

func (q *Q[T]) WhereKeys(ids ...any) *Q[T]

WhereKeys keeps the rows whose primary key is one of ids (none: no rows), as Q.Find does for one.

func (*Q[T]) WhereRaw

func (q *Q[T]) WhereRaw(sql string, args ...any) *Q[T]

WhereRaw adds a condition written in SQL with ? placeholders.

func (*Q[T]) With added in v0.1.1

func (q *Q[T]) With(rels ...Relation[T]) *Q[T]

With loads relations of the rows the query returns, with one query per relation (and level of nesting) whatever the number of rows, instead of one per row:

posts, err := db.Query[Post](ctx).
	With(PostRels.Author, PostRels.Comments.With(CommentRels.Author)).
	Get()

Get, First, Find, Paginate and CursorPaginate load them; All returns an error, since streaming rows one at a time can't batch their relations.

func (*Q[T]) WithContext

func (q *Q[T]) WithContext(ctx context.Context) *Q[T]

WithContext returns the query with another context, for example a base query built outside a transaction and run inside one:

err := db.Tx(ctx, func(ctx context.Context) error {
	rows, err := published.WithContext(ctx).ForUpdate().Get()
	…
})

func (*Q[T]) WithTrashed

func (q *Q[T]) WithTrashed() *Q[T]

WithTrashed includes soft-deleted rows.

type Rel added in v0.1.1

type Rel[T, R any] struct {
	// contains filtered or unexported fields
}

Rel is the relation field of model T that holds R values, as a value to pass to Q.With, Load, Q.WhereHas, Attach and friends. Its methods return a new Rel.

func RelOf added in v0.1.1

func RelOf[T, R any](field string) Rel[T, R]

RelOf returns the relation of model T declared on field, a pointer to R or a slice of R with a rel tag. `anetos gen` writes these; declared by hand, a wrong field or type is reported by Rel.Err and by the queries that use it. Nothing is checked before first use, so RelOf is safe in package-level variables.

var PostComments = db.RelOf[Post, Comment]("Comments")

func (Rel[T, R]) Err added in v0.1.1

func (r Rel[T, R]) Err() error

Err reports why the relation can't be used, as a query using it would: no such relation field, one that doesn't hold R, a bad rel tag, or key columns that don't exist. Tests can check hand-written handles with it.

func (Rel[T, R]) Name added in v0.1.1

func (r Rel[T, R]) Name() string

Name returns the relation's field name.

func (Rel[T, R]) OrderBy added in v0.1.1

func (r Rel[T, R]) OrderBy(orders ...Order) Rel[T, R]

OrderBy orders the related rows of a has_many or many_to_many relation, and decides which row a has_one relation gets when several match (the first). Without it, primary key order.

func (Rel[T, R]) Where added in v0.1.1

func (r Rel[T, R]) Where(conds ...Expr) Rel[T, R]

Where limits the related rows loaded (or required by Q.WhereHas):

PostRels.Comments.Where(CommentCols.Approved.Eq(true))

func (Rel[T, R]) With added in v0.1.1

func (r Rel[T, R]) With(nested ...Relation[R]) Rel[T, R]

With also loads relations of the related rows:

db.Query[Post](ctx).With(PostRels.Comments.With(CommentRels.Author))

func (Rel[T, R]) WithTrashed added in v0.1.1

func (r Rel[T, R]) WithTrashed() Rel[T, R]

WithTrashed includes soft-deleted related rows, which are left out by default.

type Relation added in v0.1.1

type Relation[T any] interface {
	// contains filtered or unexported methods
}

Relation is a relation of model T, as Q.With, Load, LoadMany and Q.WhereHas take it. Its only implementation is Rel; `anetos gen` writes one per relation field (PostRels.Comments).

type RepeatedQuery added in v0.2.0

type RepeatedQuery struct {
	// Unit is the unit of work: "request GET /posts", "job SendDigest".
	Unit anetos.Unit
	// SQL is the query, with placeholders: every run had this text, with
	// whatever arguments.
	SQL string
	// Count is how many times the unit ran it.
	Count int
	// Caller is where the app ran it from ("handlers/posts.go:42"): the
	// first frame outside the framework and the standard library when
	// Count reached the threshold (a plugin's code counts as the app's);
	// empty when there is none.
	Caller string
}

RepeatedQuery is a query a unit of work (a request, a job…) ran many times: the sign of an N+1, a query in a loop that one query (eager loading with With, WhereIn, a join) could replace.

func (RepeatedQuery) String added in v0.2.0

func (r RepeatedQuery) String() string

String describes r in one line.

type RowValues added in v0.3.0

type RowValues struct {
	// Key is the row's primary key.
	Key any
	// Values are the row's column values.
	Values Values
}

RowValues are the values of the row with Key.

type SearchConfig added in v0.3.0

type SearchConfig struct {
	// Language is how words are matched: "simple" (the default) matches
	// them as written, in any language; a language stems them ("generic"
	// also finds "generics" and "running" finds "run"): "english" on
	// PostgreSQL and SQLite, and on PostgreSQL any text search
	// configuration the server has (german, french, …). MySQL only has
	// "simple". Search indexes are built for it, and the app refuses to
	// start when an index was built for another one (run search:reindex).
	// SEARCH_LANGUAGE.
	Language string `env:"SEARCH_LANGUAGE" default:"simple"`
	// Ranking orders results: "default", the database's own ranking, or
	// "bm25", which the database must provide (SQLite; PostgreSQL 17+
	// with the pg_textsearch extension), or the app refuses to start.
	// SEARCH_RANKING.
	Ranking string `env:"SEARCH_RANKING" default:"default"`
}

SearchConfig is the full-text search settings, read by Connect.

func (SearchConfig) Validate added in v0.3.0

func (c SearchConfig) Validate() error

Validate checks the settings' form; DB.CheckSearch checks that the database supports them.

type SearchIndex added in v0.3.0

type SearchIndex struct {
	// Table is the indexed table.
	Table string
	// Columns are the indexed columns, most important first.
	Columns []string
	// Language and Ranking are the settings it was built for.
	Language, Ranking string
}

SearchIndex is a search index, as recorded by the migration that made it (see migrate.Table.SearchIndex).

func SearchIndexes added in v0.3.0

func SearchIndexes(ctx context.Context) ([]SearchIndex, error)

SearchIndexes returns the search indexes of the database in ctx, by table name.

type SetExpr added in v0.3.0

type SetExpr struct {
	// SQL is the expression, in the database's SQL.
	SQL string `json:"sql"`
	// Args are its arguments.
	Args []any `json:"args,omitempty"`
}

SetExpr is an assignment of a bulk update that isn't a plain value (Column.SetRaw), as SQL with its arguments.

type SoftDeletes

type SoftDeletes struct {
	DeletedAt *time.Time `db:"deleted_at" json:"deleted_at,omitempty"` // nil unless deleted
}

SoftDeletes makes Delete set deleted_at instead of removing the row, and hides deleted rows from queries unless WithTrashed or OnlyTrashed is used.

func (SoftDeletes) Trashed

func (s SoftDeletes) Trashed() bool

Trashed reports whether the row has been soft-deleted.

type Tabler

type Tabler interface {
	// TableName returns the table's name. It must not depend on the value.
	TableName() string
}

Tabler lets a model choose its table name. Without it, the table is the snake_case plural of the type name (Post → posts, Category → categories).

type Timestamps

type Timestamps struct {
	CreatedAt time.Time `db:"created_at" json:"created_at"` // set by Create
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"` // set by Create, Update and mass updates
}

Timestamps adds created_at and updated_at columns, set automatically by Create, Update and mass updates.

type Values added in v0.3.0

type Values map[string]any

Values are a row's column values, by column name: nil, the field's value (ints, floats, strings, bools, []byte, the value of a driver.Valuer), a time.Time (UTC, to the microsecond), or a json.RawMessage for JSON columns.

type Vector added in v0.3.0

type Vector []float32

Vector is an embedding: a column of the vector type migrations make (migrate.Table.Vector), read and written as the database needs it (pgvector's text, MariaDB's and SQLite's binary float32s).

func (*Vector) Scan added in v0.3.0

func (v *Vector) Scan(src any) error

Scan implements sql.Scanner: a string is pgvector's text ("[1,2,3]", which the PostgreSQL driver returns), bytes are 4-byte little-endian floats (MariaDB's and SQLite's storage).

type Watcher added in v0.3.0

type Watcher interface {
	// Written runs after the write, in its transaction, with its context:
	// queries made with ctx are part of the transaction. An error rolls
	// the write back and is returned by the call that made it.
	Written(ctx context.Context, w *Write) error
}

A Watcher is told about the writes to the tables it watches (DB.Watch).

type Write added in v0.3.0

type Write struct {
	// Op is the kind of write.
	Op Op
	// Table is the table written.
	Table string
	// Key is the primary key of the row (single-row writes).
	Key any
	// Before are the row's values before an update or a force delete,
	// read in the write's transaction, so they are the database's.
	Before Values
	// After are the row's values after a create (without readonly
	// columns, which the database sets) or an update (the values before,
	// with the columns the update wrote).
	After Values
	// Bulk describes a bulk write; nil for single-row writes.
	Bulk *Bulk
}

Write describes a write to a watched table. Single-row writes set Key and the values; bulk writes set Bulk.

Directories

Path Synopsis
Package dbtest is a conformance suite for db drivers.
Package dbtest is a conformance suite for db drivers.
Package factory makes model values for tests and seeders:
Package factory makes model values for tests and seeders:
Package migrate changes database schemas with versioned migrations written in Go (or SQL), compiled into the app's binary, and fills them with seeders.
Package migrate changes database schemas with versioned migrations written in Go (or SQL), compiled into the app's binary, and fills them with seeders.

Jump to

Keyboard shortcuts

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