Documentation
¶
Overview ¶
Package db stores tanGO models in SQLite or PostgreSQL.
A Store wraps a *sql.DB with the Dialect it speaks: NewStore creates one, and its methods create, read, update, delete and filter models, generating each dialect's SQL. Raw SQL goes through the Store too, when a query outgrows the built-in filtering. ParseDSN turns a scheme-qualified connection string (sqlite://app.db, postgres://…) into the dialect, driver name and connection string to open, so the three can't disagree.
See https://tangoframework.com/docs/guides/persistence-crud-and-raw-sql/ and https://tangoframework.com/docs/guides/sqlite-and-postgresql-setup/.
Index ¶
- Constants
- Variables
- func ColumnName(name string) string
- func IsUniqueConstraintViolation(err error) bool
- func SQLiteForeignKeysDSN(dsn string) string
- type Condition
- type DSN
- type Dialect
- type Expr
- type ExprOp
- type InvalidJSONError
- type Op
- type Query
- type QueryError
- type QueryLimitError
- type Store
- func (s *Store) Count(ctx context.Context, meta model.ModelMeta, query Query) (int, error)
- func (s *Store) Create(ctx context.Context, meta model.ModelMeta, dest any) error
- func (s *Store) Delete(ctx context.Context, meta model.ModelMeta, pk any) error
- func (s *Store) Exists(ctx context.Context, meta model.ModelMeta, query Query) (bool, error)
- func (s *Store) First(ctx context.Context, meta model.ModelMeta, query Query, dest any) error
- func (s *Store) Get(ctx context.Context, meta model.ModelMeta, pk any, dest any) error
- func (s *Store) InTx(ctx context.Context, fn func(tx *Store) error) (err error)
- func (s *Store) List(ctx context.Context, meta model.ModelMeta, query Query, dest any) error
- func (s *Store) Query(ctx context.Context, dest any, sqlQuery string, args ...any) error
- func (s *Store) QueryRow(ctx context.Context, dest any, sqlQuery string, args ...any) error
- func (s *Store) Update(ctx context.Context, meta model.ModelMeta, dest any) error
- func (s *Store) UseModels(models *model.Registry)
- type ValueTooLongError
Constants ¶
const ( // MaxFilterNodes is the most nodes a Query.Filter may hold, counting every // operator and every condition. MaxFilterNodes = 100 // MaxFilterDepth is the deepest a Query.Filter may nest; the root is // level 1. MaxFilterDepth = 8 )
const ( // MaxInValues is the most values one IN list may hold. MaxInValues = 1000 // MaxQueryArgs is the most bind values one Query may send across Where, // Any and Filter. It is one number for every dialect, well under each // driver's own ceiling, so a query that works on one dialect works on all. MaxQueryArgs = 10000 )
Variables ¶
var ErrInvalidForeignKey = errors.New("tango db: invalid foreign key")
ErrInvalidForeignKey is returned by Create and Update when a set (non-zero) foreign key field does not reference an existing row of its related model. This is referential-integrity validation: a preflight SELECT, not run inside a shared transaction with the write it guards, and only performed when UseModels has been called. It is distinct from model.ErrUnknownForeignKeyTarget, which validates that the related *model* exists at all, once, at registration time.
var ErrInvalidJSON = errors.New("tango db: invalid JSON")
ErrInvalidJSON is matched (with errors.Is) by the error Create and Update return when a JSON field, tagged tango:"json", holds text that is not one valid JSON document. Use errors.As with *InvalidJSONError for the details.
var ErrInvalidQuery = errors.New("tango db: invalid query")
ErrInvalidQuery is matched (with errors.Is) by every error that says a Query is malformed: an unknown field, an unsupported operator, a bad value, a malformed filter tree, or a filter over a limit. Use errors.As with *QueryError or *QueryLimitError for the details.
var ErrNotFound = errors.New("tango db: not found")
ErrNotFound is returned by Get, Update, and Delete when no row matches the given primary key.
var ErrQueryLimit = errors.New("tango db: query limit exceeded")
ErrQueryLimit is matched (with errors.Is) by the error a Query returns when it is over one of the documented limits. Such an error also matches ErrInvalidQuery.
var ErrValueTooLong = errors.New("tango db: value too long")
ErrValueTooLong is matched (with errors.Is) by the error Create and Update return when a bounded string field, tagged tango:"varchar=n", holds more than n runes. Use errors.As with *ValueTooLongError for the details.
Functions ¶
func ColumnName ¶
ColumnName derives the database table or column name for a Go identifier.
func IsUniqueConstraintViolation ¶
IsUniqueConstraintViolation reports whether err is a unique-constraint violation returned by Create or Update, on any supported dialect.
This exists so a caller can attempt a write directly and treat the database's own unique constraint as the actual correctness boundary for a duplicate value, rather than a SELECT-then-INSERT pre-check — which only narrows, and never closes, the race between two concurrent writers for the same unique value. A pre-check may still exist purely for a faster/friendlier error on the common non-racing path, but correctness must not depend on it.
func SQLiteForeignKeysDSN ¶
SQLiteForeignKeysDSN appends the modernc.org/sqlite driver's "_foreign_keys=on" query parameter to a SQLite DSN, so every connection the driver opens for it enforces FOREIGN KEY constraints (SQLite treats this as a per-connection setting, off by default). This is a plain string helper — it never imports the driver itself, keeping db driver-agnostic — so it composes with any DSN shape (a file path, ":memory:", or one that already carries other query parameters).
ParseDSN applies it to every sqlite:// DSN, so an app that opens its database from ParseDSN's result (as tango.LoadDBConfigFromEnv, and so the generated scaffold, does) already enforces foreign keys. Anyone opening their own *sql.DB with driver "sqlite" from a bare path should call it too if they rely on foreign key constraints. It isn't automatic inside db.NewStore itself: NewStore wraps an already-open *sql.DB and must never touch it, so the flags that don't need a database ("-check", "-tango-dump-models") stay database-free.
Types ¶
type Condition ¶
Condition compares one model field with a non-nil value. OpLike accepts a SQL LIKE pattern on string fields, including % and _ wildcards. OpIsNull and OpIsNotNull take no Value.
An unset foreign key (zero in Go) is stored as NULL, so a condition such as {Field: "OwnerID", Op: OpEq, Value: 0} never matches any row; the SQL is left exactly as written. Use OpIsNull to find unset foreign keys and OpIsNotNull for set ones. Comparisons with NULL are never true, so OpNe and Not(...) also leave NULL rows out; add an IS NULL alternative to include them.
type DSN ¶ added in v0.1.0
type DSN struct {
// Dialect is the SQL dialect the DSN's scheme names.
Dialect Dialect
// Driver is the database/sql driver name: "sqlite", "pgx" or "mysql".
Driver string
// Source is the driver's own data source name.
Source string
}
DSN is a parsed scheme-qualified DSN: the Dialect to build a Store with, and the two arguments to pass to sql.Open.
func ParseDSN ¶ added in v0.1.0
ParseDSN turns a scheme-qualified DSN into the Dialect, the database/sql driver name, and the driver's own data source name to pass to sql.Open:
sqlite://app.db SQLite file relative to the working directory sqlite:///var/data/app.db SQLite file at an absolute path sqlite://:memory: SQLite, in-memory postgres://… postgresql://… PostgreSQL, passed to pgx unchanged mysql://user:pass@host:3306/app MySQL, converted to the driver's own form
Anything else, including a bare path such as "app.db", is an error. For SQLite the returned Source already enables foreign key enforcement (see SQLiteForeignKeysDSN) and makes a writer wait up to 5 seconds for another connection's write lock instead of failing with SQLITE_BUSY, unless the DSN sets its own busy_timeout.
For MySQL the returned Source reads and writes times in UTC (parseTime and loc=UTC), counts matched rows as affected (clientFoundRows), uses utf8mb4 with the utf8mb4_bin collation, a strict SQL mode, and the READ COMMITTED isolation level. Query options on the mysql:// DSN are the driver's own and override those defaults; to choose another isolation level add transaction_isolation='REPEATABLE-READ' (quotes percent-encoded as %27). A DSN that does not parse is rejected without echoing it.
ParseDSN never imports or registers a driver for the caller: the app still imports modernc.org/sqlite ("sqlite"), github.com/jackc/pgx/v5/stdlib ("pgx") or github.com/go-sql-driver/mysql ("mysql") itself.
type Dialect ¶
type Dialect int
Dialect selects which SQL dialect Store generates statements for.
const ( // SQLite generates SQL using "?" placeholders and backfills // database-generated primary keys via sql.Result.LastInsertId. SQLite Dialect = iota // Postgres generates SQL using "$N" placeholders and backfills // database-generated primary keys via INSERT ... RETURNING. Postgres // MySQL generates SQL using "?" placeholders and backticked identifiers, // and backfills database-generated primary keys via // sql.Result.LastInsertId. MySQL )
type Expr ¶ added in v0.7.0
Expr is one node of a Query.Filter tree: plain data, no builder state. A leaf (ExprCond) carries Cond and no children; a group (ExprAnd, ExprOr or ExprNot) carries Children and no Cond. The zero Expr is invalid. A group with one child means that child; a group with none, or a NOT with other than one child, is rejected with ErrInvalidQuery.
func In ¶ added in v0.7.0
In returns a leaf matching rows whose field equals any value of values, which must be a slice or array whose elements have the field's type. A nil or empty list matches nothing. At most MaxInValues values are allowed.
func IsNull ¶ added in v0.7.0
IsNull returns a leaf matching rows where field is NULL, which is how an unset foreign key is stored.
type ExprOp ¶ added in v0.7.0
type ExprOp string
ExprOp says what kind of node an Expr is.
const ( // ExprCond is a leaf holding one Condition. ExprCond ExprOp = "COND" // ExprAnd holds children that must all match. ExprAnd ExprOp = "AND" // ExprOr holds children of which at least one must match. ExprOr ExprOp = "OR" // ExprNot holds exactly one child and negates it. ExprNot ExprOp = "NOT" )
type InvalidJSONError ¶ added in v0.4.0
InvalidJSONError says which JSON field was refused and where the syntax went wrong. Err carries only the byte offset of the problem, never any of the submitted text, so the error is safe to log and to show to a client. The check runs only in Create and Update: reads return stored text as it is, and raw SQL is outside the guarantee.
func (*InvalidJSONError) Error ¶ added in v0.4.0
func (e *InvalidJSONError) Error() string
func (*InvalidJSONError) Is ¶ added in v0.4.0
func (e *InvalidJSONError) Is(target error) bool
Is reports whether target is ErrInvalidJSON.
func (*InvalidJSONError) Unwrap ¶ added in v0.4.0
func (e *InvalidJSONError) Unwrap() error
Unwrap returns the syntax problem.
type Op ¶
type Op string
Op is a supported comparison in a Query condition.
const ( OpEq Op = "=" OpNe Op = "<>" OpGt Op = ">" OpGte Op = ">=" OpLt Op = "<" OpLte Op = "<=" OpLike Op = "LIKE" // OpIn matches any value of a slice or array Value; an empty list matches // nothing. See In. OpIn Op = "IN" // OpIsNull and OpIsNotNull test for SQL NULL and take no Value. They are // how to find an unset foreign key: see Condition. OpIsNull Op = "IS NULL" OpIsNotNull Op = "IS NOT NULL" )
type Query ¶
type Query struct {
Where []Condition
Any []Condition
// Filter is an optional boolean tree of conditions, ANDed with Where and
// Any. A nil Filter restricts nothing.
Filter *Expr
Limit int
Offset int
OrderBy []string
}
Query carries filtering, pagination, and ordering options for List. Where conditions are ANDed; Any conditions are ORed as one group, which is ANDed with Where when both are present, and Filter, when set, is ANDed with both.
type QueryError ¶ added in v0.7.0
type QueryError struct {
Model string
Field string
Reason string
// contains filtered or unexported fields
}
QueryError says why a Query was refused before any SQL ran. It matches ErrInvalidQuery.
func (*QueryError) Error ¶ added in v0.7.0
func (e *QueryError) Error() string
func (*QueryError) Is ¶ added in v0.7.0
func (e *QueryError) Is(target error) bool
Is reports whether target is ErrInvalidQuery.
type QueryLimitError ¶ added in v0.7.0
type QueryLimitError struct {
Limit string
Field string // the field of an IN list, when Limit is "IN values"
Max int
Got int
}
QueryLimitError says which limit a Query went over. Limit names it ("filter nodes" or "filter depth"), Max is the limit and Got the value found. When a walk stops as soon as it sees the limit exceeded, Got is the first value over Max rather than the full total. It matches ErrQueryLimit and ErrInvalidQuery.
func (*QueryLimitError) Error ¶ added in v0.7.0
func (e *QueryLimitError) Error() string
func (*QueryLimitError) Is ¶ added in v0.7.0
func (e *QueryLimitError) Is(target error) bool
Is reports whether target is ErrQueryLimit or ErrInvalidQuery.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is tanGO's persistence boundary over a database/sql connection.
func NewStore ¶
NewStore wraps sqlDB in a Store that generates SQL for dialect. It panics on a Dialect value this release does not define, rather than generating another dialect's SQL.
func (*Store) Count ¶
Count returns the number of rows matching query's Where and Any conditions. Limit, Offset, and OrderBy do not affect the total.
func (*Store) Create ¶
Create inserts dest using metadata-derived table and column names. A zero primary key is generated by the database and backfilled into dest; a set one is inserted as given, and on PostgreSQL the column's sequence is advanced past it so later Creates without an ID don't collide with it.
func (*Store) Delete ¶
Delete removes the row matching pk. If UseModels has been called, it first cascades: every row of every other registered model that references this one via a foreign key field is deleted first (recursively), all inside one transaction, before the target row itself is removed. See UseModels.
func (*Store) Exists ¶ added in v0.7.0
Exists reports whether any row matches query's Where, Any and Filter. Limit, Offset and OrderBy are ignored.
func (*Store) First ¶ added in v0.7.0
First reads the first row matching query into dest, a non-nil pointer to a struct of the model. With no OrderBy it orders by the primary key, ascending, so the result is deterministic; it always reads one row, honours Offset and ignores Limit. It returns an error matching ErrNotFound (naming the model) when no row matches. dest is left untouched on any error. The caller's query is never modified.
func (*Store) InTx ¶ added in v0.4.0
InTx runs fn in one database transaction. tx is a Store bound to it, so every method works on it, with the same validation: it commits when fn returns nil, rolls back when fn returns an error (returned as is), and on a panic rolls back and re-panics with the same value, so a programmer error is never turned into a returned error.
InTx on a Store that is already bound to a transaction joins it: there are no savepoints. An error from the inner callback rolls the work back only if it propagates out of the outermost callback; an outer callback that swallows it can still commit the inner writes. Operations that open their own transaction when called directly (a PostgreSQL insert with an explicit ID, which locks the table, and a cascading Delete) run on the enclosing one, so the lock is held until it ends.
func (*Store) List ¶
List selects rows into dest (a pointer to a slice of the model struct), applying query's Where, Any, Limit, Offset, and OrderBy.
func (*Store) Query ¶
Query executes sql with args and appends one scanned element per resulting row into dest (a pointer to a slice of structs), matching returned columns to struct fields by name (case-insensitively). A query matching no rows leaves dest an empty, non-nil slice with a nil error.
sqlQuery must use the placeholder syntax matching this Store's configured Dialect ("?" for SQLite, "$1", "$2", ... for Postgres) — Query does not translate or validate placeholder syntax between dialects.
func (*Store) QueryRow ¶
QueryRow executes sql with args and scans the single resulting row into dest (a pointer to a struct), matching returned columns to struct fields by name (case-insensitively). It returns an error wrapping ErrNotFound when the query yields no rows.
sqlQuery must use the placeholder syntax matching this Store's configured Dialect ("?" for SQLite, "$1", "$2", ... for Postgres) — QueryRow does not translate or validate placeholder syntax between dialects.
func (*Store) Update ¶
Update writes dest's current field values to the row matching its primary key.
func (*Store) UseModels ¶
UseModels attaches the model registry Delete needs to cascade: when set, deleting a row also deletes, recursively, every row of every other registered model that references it through a foreign key field (a tango:"fk=X" tag). This is application-level cascade, not a database ON DELETE CASCADE constraint. Without calling UseModels, Delete only removes the target row, exactly as before this method existed. tango.Registry.SetStore calls this automatically with its own Models(), so apps using the standard Registry/Serve flow get cascading deletes with no code change.
type ValueTooLongError ¶ added in v0.4.0
type ValueTooLongError struct {
Model string
Field string
Max int // the most runes the field may hold
Got int // the runes the value holds
}
ValueTooLongError says which bounded string field was too long, and by how much. Length is counted in runes (utf8.RuneCountInString): each byte of invalid UTF-8 counts as one rune, and a letter with a combining accent is two. It is the same rule on every dialect; raw SQL is outside it.
func (*ValueTooLongError) Error ¶ added in v0.4.0
func (e *ValueTooLongError) Error() string
func (*ValueTooLongError) Is ¶ added in v0.4.0
func (e *ValueTooLongError) Is(target error) bool
Is reports whether target is ErrValueTooLong.