Introducing sqlc - Compile SQL queries to type-safe Go
Ask any Go developer and they will tell you how painful it is to write programs that talk to SQL databases. Go feels like it isn't even half as productive compared to working with toolkits like SQLAlchemy, Diesel, Hibernate or ActiveRecord. The existing tools in the Go ecosystem force application developers to hand-write mapping functions or litter their code with unsafe empty interfaces.
Introducing sqlc
I've been feeling this pain for years, so six months ago I started developing a solution. Today, I'm excited to announce the result of that work. sqlc is a new tool that makes working with SQL in Go a joy.
It dramatically improves the developer experience of working with relational databases without sacrificing type-safety or runtime performance. It does not use struct tags, hand-written mapper functions, unnecessary reflection or add any new dependencies to your code. In fact, it even provides correctness and safety guarantees that no other SQL toolkit in the Go ecosystem can match.
sqlc accomplishes all of this by taking a fundamentally different approach: compiling SQL into fully type-safe, idiomatic Go code. sqlc can take this SQL:
CREATE TABLE authors ( id BIGSERIAL PRIMARY KEY, name text NOT NULL, bio text ); -- name: GetAuthor :one SELECT * FROM authors WHERE id = $1 LIMIT 1;
and generate the following Go code automatically.
package db import ( "context" "database/sql" ) type Author struct { ID int64 Name string Bio sql.NullString } const getAuthor = `-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1 ` type Queries struct { db *sql.DB } func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { row := q.db.QueryRowContext(ctx, getAuthor, id) var i Author err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err }
For years, software engineers have been generating SQL queries from annotated objects in programming languages. SQL is already a structured, typed language; we should be generating correct, type-safe code in every programming language from the source of truth: SQL itself.
Problems using SQL from Go, today
Working with relational databases in any programming language is challenging. Go in particular is especially painful; even with the existing packages and tools, writing and maintaining queries in a Go application is a chore.
Low-level standard library
Go’s standard library offers the database/sql
package for interacting
with relational databases, but most applications outgrow the database/sql
package and reach for a full-fledged ORM or a higher-level library abstraction.
Why?
Using the database/sql
package is straightforward. Write a query, pass in
the necessary arguments, and scan the results back into fields. Programmers are
responsible for explicitly specifying the mapping between a SQL field and its
value in the program for both inputs and outputs.
Once an application has more than a few queries, maintaining these mappings is cumbersome and severely impacts programmer productivity. Worse, it's trivial to make mistakes that are not caught until runtime. If you switch the order of parameters in your query, the parameter mapping must be updated. If a column is added to a table, all queries must be updated to return that value. If the type in SQL does not match the type in your code, failures will not occur until query execution time.
Higher-level libraries
The Go community has produced higher-level libraries (github.com/jmoiron/sqlx) and ORMs (github.com/jinzhu/gorm) to solve these issues. However, higher-level libraries still require manual mapping via query text and struct tags that, if incorrect, will only fail at runtime.
ORMs do away with much of the manual mapping but require you to write your queries now in a pseudo-sql DSL that basically reinvents SQL in a set of Go function calls.
Invalid SQL
With either approach, it is still trivial to make errors that the compiler can't check. As a Go programmer, have you ever:
- Mixed up the order of the arguments when invoking the query so they didn't match up with the SQL text
- Updated the name of a column in one query both not another
- Mistyped the name of a column in a query
- Changed the number of arguments in a query but forgot to pass the additional values
- Changed the type of a column but forgot to change the type in your code?
All of these errors are impossible with sqlc. Wait, what? How?
How to use sqlc in 3 steps
- You write SQL queries
- You run
sqlc
to generate Go code that presents type-safe interfaces to those queries - You write application code that calls the methods
sqlc
generates
Seriously, it's that easy. You don't have to write any boilerplate SQL querying code ever again. sqlc generates fully-type-safe idiomatic Go code from your queries. sqlc also prevents entire classes of common errors in SQL code.
During code generation, sqlc parses all of your queries and DDL statements
(e.g. CREATE TABLE
) so that it knows the names and types of every column in
your tables and every expression in your queries. If any of them do not match,
sqlc will fail to compile your queries, catching would-be runtime errors
before they happen.
Likewise, the methods that sqlc generates for you have a strict arity and correct Go type definitions that match your columns. So if you change a query's arguments or a column's type but don't update your code, it will fail to compile.
A guided tour of sqlc
That all sounds great, but what does it look like in practice?
Download and install
To start, download the latest version of sqlc and add it to your
$PATH
. You’ll also want to have Go 1.13 installed on your system. Create a
new directory for the example project.
$ mkdir sqlc-tour
$ cd sqlc-tour
$ go mod init github.com/kyleconroy/sqlc-tour
Configuration file
sqlc uses a configuration file
at the root of your project to store settings. Create sqlc.json
with the
following contents:
{ "version": "1", "packages": [{ "schema": "schema.sql", "queries": "query.sql", "name": "main", "path": "." }] }
sqlc will generate a Go package for each entry in the packages list. Each entry has four required properties:
schema
- Path to a SQL file that defines database tables (can also be a directory of SQL files)
queries
- Path to a SQL file with application queries (can also be a directory of SQL files)
name
- The package name to use for the generated code. Defaults to
path
basename
- The package name to use for the generated code. Defaults to
path
- Output directory for generated code
Write DDL
Let’s build an application for tracking authors. It’s a small application with
only a single table. Define the authors
table in schema.sql
.
-- schema.sql CREATE TABLE authors ( id BIGSERIAL PRIMARY KEY, name text NOT NULL, bio text );
Write queries
Your application needs a few queries to create, insert, update and delete
author
records. Queries are annotated with a small comment that includes a Go
method name and the database/sql
function to use.
-- query.sql -- name: GetAuthor :one SELECT * FROM authors WHERE id = $1 LIMIT 1; -- name: ListAuthors :many SELECT * FROM authors ORDER BY name; -- name: CreateAuthor :one INSERT INTO authors ( name, bio ) VALUES ( $1, $2 ) RETURNING *; -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $1;
Generate code
With just the information stored in these two SQL files, sqlc can now generate
database access methods for your application. Make sure that you’re in the
sqlc-tour
directory and run the following command:
$ sqlc generate
This will generate three files: db.go
, models.go
, and query.sql.go
.
db.go
defines a shared interface for using a *sql.DB
or *sql.Tx
to
execute queries, as well as the Queries
struct which contains the database
access methods.
db.go
// db.go package main import ( "context" "database/sql" ) type dbtx interface { ExecContext(context.Context, string, ...interface{}) (sql.Result, error) PrepareContext(context.Context, string) (*sql.Stmt, error) QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) QueryRowContext(context.Context, string, ...interface{}) *sql.Row } func New(db dbtx) *Queries { return &Queries{db: db} } type Queries struct { db dbtx } func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ db: tx, } }
models.go
models.go
contains the structs associated with the database tables. In this
example it contains a single struct, Author
.
// models.go package main import "database/sql" type Author struct { ID int64 Name string Bio sql.NullString }
query.sql.go
query.sql.go
contains the data access methods we defined in query.sql
. It’s
a bit verbose, but this is code you would have had to write yourself!
// query.sql.go package main import ( "context" "database/sql" ) const createAuthor = `-- name: CreateAuthor :one INSERT INTO authors ( name, bio ) VALUES ( $1, $2 ) RETURNING id, name, bio ` type CreateAuthorParams struct { Name string Bio sql.NullString } func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) var i Author err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err } const deleteAuthor = `-- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $1 ` func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { _, err := q.db.ExecContext(ctx, deleteAuthor, id) return err } const getAuthor = `-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1 ` func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { row := q.db.QueryRowContext(ctx, getAuthor, id) var i Author err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err } const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name ` func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { rows, err := q.db.QueryContext(ctx, listAuthors) if err != nil { return nil, err } defer rows.Close() var items []Author for rows.Next() { var i Author if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil }
Star expansion
Were you surprised to see SELECT *
/ RETURNING *
in query.sql
? sqlc
replaces *
references with the correct columns when generating code. Take a
second look at the createAuthor
, listAuthor
and getAuthor
SQL queries in
the example above.
Write your application code
It’s now easy to create, delete and fetch author
records. Paste the following
into main.go
. It should build (go build
) without any errors.
package main import ( "context" "database/sql" "fmt" ) func run(ctx context.Context, db *sql.DB) error { q := &Queries{db: db} insertedAuthor, err := q.CreateAuthor(ctx, CreateAuthorParams{ Name: "Brian Kernighan", Bio: sql.NullString{ String: "Co-author of The C Programming Language", Valid: true, }, }) if err != nil { return err } authors, err := q.ListAuthors(ctx) if err != nil { return err } fmt.Println(authors) err = q.DeleteAuthor(ctx, insertedAuthor.ID) if err != nil { return err } return nil } func main() { // TODO: Open a connection to your PostgreSQL database run(context.Background(), nil) }
All the above code can be found on in the sqlc-tour repository. A larger, more complicated example application can be found in the ondeck package in the sqlc repository.
Packed with power
Don’t let the previous example’s simplicity fool you. sqlc has support for complex queries and advanced usage patterns:
- Transactions and prepared statements
- Aggregates, case statements, and common table expressions
RETURNING
values from INSERT, UPDATE, and DELETE statements- PostgreSQL types like arrays, enums, timestamps, and UUIDs
- Go type overrides for individual columns or PostgreSQL values
- Generated structs with JSON tags
How it works
You might be wondering how this all works. It’s not magic, but it’s close: sqlc parses queries using the same parser as your PostgreSQL database.
A first pass uses DDL statements to build an in-memory representation of your database. Next, sqlc parses each query and uses the in-memory representation to determine input parameters and output columns.
This is only possible thanks to the amazing work by Lukas Fittl on pg_query_go. If you need help diagnosing PostgreSQL performance issues, his service pganalyze may be exactly what you need.
What’s next
While it’s still early days, sqlc is ready for production. It’s used for all database access in my own projects:
- https://equinox.io - Go application packaging & distribution
- https://upcoming.fm - Spotify playlists for your favorite music venues
- https://chaincontrol.org - SMS notifications for winter road closures in Tahoe
More importantly, it’s seen adoption in larger companies. Both ngrok and Weave use sqlc to power portions of their stack.
sqlc is a young project in active development. It currently only supports
PostgreSQL and Go. However, it's designed to support additional language
backends in the future. If you'd like sqlc support for your language of choice,
create an issue or send me an email at kyle@conroy.org
.
Lastly, I like to thank the authors of hugsql
,
pugsql
, and
protoc
, which served as
inspiration for sqlc
. Without these tools, sqlc would not exist.