AnnouncementsIntroducing Arkoren

    Menu

    Drivers

    Choose and configure SQLite, PostgreSQL, or MySQL, with clear compatibility differences.

    What's on this page
    1. Compare database drivers by transaction, locking, and SQL capabilities.
    2. Configure connections, verify readiness, and own pool shutdown.
    3. Understand memory-database limits and native-client escape hatches.
    On this page

    Choose SQLite for an embedded database or isolated tests. Choose PostgreSQL or MySQL when several application instances need a shared database server. All three implement the database contract; their SQL dialects and coordination boundaries still matter. There is no simulated memory driver: SQLite’s memory mode runs a real SQLite engine.

    Compatibility matrix

    This compares the shipped Arkoren drivers and query compiler, not every feature of each database product. Named locks coordinate cooperating callers, not all SQL writers. Persistence assumes a persistent database and suitable server or filesystem configuration; the driver does not supply replication or backups.

    Driver Persistent Storage Nested Transactions Transactional DDL Row Locks Cross-Process Named Locks RETURNING Upserts
    SQLite Yes No Yes Yes
    PostgreSQL Yes Yes Yes Yes Yes
    MySQL Yes No No

    All three support typed reads, writes, cursors, retained sessions, and named locks. Pagination and batching operate through the same facade. Read Locking for lifetime rules and migration deployment before relying on DDL rollback.

    SQLite

    SQLite runs in your process without a database server. Install its driver from your application’s module directory:

    go get arkoren.dev/driver/sqlite

    Connecting

    Import arkoren.dev/driver/sqlite/sqlitedatabase and arkoren.dev/core/service/database. For a local application file:

    driver, err := sqlitedatabase.New("app.db")
    db := database.New(driver)

    Handle the construction error before using the driver. Construction validates the required, nonblank DSN and nonnegative pool limits but opens the database lazily. With your startup context, verify the connection before accepting work:

    err := driver.Start(ctx)

    Start is a connectivity check, equivalent to Ping, not a background worker. After successful startup, use the shared typed read and write examples. Constructors do not create application tables; use migrations.

    When work has drained and all callbacks and cursors have finished, release the owned pool with driver.Stop(cleanupCtx). SQLite’s stop ignores that context and closes through database/sql. In an Arkoren application, register startup and cleanup using application composition.

    Memory Databases

    For a disposable test database, use an alternative construction:

    driver, err := sqlitedatabase.New(":memory:")

    New defaults recognized memory DSNs to one open connection. A private memory database belongs to a connection, so overriding that default with several connections can produce independent databases with different tables. Closing the last connection destroys the database, including pool expiry. Avoid connection lifetime/idle expiry when the memory database must survive.

    Memory databases have only process-local named locks. File-backed databases on Linux, macOS, and BSD also use sibling lock files for other processes on the same host. Other operating systems and network filesystems have no supported cross-process lock guarantee. SQLite is not a horizontally scaled database.

    Connection Settings

    For a file database, bound connections explicitly when needed:

    driver, err := sqlitedatabase.New("app.db", func(connection *sqlitedatabase.Builder) {
    	connection.MaxOpenConns(4)
    	connection.MaxIdleConns(2)
    })

    Positive limits replace native settings. Zero leaves them unchanged except New’s memory default. ConnMaxLifetime and ConnMaxIdleTime optionally bound connection reuse; zero does not disable an existing supplied-pool limit. Negative settings are rejected by New.

    New adds _time_format=sqlite unless the DSN explicitly sets it. DATE, DATETIME, and TIMESTAMP values round-trip using that format, but SQLite compares timestamps as text: write UTC consistently for correct ordering and comparisons. Configure per-connection database policies through the native DSN where needed; a one-off PRAGMA on the root pool does not configure every connection.

    For a file-backed application that needs foreign-key enforcement and a bounded wait for concurrent writers, set native pragmas in the DSN:

    dsn := "file:app.db?_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)"
    driver, err := sqlitedatabase.New(dsn)

    SQLite permits one writer at a time. A busy timeout lets competing writes wait up to 5,000 milliseconds rather than immediately returning SQLITE_BUSY. Foreign-key enforcement is per connection and is not enabled by declaring a schema foreign key alone. Add _pragma=journal_mode(WAL) if your deployment needs WAL’s reader/writer behavior, after checking its filesystem requirements.

    Native Pools

    If you already own a *sql.DB opened with the SQLite driver, transfer it with sqlitedatabase.NewFrom(pool). The Arkoren driver then owns shutdown. NewFrom does not rewrite the DSN or apply the memory single-connection default; configure both on the supplied pool yourself. Its positive builder policies apply to the existing pool.

    Native connections supplied to SQLite must implement database/sql/driver.ConnBeginTx, as the pinned modernc driver does. Unsupported native profiles fail transactions with errors.ErrUnsupported rather than falling back to a BEGIN that drops cancellation. Cancellation finalizes rollback before managed locks release, but native Commit/Rollback have no context argument and can outlive cleanup budgets.

    driver.SQL() returns the root *sql.DB, useful for Stats(). Native SQL bypasses facade telemetry, transaction routing, and retained-session lifetime checks. Keep protected work on the facade’s callback context.

    Running Locally

    No server is required. After driver.Start(ctx), verify a facade call with db.Find[int](ctx, query.Raw("SELECT 1")), importing arkoren.dev/foundation/query; the result is 1. Stop the driver when finished. Memory data disappears on last connection close; stopping a file-backed driver does not delete the database. Keep app.db, its -wal and -shm files, and app.db.*.lock sibling files out of version control.

    See the sqlitedatabase API.

    PostgreSQL

    Use PostgreSQL for a shared SQL server with transactional DDL and advisory locks within a database. Provision the database, a role with application permissions, and a DSN with the TLS policy required by your deployment. Install:

    go get arkoren.dev/driver/postgres

    Connecting

    Import arkoren.dev/driver/postgres/postgresdatabase and arkoren.dev/core/service/database. Here dsn is your configured PostgreSQL URL or key-value DSN, not a hardcoded production credential:

    driver, err := postgresdatabase.New(dsn)
    db := database.New(driver)

    Handle construction errors before using the driver. Blank or invalid DSNs and negative limits match postgresdatabase.ErrInvalidConfig. Parse diagnostics omit supplied values, but unwrapped native errors can still expose them; do not log those causes or the DSN.

    Construction performs no network I/O. driver.Start(ctx) or driver.Ping(ctx) verifies connectivity through pgxpool. Use that as a startup/readiness check, then use the shared typed operations. Drain callbacks and cursors before driver.Stop(cleanupCtx): stop waits for checked-out connections. If its context ends, it returns that error while pool closure continues in the background.

    Connection Settings

    If the application’s connection budget is twenty, configure the pool at construction:

    driver, err := postgresdatabase.New(dsn, func(connection *postgresdatabase.Builder) {
    	connection.MaxOpenConns(20)
    	connection.ConnMaxIdleTime(10 * time.Minute)
    })

    Import time. Zero retains pgxpool settings: by default the maximum is the larger of four and the CPU count, maximum lifetime is one hour, and maximum idle time is thirty minutes. MaxIdleConns is accepted for symmetry but has no effect; pgxpool has no idle-count limit. Use idle time instead.

    Nonzero native pool_min_conns and pool_min_idle_conns are rejected because they would connect during construction; pool_health_check_period must be positive. For eager minimum connections, create the native pool at application startup and transfer ownership with NewFrom.

    Native Pools

    With an existing *pgxpool.Pool, call postgresdatabase.NewFrom(pool). Configure its pgxpool.Config before creating it: NewFrom has no builder callbacks. The Arkoren driver owns the pool and closes it on stop.

    driver.DB() exposes that root pool for pgx features such as COPY, LISTEN, and batches. For example, driver.DB().Stat() reads native utilization. SQL issued through it bypasses facade scopes and telemetry, even if the accessor was reached from a transaction driver. Do not use it for work protected by a managed lock.

    For a native bulk import, assume an existing imports table with a text email column and import github.com/jackc/pgx/v5:

    copied, err := driver.DB().CopyFrom(ctx, pgx.Identifier{"imports"}, []string{"email"},
    	pgx.CopyFromRows([][]any{{"ada@example.com"}, {"grace@example.com"}}))

    On success, copied is 2. This native operation is outside facade transactions; use a native pgx transaction if several native operations must be atomic. For LISTEN, acquire and retain a native connection until listening ends, then release it; a pool-level LISTEN alone does not pin subsequent notification reads. Consult the pgx API for native batches and notification handling rather than assuming facade scope propagation.

    PgBouncer transaction pooling cannot preserve session advisory locks across operations. Use a direct or session-pooled connection for managed named locks and migrations; otherwise acquisition and release can reach different sessions.

    Running Locally

    With Docker installed and port 15432 free, start a disposable loopback-only server:

    docker run --name app-postgres -d -p 127.0.0.1:15432:5432 -e POSTGRES_USER=app -e POSTGRES_PASSWORD=local-only -e POSTGRES_DB=app postgres:17

    Wait for readiness using docker exec app-postgres pg_isready -U app -d app. When it reports accepting connections, use this development-only DSN:

    driver, err := postgresdatabase.New("postgres://app:local-only@127.0.0.1:15432/app?sslmode=disable")

    Call driver.Start(ctx), wrap with database.New(driver), and read db.Find[int](ctx, query.Raw("SELECT 1")); success returns 1. These credentials and disabled TLS are only for the disposable loopback server. Stop your application and driver before removing it. The following command deletes the container and its anonymous data volume; never use it for data you need to keep:

    docker rm -f -v app-postgres

    See the postgresdatabase API. PostgreSQL’s separate search adapter is configured in Search Drivers; constructing a database driver does not construct or migrate that adapter.

    MySQL

    Use MySQL when your application already operates a MySQL server. Provision a database and role, then supply the native Go driver’s DSN with appropriate TLS and permissions. The query compiler’s row-lock syntax targets MySQL 8; do not infer full MariaDB compatibility from a compatible wire protocol. Install:

    go get arkoren.dev/driver/mysql

    Connecting

    Import arkoren.dev/driver/mysql/mysqldatabase and arkoren.dev/core/service/database. With a configured native dsn:

    driver, err := mysqldatabase.New(dsn)
    db := database.New(driver)

    Handle construction errors before use. Blank or malformed DSNs and negative limits match mysqldatabase.ErrInvalidConfig. Parse messages omit supplied values; unwrapped native causes are not safe diagnostic output.

    Construction opens a lazy database/sql pool. driver.Start(ctx) or driver.Ping(ctx) checks connectivity before serving traffic. Use the shared typed operations after startup. Finish callbacks and cursors before driver.Stop(cleanupCtx), which ignores the context and closes the owned pool through database/sql.

    Connection Settings

    To keep this process within its connection budget:

    driver, err := mysqldatabase.New(dsn, func(connection *mysqldatabase.Builder) {
    	connection.MaxOpenConns(20)
    	connection.MaxIdleConns(5)
    })

    Positive settings replace native limits; zero leaves them unchanged. Without configuration, database/sql supplies the pool defaults, including an unlimited open-connection count. ConnMaxLifetime and ConnMaxIdleTime optionally limit reuse. Account for held locks and sessions as well as request concurrency.

    New adds parseTime=true unless the DSN’s parameter list explicitly sets it. This enables DATETIME scanning into time.Time; explicit parseTime=false remains false. MySQL has no timezone-aware datetime type: use a consistent UTC storage and connection policy.

    Native Pools

    Transfer a *sql.DB opened with the native MySQL driver through mysqldatabase.NewFrom(pool). The Arkoren driver owns it afterward. NewFrom applies positive builder policies but does not add parseTime=true; configure the original DSN if time scanning needs it. driver.SQL().Stats() exposes native utilization. Native SQL bypasses facade telemetry and context scopes.

    MySQL DDL can implicitly commit and invalidate savepoints. Schema changes and migration bookkeeping are not generally atomic; follow deployment and recovery rather than relying on a transaction around DDL. Named locks use GET_LOCK, span a server, and hash names longer than 64 bytes. They still obey the shared managed lifetime rules.

    MySQL upserts ignore the builder’s conflict target: any unique key can trigger an update. DoNothing uses INSERT IGNORE, which is not identical to PostgreSQL’s conflict-only behavior. Returning is unsupported even when a wire-compatible server offers a native returning feature. To read a generated row, select it by an application-owned unique key inside the same transaction.

    The MySQL ledger requirement applies to migration history, not your application table collation. The runner checks canonical binary storage and never repairs legacy history automatically.

    Running Locally

    With Docker installed and port 13306 free, start a disposable loopback-only server:

    docker run --name app-mysql -d -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_PASSWORD=local-root-only -e MYSQL_DATABASE=app -e MYSQL_USER=app -e MYSQL_PASSWORD=local-only mysql:8.4

    Repeat docker exec app-mysql mysqladmin ping -h 127.0.0.1 -uapp -plocal-only --silent until it succeeds, then connect with the application user:

    driver, err := mysqldatabase.New("app:local-only@tcp(127.0.0.1:13306)/app")

    Call driver.Start(ctx), wrap with database.New(driver), and read db.Find[int](ctx, query.Raw("SELECT 1")); success returns 1. This plaintext DSN and disposable password are not production settings. After stopping the application and driver, the following command deletes the container and anonymous data volume; use it only for this disposable data:

    docker rm -f -v app-mysql

    See the mysqldatabase API.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive