rasql (pronounced “rascal”) is an all-in-one SQL toolkit for Go.
It gives an application one model for schema definitions, dynamic queries, static queries, result decoding, and database inspection. Every statement it produces is parameterized: values travel as bound arguments, never as SQL text.
- PostgreSQL, MySQL, and SQLite dialects.
- Schema definitions written as Go code, including generation from live database metadata.
- Type-safe result-set access.
- Dynamic query building at runtime.
- Static query building with templates.
rasql applies ordered, forward-only DDL migrations for PostgreSQL, MySQL, and SQLite. Run checked-in SQL migration directories with rasqlmigrate, and generate reviewed PostgreSQL, MySQL, or SQLite migrations from desired-schema sources when useful. It also describes and inspects schemas for use in application code and migration planning.
rasql requires Go 1.26 or newer. It builds on database/sql, so an application imports the driver it wants where it opens the connection.
go get github.com/lestrrat-go/rasqlPoint rasqlgen at the database and name the tables to generate. Nothing needs installing first, and the command carries its own PostgreSQL driver:
mkdir -p internal/store
go get github.com/lestrrat-go/rasql/cmd/rasqlgen@latest
go run github.com/lestrrat-go/rasql/cmd/rasqlgen schema \
-dsn "$DATABASE_URL" \
-table users \
-package store \
-output internal/storeFor each table it writes a <table>_gen.go file with a row type, column-mapping methods, a table type, and an accessor:
// Code generated by rasqlgen; DO NOT EDIT.
package store
type UsersRow struct {
ID int64
Email string
}
// DecodeRow assigns each result column to its field.
func (r *UsersRow) DecodeRow(src row.Dynamic) error {
if err := row.Assign(src, "id", &r.ID); err != nil {
return err
}
return row.Assign(src, "email", &r.Email)
}
// ColumnValue returns the value of the named column. ColumnValue and DecodeRow
// state the mapping a hand-written row type states with `rasql` struct tags.
func (r UsersRow) ColumnValue(name string) (any, bool) {
switch name {
case "id":
return r.ID, true
case "email":
return r.Email, true
}
return nil, false
}
// UsersTable is the generated table type for the "users" table.
type UsersTable struct {
rasql.Table[UsersRow]
ID query.Column
Email query.Column
}
var usersTable = newUsersTable(rasql.MustTable[UsersRow](schema.Table{ /* … */ }))
// Users returns the descriptor for the "users" table.
func Users() UsersTable {
return usersTable
}The descriptor stays unexported so no importer can replace it; store.Users() hands out a copy. Its column fields are what the query builders take, so WhereEqual(users.ID, 42) builds while a misspelled users.Emial does not compile. See what the column fields catch and the two mapping methods.
Writing the same descriptors by hand works too, and generating from a checked-in JSON snapshot keeps builds offline. See Schemas and rasqlgen.
Pair the database handle with a dialect, then use the generated descriptor for every statement. This example writes a row, reads it back, updates it, and deletes it, all against SQLite.
package examples_test
import (
"context"
"database/sql"
"fmt"
"github.com/lestrrat-go/rasql"
"github.com/lestrrat-go/rasql/dialect"
_ "modernc.org/sqlite" // Registers the database/sql "sqlite" driver for this example.
)
func Example_rasql_quickstart() {
// This example runs one insert, read, update, and delete against a table
// descriptor. users and UserRow are declared in query_example_tables_test.go
// with the shape rasqlgen emits; an application that generated into package
// store would write store.Users() and store.UsersRow instead.
ctx := context.Background()
database, err := sql.Open("sqlite", ":memory:")
if err != nil {
fmt.Printf("failed to open SQLite database: %s\n", err)
return
}
defer func() { _ = database.Close() }()
// An in-memory SQLite database is per connection, so keep this example on one.
database.SetMaxOpenConns(1)
// A Client couples a database handle with the dialect used to render SQL.
client, err := rasql.New(database, dialect.SQLite())
if err != nil {
fmt.Printf("failed to create rasql client: %s\n", err)
return
}
// A real application creates its tables through migrations instead.
if err := rasql.Create(ctx, client, users); err != nil {
fmt.Printf("failed to create users table: %s\n", err)
return
}
// Insert writes the tagged fields of UserRow as bound values.
if _, err := rasql.Insert(ctx, client, users, UserRow{ID: 1, Email: "ada@example.com"}); err != nil {
fmt.Printf("failed to insert user: %s\n", err)
return
}
// One returns a single decoded row and fails when the result holds any other count.
user, err := rasql.SelectFrom(users).WhereEqual(users.ID, 1).One(ctx, client)
if err != nil {
fmt.Printf("failed to query user: %s\n", err)
return
}
fmt.Println(user.Email)
// Update matches the row's primary key and writes its remaining fields.
if _, err := rasql.Update(ctx, client, users, UserRow{ID: 1, Email: "grace@example.com"}); err != nil {
fmt.Printf("failed to update user: %s\n", err)
return
}
// All collects every result row instead of ranging over them.
found, err := rasql.SelectFrom(users).OrderAsc(users.ID).All(ctx, client)
if err != nil {
fmt.Printf("failed to query users: %s\n", err)
return
}
fmt.Println(len(found), found[0].Email)
// DeleteFrom builds the predicate from generated columns, like the select builder.
result, err := rasql.DeleteFrom(users).WhereEqual(users.ID, 1).Exec(ctx, client)
if err != nil {
fmt.Printf("failed to delete user: %s\n", err)
return
}
deleted, err := result.RowsAffected()
if err != nil {
fmt.Printf("failed to count deleted users: %s\n", err)
return
}
fmt.Printf("%d user deleted\n", deleted)
// Output:
// ada@example.com
// 1 grace@example.com
// 1 user deleted
}source: examples/rasql_quickstart_example_test.go
users and UserRow stand in for the generated store.Users() and store.UsersRow, so an application would write rasql.SelectFrom(store.Users()) instead. Swap dialect.SQLite() for dialect.PostgreSQL() or dialect.MySQL() to run the same code against another database; only the driver and the DSN change with it.
Inserts, updates, deletes, and typed selects have dedicated helpers. Fluent deletes can read deleted rows through Returning, Query, QueryDeleteAll, or QueryDeleteOne. Upserts and anything else beyond them are built through the query package and run with rasql.Exec, except a statement with a RETURNING clause, which reads its rows back through rasql.QueryWrite instead.
The two SQLite-only lines are the :memory: DSN and SetMaxOpenConns(1), since an in-memory database belongs to one connection. A real application also creates its tables through migrations rather than rasql.Create.
The Taskboard sample is a standalone HTTP application whose checked-in SQLite SQL migrations run with rasqlmigrate before startup. Its Taskboard page shows typed descriptors, inserts, an update, and a joined query in one small application.
cd sample/taskboard
go run ./cmd/taskboardOpen http://127.0.0.1:8080/ in another terminal.
| Page | Covers |
|---|---|
| Getting started | Installing, creating a client, and running a first query. |
| Schemas | Describing tables in Go and reading them back from a live database. |
| Querying | Typed selects, joins, custom projections, and a reference table for every operation and predicate. |
| Writing rows | Creating tables and inserting, updating, or deleting rows. |
| Static templates | Compiling SQL text with named binds into parameterized statements. |
rasqlgen |
Generating Go source from a database, a schema snapshot, or a template. |
| Migrations | Applying ordered forward-only DDL migrations. |
The API reference lives at pkg.go.dev. Each code block that links to a source file is a runnable Go example from examples/, verified by go test.
Most applications only import the root rasql package plus dialect and schema. The rest are building blocks the root package uses on their behalf.
| Package | Responsibility |
|---|---|
rasql |
Executes statements, decodes typed rows, and provides the fluent API. |
schema |
Describes tables, columns, indexes, constraints, and logical types. |
dialect |
Decides identifier quoting, placeholders, type mapping, and syntax support. |
query |
Represents dialect-neutral statements and expressions, with validation. |
render |
Turns a validated query into SQL text and an ordered argument list. |
row |
Provides typed column access and result decoding. |
inspect |
Reads live database metadata into schema descriptors. |
migrate |
Plans and executes forward-only DDL migrations with durable history. |
template, generate, cmd/rasqlgen |
Compile templates and descriptors into deterministic Go source. |
See DESIGN.md for the architecture and the reasoning behind these boundaries.
See CONTRIBUTING.md for the local development workflow, including running the live-database tests.