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

sqlitedatabase

package
v0.1.0 Latest Latest
Warning

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

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

README

sqlitedatabase

Use a local SQLite database through the database contract and the pure-Go modernc.org/sqlite driver. Run go get arkoren.dev/driver/sqlite from your application's module, then import arkoren.dev/driver/sqlite/sqlitedatabase and arkoren.dev/core/service/database.

Open a file or an isolated memory database

For a file path or native SQLite URI in dsn, construct a lazy pool:

driver, err := sqlitedatabase.New(dsn)

Check err before use. Start(ctx), Ping(ctx), or the first operation opens the database; construction does not open files or run schema DDL. Register Stop with your application's cleanup. NewFrom(pool, configure...) takes ownership of the supplied *sql.DB and applies only positive pool settings; it does not rewrite the DSN or apply the memory-connection default. Its native connections must implement database/sql/driver.ConnBeginTx, as the pinned modernc driver does. Transactions on other native profiles return errors.ErrUnsupported rather than falling back to a contextless BEGIN.

For isolated tests, use New(":memory:"). Memory DSNs default to one connection, including :memory: with parameters, file::memory:, and file: URIs whose decoded mode parameter is memory. Text such as mode=memory inside a file path does not select this policy. An explicit positive MaxOpenConns overrides the default; private memory connections then have independent databases. Named shared-cache memory databases share their native identity. Closing the last connection destroys the database, so avoid connection expiry for memory pools.

New adds _time_format=sqlite unless the native parameters already set it; filename text does not select the timestamp policy. This keeps emitted timestamp text in SQLite's format. The pinned native driver can also read Go's time text, but that does not make mixed text representations safe for SQL comparisons. Store times in UTC for consistent text ordering.

Keep scoped SQL on one connection

With an existing ctx and the arkoren.dev/foundation/query import:

db := database.New(driver)
err := db.WithSession(ctx, func(ctx context.Context) error {
	return db.WithTransaction(ctx, func(ctx context.Context) error {
		_, err := db.Exec(ctx, query.Raw("UPDATE counters SET value = value + 1"))
		return err
	})
})

Handle the returned error. Sessions do not start implicit transactions or undo autocommit statements. Explicit nested transactions use savepoints. Cancellation requests synchronous native rollback before managed locks release; it does not use an independent sql.Tx cancellation worker. Native BEGIN still receives the caller's context. The bounded savepoint-cleanup context does not put a time limit on native Commit or Rollback, whose interfaces take no context. Panic and runtime.Goexit paths roll back too. Cleanup closes outstanding cursors before releasing the retained connection. Callback contexts and drivers must be used synchronously; after return, session I/O fails with database.ErrSessionDone and transaction I/O fails with sql.ErrTxDone. database.ErrAfterCommit means committed data with failed cleanup; do not retry the transaction. Native SQL() returns the root pool and deliberately bypasses scopes and facade telemetry.

Raw Driver.Select replaces previous destination elements. An empty result leaves the slice empty; a scan failure can leave the successfully scanned prefix. The typed facade's Database.Select creates a fresh result for each call. Find and Select preserve native cursor-close failures alongside scan errors. A native cleanup failure makes the retained session unsafe to reuse or commit, even if the callback ignores the immediate read error.

See the database driver comparison for file-lock platform limits and the API reference.

Documentation

Overview

Package sqlitedatabase provides a SQLite database.Driver on database/sql. Constructors configure a lazily opened pool and never run schema DDL. NewFrom takes ownership of the supplied pool, which the root Driver.Stop closes. New and NewFrom apply Builder policies directly once in order before validation. Nil entries panic; nil or empty slice expansions omit edits. Completed settings detach from retained drafts. NewFrom only applies positive pool limits, not New's DSN rewriting or in-memory single-connection default. Memory DSNs include :memory: with parameters, file::memory:, and file URIs whose mode parameter is memory. New defaults them to one connection unless a positive MaxOpenConns is supplied; private memory connections do not share contents. Closing the last connection destroys the memory database. Only native query parameters select _time_format; filename substrings do not.

driver, err := sqlitedatabase.New(":memory:", func(connection *sqlitedatabase.Builder) {
	connection.MaxOpenConns(1)
})
if err != nil {
	return err
}
db := database.New(driver)

WithSession retains one physical connection without starting a transaction:

err := driver.WithSession(ctx, func(session database.Driver) error {
	return session.WithTransaction(ctx, func(tx database.Driver) error {
		_, err := tx.Exec(ctx, "UPDATE counters SET value = value + 1")
		return err
	})
})

