Documentation
¶
Overview ¶
Package sqlx lets one store implementation serve both SQL backends.
SQLite and PostgreSQL stores in this repository were historically written twice. Comparing the two halves, the differences were never about business logic — they were the driver handle type, the placeholder syntax, the no-rows sentinel, the Exec result shape, and a handful of DDL type names. This package absorbs exactly those five things and nothing else.
Deliberately NOT abstracted:
- Value binding and scanning. Both drivers already agree: a Go bool binds to SQLite INTEGER and PostgreSQL BOOLEAN, an INTEGER scans into *bool, and TEXT/JSON/JSONB all scan into *[]byte. Stores bind and scan plain Go types on both backends.
- Genuinely dialect-specific migrations. Rewriting a table is a different operation in each engine; those stay behind a Dialect() check rather than pretending to be portable.
- MongoDB. Document semantics are not a SQL dialect and stay hand-written.
Index ¶
Constants ¶
const ( // TypeInt64 is a 64-bit integer column: unix timestamps, byte counts. TypeInt64 = "{int64}" // TypeBool is a boolean column. SQLite has no boolean type and stores // 0/1 in an INTEGER; both drivers bind and scan Go bool against it. TypeBool = "{bool}" // TypeFloat is a floating-point column: costs, scores. TypeFloat = "{float}" // TypeJSON is a JSON column that SQLite declares as JSON. TypeJSON = "{json}" // TypeJSONText is a JSON column that SQLite declares as TEXT. TypeJSONText = "{json_text}" // TypeTimestamp is an absolute-time column. Note that the two engines do // not agree on how a value binds to it: see TimestampArg. TypeTimestamp = "{timestamp}" // TypeSerialPK is an auto-assigned integer primary key. TypeSerialPK = "{serial_pk}" )
Portable DDL type tokens.
The two backends declare the same logical column with different type names. A store writes the token; the dialect expands it. Types that are already spelled the same on both backends (TEXT, INTEGER for genuinely 32-bit counters) are written literally and need no token.
The expansions reproduce the column types the hand-written stores already used, so `CREATE TABLE IF NOT EXISTS` stays a no-op against existing databases and a fresh database gets a byte-identical schema. Both JSON tokens exist for that reason: some SQLite tables declared JSON columns as `JSON` and others as `TEXT`, and preserving each avoids changing the column affinity of tables already in the field.
Variables ¶
var ErrNoRows = errors.New("sqlx: no rows in result set")
ErrNoRows is returned by Row.Scan when a query selected no rows. It replaces the driver-specific sql.ErrNoRows and pgx.ErrNoRows so store code has one sentinel to match with errors.Is.
Functions ¶
func AddColumns ¶
AddColumns applies ALTER TABLE ... ADD COLUMN migrations, expanding type tokens and ignoring the error each engine returns when the column is already present.
Tolerating the error is the portable form: SQLite has no ADD COLUMN IF NOT EXISTS, so the PostgreSQL store used the IF NOT EXISTS spelling while the SQLite store pattern-matched its error text. Both engines reject a duplicate add with a recognisable message, so one list now serves both.
func IsDuplicateColumnError ¶
IsDuplicateColumnError reports whether err is an engine's complaint that a column being added already exists: SQLite says "duplicate column name", and PostgreSQL says `column "x" of relation "y" already exists`.
The "already exists" arm requires the word "column" as well. The two stores that pattern-matched this before disagreed on that point, and the loose form would let an unrelated already-exists failure pass for an applied migration.
Types ¶
type DB ¶
type DB interface {
Querier
// Dialect reports the backend, for the rare statement that cannot be
// written portably (schema migrations, mostly).
Dialect() Dialect
// Schema executes DDL statements in order, expanding portable type tokens
// (see Dialect.ExpandTypes). It is what store constructors call to create
// their tables and indexes.
Schema(ctx context.Context, statements ...string) error
// InTx runs fn inside a transaction, committing when fn returns nil and
// rolling back otherwise.
//
// Atomicity is guaranteed on both engines; serializability is not. SQLite
// takes the write lock up front (BEGIN IMMEDIATE), so concurrent
// transactions queue. PostgreSQL runs at its default READ COMMITTED, so
// two transactions can read the same value and the second fails on a
// constraint instead of waiting. Code that allocates a key from a MAX must
// therefore be prepared for a conflict error rather than assuming it was
// serialized.
InTx(ctx context.Context, fn func(Querier) error) error
}
DB is a database handle shared by every SQL store.
func NewPostgreSQL ¶
NewPostgreSQL wraps a pgx pool. The caller retains ownership of the pool.
type Dialect ¶
type Dialect string
Dialect identifies a SQL backend.
func (Dialect) ExpandTypes ¶
ExpandTypes replaces portable type tokens with this dialect's column types. Statements containing no tokens are returned unchanged.
func (Dialect) NullableTimestampArg ¶
NullableTimestampArg is TimestampArg, mapping the zero time to SQL NULL.
func (Dialect) TimestampArg ¶
TimestampArg converts a time into the form this dialect's TypeTimestamp column expects.
PostgreSQL binds a time.Time to TIMESTAMPTZ directly. SQLite has no real date type: these columns hold RFC3339 text, which is what the readers parse back, so the store must keep writing text rather than letting the driver choose a representation.
type Querier ¶
type Querier interface {
// Exec runs a statement and reports how many rows it affected.
Exec(ctx context.Context, query string, args ...any) (int64, error)
// Query runs a query returning multiple rows.
Query(ctx context.Context, query string, args ...any) (Rows, error)
// QueryRow runs a query returning at most one row. Errors surface from
// the returned Row's Scan.
QueryRow(ctx context.Context, query string, args ...any) Row
}
Querier runs statements against a database or an open transaction.
Queries are written with `?` placeholders regardless of backend; the PostgreSQL adapter rewrites them to $1, $2, ... in order. A `?` inside a string literal, quoted identifier, or comment is left alone.
type Row ¶
type Row interface {
// Scan copies the row's columns into dest. It returns ErrNoRows when the
// query selected nothing.
Scan(dest ...any) error
}
Row is a single-row query result.
type Rows ¶
Rows is a multi-row query result. Callers must Close it, and must check Err after the Next loop ends.
type Timestamp ¶
Timestamp scans a TypeTimestamp column from either engine. It is the read side of TimestampArg: PostgreSQL hands back a time.Time, SQLite the RFC3339 text that was written.
Text it cannot parse leaves Time zero and Valid false rather than failing the scan. A reader returning a page of rows should not fail the whole page because one row holds an unreadable timestamp; callers that care report Raw.