Portable SQL toolkit for Go
Write your database code once. Run it on MySQL, MariaDB, PostgreSQL, CockroachDB and SQLite.
A small ORM and query builder that keeps your SQL portable across engines.
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.
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.
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.
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.
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.
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.
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).
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.
Install the core plus the driver you need. Drivers register themselves on import and psql.New(dsn) picks the right one from the DSN.
Struct binding, query builder, transactions, associations, hooks and the engine abstraction.
go get github.com/portablesql/psqlMySQL and MariaDB via go-sql-driver/mysql (pure Go). DSN: user:pass@tcp(host:3306)/db.
go get github.com/portablesql/psql-mysqlPostgreSQL and CockroachDB via pgx (pgxpool). DSN: postgres://user:pass@host/db or host=... dbname=....
go get github.com/portablesql/psql-pgsqlSQLite via modernc.org/sqlite (pure Go, no CGO). DSN: :memory:, file:app.db or app.sqlite3.
go get github.com/portablesql/psql-sqlite