Transactions use the retained connection and nested transactions use savepoints. Named locks combine process-local exclusion with sibling file locks on supported Unix hosts. They are not reentrant. Transaction locks survive nested rollback; nontransactional session locks end with their owning callback unless explicitly unlocked earlier. Callback drivers and cursors expire on return, and cleanup closes outstanding results before releasing the connection. Native SQL returns the root pool and deliberately bypasses scopes. The driver owns native transaction completion instead of using sql.Tx's automatic rollback worker, so cancellation finishes rollback before managed locks release. Native BEGIN keeps its caller context. Native Commit/Rollback do not take contexts and are not bounded by the savepoint-cleanup timeout. Panic and runtime.Goexit both roll back rather than commit incomplete bodies.

Raw Driver.Select replaces previous destination elements; an empty result leaves an empty slice. A scanning failure can leave the successfully scanned prefix. The Database.Select facade creates a fresh typed result instead. Native close errors from Find and Select are preserved alongside scan errors and prevent a retained session from committing or being pooled as healthy.

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidConfig = errors.New("invalid SQLite configuration")

ErrInvalidConfig indicates that the arguments or pool settings given to New cannot establish a driver. Blank DSNs and negative pool limits fail before opening a database; native file/URI errors can still arise on first use.

Example:

_, err := sqlitedatabase.New(" ")
if errors.Is(err, sqlitedatabase.ErrInvalidConfig) {
	return errors.New("check the database configuration")
}

Functions

This section is empty.

Types

type Builder

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

Builder edits optional pool limits during New or NewFrom. Zero leaves native limits unchanged, except New's single-connection in-memory default. Completed settings are copied; retained drafts cannot change a driver. Callbacks run once in order; supplied nil callbacks panic, nil slice expansion omits edits.

Example:

driver, err := sqlitedatabase.New("app.db", func(connection *sqlitedatabase.Builder) {
	connection.MaxOpenConns(10)
})
if err != nil {
	return err
}
return driver.Start(ctx)

func (*Builder) ConnMaxIdleTime

func (connection *Builder) ConnMaxIdleTime(value time.Duration) *Builder

ConnMaxIdleTime replaces the idle connection age and returns this draft. Only positive values apply to supplied pools; New rejects negative durations.

Example:

driver, err := sqlitedatabase.New("app.db", func(connection *sqlitedatabase.Builder) {
	connection.ConnMaxIdleTime(time.Minute)
})
if err != nil {
	return err
}
return driver.Start(ctx)

func (*Builder) ConnMaxLifetime

func (connection *Builder) ConnMaxLifetime(value time.Duration) *Builder

ConnMaxLifetime replaces the connection age limit and returns this draft. Zero preserves the native limit; New rejects negative durations.

Example:

driver, err := sqlitedatabase.New("app.db", func(connection *sqlitedatabase.Builder) {
	connection.ConnMaxLifetime(time.Hour)
})
if err != nil {
	return err
}
return driver.Start(ctx)

func (*Builder) MaxIdleConns

func (connection *Builder) MaxIdleConns(value int) *Builder

MaxIdleConns replaces the idle connection limit and returns this draft. Only positive values apply to supplied pools; New rejects negative limits.

Example:

driver, err := sqlitedatabase.New("app.db", func(connection *sqlitedatabase.Builder) {
	connection.MaxIdleConns(2)
})
if err != nil {
	return err
}
return driver.Start(ctx)

func (*Builder) MaxOpenConns

func (connection *Builder) MaxOpenConns(value int) *Builder

MaxOpenConns replaces the open connection limit and returns this draft. Zero keeps native defaults; New rejects negative limits.

Example:

driver, err := sqlitedatabase.New("app.db", func(connection *sqlitedatabase.Builder) {
	connection.MaxOpenConns(10)
})
if err != nil {
	return err
}
return driver.Start(ctx)

type BuilderFunc

type BuilderFunc = func(*Builder)

BuilderFunc edits a fresh construction draft supplied to New or NewFrom. Policies run synchronously once in order; nil entries panic, and retaining the draft does not let a caller change completed driver settings.

Example:

policy := sqlitedatabase.BuilderFunc(func(connection *sqlitedatabase.Builder) {
	connection.MaxOpenConns(1)
})
driver, err := sqlitedatabase.New(":memory:", policy)
if err != nil {
	return err
}
return driver.Start(ctx)

type Driver

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

Driver implements database.Driver for SQLite on database/sql, scanning rows with query.Targets. Callback drivers from WithSession and WithTransaction use one retained connection and expire when their callback returns. Native SQL access deliberately returns the root pool instead.

Example:

driver, err := sqlitedatabase.New(":memory:")
if err != nil {
	return err
}
db := database.New(driver)
return db.WithSession(ctx, func(ctx context.Context) error {
	_, err := db.Find[int](ctx, query.Raw("SELECT 1"))
	return err
})

func New

func New(dsn string, configure ...BuilderFunc) (*Driver, error)

New configures SQLite and returns a Driver. In-memory DSNs default to one connection, including ":memory:" with parameters, "file::memory:", and file URIs with mode=memory. An explicit positive MaxOpenConns overrides that default; private memory databases then have independent contents per connection. Closing the last connection destroys a memory database, including after pool expiry.

dsn is the SQLite data source name. It is required and has no default; a blank dsn returns an error wrapping ErrInvalidConfig naming the data source name, as does a negative pool limit. Policies edit Builder directly once in order before validation; nil entries panic. No database opens until Driver.Start or the first operation. Completed settings detach from retained drafts. Use NewFrom to wrap a pool the caller already opened.

New adds _time_format=sqlite unless the native query parameters already set _time_format. Filename text does not select this policy. The default emits SQLite timestamp text rather than Go's time.Time.String text. Values written to columns declared DATE, DATETIME, or TIMESTAMP (the types the query builder uses for Date, Timestamp, and TimestampTZ on query.SQLite) round-trip exactly, offset included, and CURRENT_TIMESTAMP defaults scan too. SQLite has no timestamp type and compares these values as text, so ordering and comparisons are correct only when every value is written in UTC; convert with time.Time.UTC before writing.

Example:

driver, err := sqlitedatabase.New(dsn, func(connection *sqlitedatabase.Builder) {
	connection.MaxOpenConns(10)
})
if err != nil {
	return err
}
return driver.Start(ctx)

func NewFrom

func NewFrom(pool *sql.DB, configure ...BuilderFunc) *Driver

NewFrom wraps a SQLite pool the caller opened, for example with database/sql.Open and the "sqlite" driver. The driver takes ownership: Driver.Stop closes the pool.

Only positive Builder pool limits apply, on top of the limits already set on pool. New's in-memory single-connection rule and its time format are not applied to a pool opened elsewhere. Native connections must implement database/sql/driver.ConnBeginTx, as the pinned modernc driver does, so BEGIN uses its caller context without an independent sql.Tx rollback worker. Other native profiles fail transactions with an error matching errors.ErrUnsupported rather than dropping cancellation. Policies run once in order on one draft; nil entries panic and expanded nil or empty slices omit edits. Retained drafts cannot change the pool later.

Example:

driver := sqlitedatabase.NewFrom(pool, func(connection *sqlitedatabase.Builder) {
	connection.MaxOpenConns(10)
})
return driver.Stop(ctx)

func (*Driver) Dialect

func (driver *Driver) Dialect() query.Dialect

Dialect reports query.SQLite for roots and callback handles alike. Use it when compiling SQL placeholders and identifiers for a native operation.

Example:

statement, err := query.Raw("SELECT ?", 7).Compile(driver.Dialect())
if err != nil {
	return err
}
_, err = driver.Exec(ctx, statement.SQL(), statement.Args()...)

func (*Driver) Exec

func (driver *Driver) Exec(ctx context.Context, statement string, args ...any) (int64, error)

Exec executes a statement that modifies data and returns the number of rows it affected. Callback drivers use only their retained executor; an ended callback or canceled enclosing scope fails before native SQL is sent.

Example:

changed, err := driver.Exec(ctx, "UPDATE counters SET value = value + 1 WHERE id = ?", id)

func (*Driver) Find

func (driver *Driver) Find(ctx context.Context, statement string, dest any, args ...any) (err error)

Find scans the first result row into dest, a pointer to a struct or to a single-column value. When no row matches, the returned error wraps both sql.ErrNoRows and database.ErrNoRows. Cursor-close failures are returned alongside scan or iteration errors, and make a retained session unsafe to reuse.

Example:

var value int
if err := driver.Find(ctx, "SELECT value FROM counters WHERE id = ?", &value, id); err != nil {
	return err
}

func (*Driver) Lock

func (driver *Driver) Lock(ctx context.Context, name string) (func(context.Context) error, error)

Lock blocks until the named lock is held or ctx ends. The returned unlock function releases the lock and is safe to call more than once.

The lock is an in-process lock shared by all drivers in the process and, for a file-backed database on Linux, macOS, or a BSD, an operating system file lock on a sibling lock file, so the lock also spans processes. Inside a transaction the lock is held until the transaction ends and unlock does nothing. In an ordinary session callback, remaining locks are released on callback return. An inner session owns its new locks without adopting its parent's.

Example:

unlock, err := driver.Lock(ctx, "nightly-report")
if err != nil {
	return err
}
defer unlock(context.WithoutCancel(ctx))

func (*Driver) Ping

func (driver *Driver) Ping(ctx context.Context) error

Ping verifies connectivity through the pool on the root and the current retained executor in a callback. It never allocates a second scoped connection. An expired callback fails with its session or transaction lifetime sentinel.

Example:

if err := driver.Ping(ctx); err != nil {
	return err
}

func (*Driver) Query

func (driver *Driver) Query(ctx context.Context, statement string, args ...any) (database.Rows, error)

Query executes a statement and returns a cursor over its rows. Close it when done because it retains connection capacity. Callback cursors are also closed automatically on return and reject later iteration with the scope's sentinel.

Example:

rows, err := driver.Query(ctx, "SELECT value FROM counters ORDER BY id")
if err != nil {
	return err
}
defer rows.Close()
for rows.Next() {
	var value int
	if err := rows.Scan(&value); err != nil {
		return err
	}
}
return rows.Err()

func (*Driver) SQL

func (driver *Driver) SQL() *sql.DB

SQL returns the underlying database/sql pool, accepted by any Go SQL library. It is always the pool, also when called on the driver of a transaction, so queries made through it run outside any transaction. Native calls also bypass callback lifetime guards and facade telemetry; keep protected work on the callback driver or its facade context instead.

Example:

stats := driver.SQL().Stats()
openConnections := stats.OpenConnections

func (*Driver) Select

func (driver *Driver) Select(ctx context.Context, statement string, dest any, args ...any) (err error)

Select scans every result row into dest, a pointer to a slice of structs, of pointers to structs, or of single-column values. Struct columns follow query.Targets. A successful query replaces existing elements; no rows leaves dest empty. A scan failure can leave the successfully scanned prefix. Invalid destinations and expired callbacks fail before any SQL is sent. Cursor-close failures are joined with scan/iteration errors and poison a retained session.

Example:

ids := []int{99}
err := driver.Select(ctx, "SELECT id FROM reports ORDER BY id", &ids)

func (*Driver) Start

func (driver *Driver) Start(ctx context.Context) error

Start verifies connectivity so a misconfigured database fails fast. The pool itself is opened lazily by New. On callback drivers, readiness runs through the retained executor and rejects an expired callback.

Example:

if err := driver.Start(ctx); err != nil {
	return err
}

func (*Driver) Stop

func (driver *Driver) Stop(context.Context) error

Stop closes the owned root connection pool through database/sql, including a pool transferred through NewFrom. It ignores ctx, so an expired context does not skip release. Finish callbacks and close cursors before stopping. On a callback driver Stop is a no-op because only the root owns the pool.

Example:

if err := driver.Stop(context.Background()); err != nil {
	return err
}

func (*Driver) TryLock

func (driver *Driver) TryLock(ctx context.Context, name string) (func(context.Context) error, bool, error)

TryLock takes the named lock without waiting. It reports acquired false, with a nil error, when another holder has the lock. The returned unlock function releases the lock and is safe to call more than once.

The lock is the one Driver.Lock takes, so the two exclude each other in the process and, where Driver.Lock spans processes, across them. Inside a transaction the lock is held until the transaction ends and unlock does nothing.

Example:

unlock, acquired, err := driver.TryLock(ctx, "nightly-report")
if err != nil || !acquired {
	return err
}
defer unlock(ctx)

func (*Driver) WithSession

func (driver *Driver) WithSession(ctx context.Context, fn func(session database.Driver) error) (retErr error)

WithSession retains one physical SQLite connection without starting a transaction. Nested calls borrow it but expire their own callback drivers and cursors on return. Errors do not undo committed statements, and failed cleanup discards the connection rather than allowing unrelated work to reuse it.

Example:

err := driver.WithSession(ctx, func(session database.Driver) error {
	_, err := session.Exec(ctx, "CREATE TEMPORARY TABLE selected (id INTEGER)")
	return err
})

func (*Driver) WithTransaction

func (driver *Driver) WithTransaction(ctx context.Context, fn func(tx database.Driver) error) error

WithTransaction begins on a retained physical connection, or uses a savepoint when an enclosing transaction exists. Nested locks belong to the outermost transaction and survive savepoint rollback. Callback drivers and cursors expire on return, even while an enclosing transaction remains live.

A body error rolls back. Panic cleanup re-raises the original panic. Cleanup errors after a confirmed commit wrap database.ErrAfterCommit, so callers must not retry committed work. Session-owned locks predating this transaction are not released by its cleanup. The driver owns native transaction completion: caller cancellation requests synchronous rollback before managed locks are released, rather than relying on database/sql's asynchronous sql.Tx cancellation worker. Native BEGIN still uses the caller context. A callback that exits via runtime.Goexit rolls back. Native Commit and Rollback have no context parameter and can outlive the savepoint cleanup budget; this driver does not claim to interrupt those calls.

Example:

err := driver.WithTransaction(ctx, func(tx database.Driver) error {
	_, err := tx.Exec(ctx, "UPDATE counters SET value = value + 1")
	return err
})

Directories

Path Synopsis
internal
filelock
Package filelock implements only the platform file-lock primitive.
Package filelock implements only the platform file-lock primitive.

Jump to

Keyboard shortcuts

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