Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

177 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rasql

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.

Requirements

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.

Install

go get github.com/lestrrat-go/rasql

Quick start

1. Generate descriptors for your tables

Point 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/store

For 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.

2. Read and write rows

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.

Sample application

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/taskboard

Open http://127.0.0.1:8080/ in another terminal.

Documentation

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.

How the packages fit together

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.

Contributing

See CONTRIBUTING.md for the local development workflow, including running the live-database tests.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages