MyGo
SQLite

Official plugins

SQLite

Local databases with parameterized queries and atomic transactions, without cgo.

The SQLite plugin provides local databases to web pages and Go, including native UI apps. It ships prebuilt SQLite libraries and loads them through purego, so apps build without cgo or a C compiler. It supports macOS, Linux and Windows on amd64 and arm64.

Using the page plugin

Call mygo.Use(sqlite.Plugin) only when the web frontend uses @mygo-plugins/sqlite. It registers the Go service called by that TypeScript client. Go and native UI apps use sqlite.Open directly, as shown below.

import (
    "github.com/egoist/mygo"
    "github.com/egoist/mygo/plugins/sqlite"
)

func main() {
    mygo.Use(sqlite.Plugin)
    // Create windows and call App.Run as usual.
}

Install the frontend package with bun add @mygo-plugins/sqlite, then:

import { open } from "@mygo-plugins/sqlite";

const db = await open("app.db");
await db.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, title TEXT)");
const result = await db.execute("INSERT INTO notes(title) VALUES (?)", ["Hello SQLite"]);
const { columns, rows } = await db.query("SELECT id, title FROM notes WHERE id = ?", [result.lastInsertId]);
// columns: ["id", "title"], rows: [[1, "Hello SQLite"]]
await db.close();

open takes a database file name, such as app.db, or :memory: for a private in-memory database. Files live in mygo.PathUserData. Absolute paths, directory traversal, symbolic-link database files and Windows device names are rejected. To choose the directory and lock wait:

mygo.Use(sqlite.New(sqlite.Options{
    Directory: "/path/to/databases",
    BusyTimeout: 2 * time.Second,
}))

Directory defaults to mygo.PathUserData; a missing directory is created for writable databases. BusyTimeout defaults to five seconds; a negative duration disables waiting. open("app.db", { readOnly: true }) opens an existing file without allowing writes.

Queries and values

execute(sql, params?) accepts exactly one statement, consumes any RETURNING rows, and returns { changes, lastInsertId }. changes counts directly affected rows; lastInsertId is SQLite's last inserted rowid on that connection, always a bigint in TypeScript.

query(sql, params?) returns { columns, rows }. Each row is an array of cells in column order, preserving duplicate column names. It supports SELECT, pragmas and statements with RETURNING. Results are buffered; use LIMIT and pagination for large tables.

Parameters bind by their SQLite index: ?, ?NNN, :name, $name and @name are accepted, with values supplied in an array. All parameter indexes must have values; missing or excess parameters fail. Never interpolate application values into SQL strings. Multiple statements are rejected before the first executes; a trailing semicolon and comments are allowed.

SQLite storage class TypeScript Go
NULL null nil
INTEGER number when safe, otherwise bigint int64
REAL number float64
TEXT string string
BLOB Uint8Array []byte

Boolean parameters become integers 0 or 1. Bind a bigint when an integer exceeds JavaScript's safe range; values outside SQLite's signed 64-bit range fail. Nonfinite numeric parameters fail. Empty BLOBs and empty text remain distinct from NULL. Text and BLOB parameters are copied by SQLite, and result BLOBs are copied before a statement closes.

Transactions and lifetime

await db.transaction([
  { sql: "INSERT INTO notes(title) VALUES (?)", params: ["first"] },
  { sql: "INSERT INTO notes(title) VALUES (?)", params: ["second"] },
]);

transaction returns one execution result per statement after COMMIT. It uses BEGIN IMMEDIATE, holds the connection for the entire batch, and rolls back on any error or cancellation. An empty batch returns []. Transaction-control statements (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE) are rejected in user SQL; use the batch API. ATTACH, DETACH and VACUUM INTO are rejected so SQL cannot open files outside the configured directory. Ordinary VACUUM is supported. SQLite extensions cannot be loaded.

Each connection belongs to its calling page. Other windows cannot use its handle. Page navigation and window close cancel calls and close the page's connections; app quit closes every remaining connection. close() is idempotent and cancels active operations. Concurrent operations on one connection serialize; await dependent calls to give them an order.

Cancellation is handled by a progress handler and busy handler in C, with an atomic cancellation token. SQLite retains no Go pointers or purego callbacks. Go callers can cancel each operation with its context.

From Go or native UI

No mygo.Use(sqlite.Plugin) registration is needed. Open a connection directly and close it when finished:

ctx := context.Background()
db, err := sqlite.Open(ctx, "app.db", sqlite.OpenOptions{})
if err != nil { return err }
defer db.Close()

_, err = db.Execute(ctx, "CREATE TABLE IF NOT EXISTS notes (title TEXT)")
if err != nil { return err }
_, err = db.Transaction(ctx, []sqlite.Statement{
    {SQL: "INSERT INTO notes VALUES (?)", Args: []any{"first"}},
    {SQL: "INSERT INTO notes VALUES (?)", Args: []any{"second"}},
})
if err != nil { return err }
rows, err := db.Query(ctx, "SELECT title FROM notes")
// rows.Columns and rows.Values contain copied results.

Go's Open accepts a path directly; its parent directory must exist. Connections are safe from any goroutine. Their lifetime belongs to the Go caller, who must call Close. sqlite.Error exposes the primary and extended SQLite error codes; closed connections return sqlite.ErrClosed, and canceled operations return their context error.