portablesql

Portable SQL toolkit for Go

Write your database code once. Run it on MySQL, MariaDB, PostgreSQL, CockroachDB and SQLite.

Why portablesql?

A small ORM and query builder that keeps your SQL portable across engines.

Multi-Database

MySQL and MariaDB, PostgreSQL and CockroachDB, SQLite. Placeholders, upserts, LIMIT/OFFSET, LIKE and date arithmetic are rendered per engine, so the same code runs on all of them.

Zero Bloat

Each driver is its own Go module that registers itself on import. Import psql-sqlite and you will not download pgx or the MySQL driver.

Struct Binding

Map structs to tables with sql tags. Tables are created and missing columns or indexes added on first use; opt out with WithSchemaCheck(false) and call CheckStructure yourself. Generic Fetch[T], Get[T] and Iter[T] return your own types.

Query Builder

Fluent SELECT, INSERT, UPDATE and DELETE with joins, subqueries, ON CONFLICT and Limit(offset, count). Render gives you the SQL for inspection, RenderArgs the parameterized query and its arguments.

Transactions & Relations

Tx with nested savepoints and EscapeTx for out-of-transaction writes. belongs_to, has_one, has_many and many_to_many with batch preloading, plus Lazy futures that collapse many lookups into one IN query.

And More

Lifecycle hooks, soft delete with Restore and ForceDelete, enums, portable IsDuplicate / IsNotExist error helpers, Go 1.23 iterators, and vector similarity search on PostgreSQL (pgvector) and CockroachDB.

Quick Start

Import a driver, connect with a DSN, and work with your own structs. Requires Go 1.24 or later (1.25 for psql-pgsql and psql-sqlite).

main.go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/portablesql/psql"
    _ "github.com/portablesql/psql-sqlite" // or psql-mysql / psql-pgsql
)

type User struct {
    psql.Name `sql:"users"`
    ID        uint64 `sql:",key=PRIMARY"`
    Username  string `sql:",type=VARCHAR,size=128"`
    Email     string `sql:",type=VARCHAR,size=255,key=UNIQUE:email"`
}

func main() {
    be, err := psql.New(":memory:") // engine detected from the DSN
    if err != nil {
        log.Fatal(err)
    }
    ctx := be.Plug(context.Background())

    // Insert: the table is created (or migrated) on first use
    err = psql.Insert(ctx, &User{ID: 1, Username: "alice", Email: "alice@example.com"})
    if psql.IsDuplicate(err) {
        log.Println("already registered")
    }

    // Load one record, change it, write back only the changed columns
    alice, err := psql.Get[User](ctx, map[string]any{"Email": "alice@example.com"})
    if err != nil {
        log.Fatal(err)
    }
    alice.Username = "alice.smith"
    err = psql.Update(ctx, alice)

    // Transactions (nested ones become savepoints)
    err = psql.Tx(ctx, func(ctx context.Context) error {
        return psql.Insert(ctx, &User{ID: 2, Username: "bob", Email: "bob@example.com"})
    })

    // Iterate over matches (Go 1.23 range-over-func)
    it, err := psql.Iter[User](ctx, map[string]any{"Username": &psql.Like{Like: "a%"}},
        psql.Sort(psql.S("Username", "ASC")))
    for u := range it {
        fmt.Println(u.Username)
    }

    // Query builder: renders ? or $1 placeholders depending on the engine
    query, args, err := psql.B().Select("Username").From("users").
        Where(psql.Gt(psql.F("ID"), 1)).Limit(0, 10).RenderArgs(ctx)
    fmt.Println(query, args, err)
}

On SQLite this prints SELECT "Username" FROM "users" WHERE ("ID">?) LIMIT 10 OFFSET 0 [1]; on PostgreSQL the placeholder becomes $1. Swap the DSN for user:pass@tcp(host:3306)/db or postgres://user:pass@host/db and import the matching driver to run the same program elsewhere. Read the guides for hooks, associations, scopes, soft delete, vectors and more.

Packages

Install the core plus the driver you need. Drivers register themselves on import and psql.New(dsn) picks the right one from the DSN.

psql core · v0.5.8

Struct binding, query builder, transactions, associations, hooks and the engine abstraction.

go get github.com/portablesql/psql
View on GitHub
psql-mysql driver · v0.5.0

MySQL and MariaDB via go-sql-driver/mysql (pure Go). DSN: user:pass@tcp(host:3306)/db.

go get github.com/portablesql/psql-mysql
View on GitHub
psql-pgsql driver · v0.5.0

PostgreSQL and CockroachDB via pgx (pgxpool). DSN: postgres://user:pass@host/db or host=... dbname=....

go get github.com/portablesql/psql-pgsql
View on GitHub
psql-sqlite driver · v0.5.0

SQLite via modernc.org/sqlite (pure Go, no CGO). DSN: :memory:, file:app.db or app.sqlite3.

go get github.com/portablesql/psql-sqlite
View on GitHub