Skip to content

bnema/purego-sqlite

Repository files navigation

purego-sqlite

Go bindings for SQLite via purego instead of cgo.

Linux only. CGO_ENABLED=0.

Warning

This is early stage. The full SQLite C API (~350 functions) is bound, but the database/sql driver covers the common path only. API may change.

Architecture

The binding layer is generated from sqlite3.h. The generator parses the system header and produces:

  • purego function bindings for all ~350 non-variadic SQLite functions
  • Typed SyscallN wrappers for variadic functions at known arities
  • Outbound port interfaces grouped by API surface (Lifecycle, Prepare, Column, Bind, etc.)
  • A composite CAPI interface embedding all port groups
  • Inbound port interfaces (DB, Stmt, Rows, Result) for the public API
  • Public API composition root wiring everything together

A hand-written core domain (~500 LOC) implements the business logic: open, prepare, bind, step, scan, close. A thin database/sql/driver adapter sits on top.

Mocks for all interfaces are generated by Mockery v3 and shipped in sqlite/mocks/.

Usage

database/sql driver

import (
    "database/sql"
    _ "github.com/bnema/purego-sqlite/driver"
)

db, err := sql.Open("sqlite3", "./my.db")

QueryContext, ExecContext, PrepareContext, and BeginTx use direct SQLite paths. Cancelling a context interrupts only its active operation; the connection remains safe to reuse after rows are closed or exhausted.

Direct API

import "github.com/bnema/purego-sqlite/sqlite"

db, err := sqlite.Open("./my.db")
defer db.Close()

stmt, _ := db.Prepare("SELECT name FROM users WHERE id = ?")
rows, _ := stmt.Query(42)

Testing with mocks

The package ships pre-generated Mockery v3 mocks for all public interfaces. Your code accepts sqlite.DB (an interface), and in tests you swap in a mock — no libsqlite3.so needed.

Step 1: Accept the interface in your code

package userrepo

import "github.com/bnema/purego-sqlite/sqlite"

type UserRepo struct {
    db sqlite.DB
}

func New(db sqlite.DB) *UserRepo {
    return &UserRepo{db: db}
}

func (r *UserRepo) GetName(id int) (string, error) {
    rows, err := r.db.Query("SELECT name FROM users WHERE id = ?", id)
    if err != nil {
        return "", err
    }
    defer rows.Close()
    if !rows.Next() {
        return "", fmt.Errorf("user %d not found", id)
    }
    var name string
    if err := rows.Scan(&name); err != nil {
        return "", err
    }
    return name, nil
}

Step 2: Mock it in tests

package userrepo_test

import (
    "testing"

    "github.com/bnema/purego-sqlite/sqlite/mocks"
    "github.com/stretchr/testify/require"
)

func TestGetName(t *testing.T) {
    // Create mocks — expectations are auto-asserted on cleanup
    mockDB := mocks.NewMockDB(t)
    mockRows := mocks.NewMockRows(t)

    // Set up the call chain
    mockDB.EXPECT().
        Query("SELECT name FROM users WHERE id = ?", 42).
        Return(mockRows, nil)
    mockRows.EXPECT().Next().Return(true)
    mockRows.EXPECT().Scan().RunAndReturn(func(dest ...any) error {
        *(dest[0].(*string)) = "Alice"
        return nil
    })
    mockRows.EXPECT().Close().Return(nil)

    // Test
    repo := userrepo.New(mockDB)
    name, err := repo.GetName(42)
    require.NoError(t, err)
    require.Equal(t, "Alice", name)
}

Available mocks in github.com/bnema/purego-sqlite/sqlite/mocks:

Mock Interface Key methods
MockDB sqlite.DB Prepare, Exec, Query, Interrupt, Close
MockStmt sqlite.Stmt Exec, Query, NumInput, Close
MockRows sqlite.Rows Next, Scan, Columns, Err, Close
MockResult sqlite.Result LastInsertId, RowsAffected

All mocks support .EXPECT() for type-safe expectations and .On()/.Return() for classic testify style.

Note: The driver registers as sqlite3, the same name as mattn/go-sqlite3. Do not import both in the same binary.

Requirements

  • Go 1.23+
  • SQLite runtime library: libsqlite3.so or the common versioned SONAME libsqlite3.so.0
  • Optional strict override path: SQLITE_LIB_PATH=/custom/path/libsqlite3.so (when set, no default names are tried)

Building

CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 go test ./...

Integration tests need libsqlite3.so:

go test -tags integration ./integration/

Row decoding

The database/sql driver returns time.Time values only for result columns whose SQLite declared type is DATE, DATETIME, or TIMESTAMP. Date-shaped values in ordinary TEXT columns remain strings. Expressions and columns without a declared type likewise remain their SQLite storage type.

Regenerating bindings

go generate ./...

Or manually:

go run ./cmd/sqlitegen --header /usr/include/sqlite3.h --output-dir .
mockery

Project layout

sqlite/              public API (generated) + mocks
driver/              database/sql adapter (hand-written)
internal/
  ports/in/          inbound port interfaces (generated)
  ports/out/         outbound port interfaces (generated) + mocks
  core/              domain logic: open, prepare, bind, step, scan (hand-written)
  capi/              purego bindings + bridge (generated + hand-written)
  loader/            dlopen libsqlite3.so (hand-written)
cmd/sqlitegen/       binding generator (parser, model, emitter, templates)
integration/         integration tests (require libsqlite3.so)

License

MIT

About

Go bindings for SQLite via purego instead of cgo.

Topics

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors