Declarative migrations for sqlc
sqlc is a tool that generates type-safe idiomatic Go code from SQL queries. It's like a transpiler, where you provide the required queries along with the database schema, and sqlc generates code that implements type-safe interfaces for these queries.
sqlc does not impose any requirements for handling migrations. While it has support for basic DDL commands, it's not designed for handling the migration process.
In this guide we will show you how Atlas can be used with sqlc in a declarative way, filling the gaps and providing a complete solution for building applications with sqlc.
For this guide we will assume that you already have a project using sqlc. If you are new to sqlc or don't have a project yet, check the getting started guide.
Schema
To generate code, sqlc depends on the database schema and the queries to be generated against it. We need both the schema and queries to ensure sqlc knows what types and queries it needs to generate.
As your application’s schema evolves, drift between the desired schema and database can cause many issues with sqlc, since the most common time to detect these errors is during query execution.
In this guide, we will show how sqlc users can utilize Atlas’s declarative schema migration workflow to ensure their database schemas are always in sync with their desired state.
Desired state
Since both Atlas and sqlc can accept a single SQL file to describe the desired state of the database schema, this file can be the source of truth (or desired state) for both tools, letting each tool handle its part of the job.
This is a great example of the power of the declarative concept, where we declare what we expect and let the tooling figure out how to reach the desired state.
Migrating the initial schema
If you followed the sqlc tutorial, you may end up with a schema.sql
file that looks like this:
CREATE TABLE authors
(
id BIGSERIAL PRIMARY KEY,
name text NOT NULL,
bio text
);
And a query.sql
file that looks like this:
-- 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;
You may already have executed the sqlc generate
command, if not, make sure to execute it.
The next step is migrating the database, for the purpose of this guide, we will assume that our application is backed by a PostgreSQL database running in a local Docker container. You can start such a container by running:
docker run --rm -d --name atlas-sqlc -p 5432:5432 -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=sqlc postgres
While in this example we are using Postgres, this is not a requirement by sqlc or Atlas. You can switch to any other
database as long the schema.sql
is compatible.
The first thing we do is initialize the database schema. This can be accomplished with a simple command:
atlas schema apply \
--url "postgres://postgres:pass@localhost:5432/sqlc?sslmode=disable" \
--dev-url "docker://postgres" \
--to "file://schema.sql"
After executing the command, you should see the planned changes, similar to the example below:
-- Planned Changes:
-- Add new schema named "public"
CREATE SCHEMA "public";
-- Create "authors" table
CREATE TABLE "public"."authors" ("id" bigserial NOT NULL, "name" text NOT NULL, "bio" text NULL, PRIMARY KEY ("id"));
Use the arrow keys to navigate: ↓ ↑ → ←
? Are you sure?:
▸ Apply
Abort
Atlas CLI will ask for approval during the command execution. If you want to skip this check, you can use the
flag --auto-approve
.
Let's break this command down: first we are telling Atlas to connect to the database using the url
flag, then compare
to the desired state defined by the to
flag and apply all the changes required to ensure we get to the desired state.
There is one more flag used on the command: dev-url
. Atlas uses a temporary database to check, simulate and validate
the generated queries. For more information about the dev database, read about it here. Atlas has
support for running database containers, which is what we are using in this example.
After running the command above and confirming the changes, your database should be in sync with the schema.sql
file.
Evolving the schema
As your application evolves, it is very common to have the database schema evolve as well. Suppose that to support a new
feature in your application, you need to add a new age
column. With Atlas, the process can be as simple as updating
the schema.sql
to the desired schema, updating the query.sql
file, executing the sqlc generate
command
and running schema apply
again.
First, let's update our schema.sql
file, adding the new column.
CREATE TABLE authors
(
id BIGSERIAL PRIMARY KEY,
name text NOT NULL,
bio text,
age integer
);
Let's add a new GetAuthorsByAge
query and update our CreateAuthor
query.
-- name: GetAuthor :one
SELECT *
FROM authors
WHERE id = $1
LIMIT 1;
-- name: GetAuthorsByAge :many
SELECT *
FROM authors
WHERE age = $1
ORDER BY age;
-- name: ListAuthors :many
SELECT *
FROM authors
ORDER BY name;
-- name: CreateAuthor :one
INSERT INTO authors (name, bio, age)
VALUES ($1, $2, $3)
RETURNING *;
-- name: DeleteAuthor :exec
DELETE
FROM authors
WHERE id = $1;
After changes to the schema.sql
or query.sql
file, we must run the sqlc generate command again:
sqlc generate
It's important to run sqlc generate
even for changes that don't change the query.sql
file, since sqlc
will replace the *
with explicit column names, to ensure the query never returns unexpected data.
As the last step, we run the atlas schema apply
command to ensure the database (whether production or development) is
in sync with the desired state.
atlas schema apply \
--url "postgres://postgres:pass@localhost:5432/sqlc?sslmode=disable" \
--dev-url "docker://postgres" \
--to "file://schema.sql"
This time Atlas should show a different plan for executing these changes:
-- Planned Changes:
-- Modify "authors" table
ALTER TABLE "public"."authors" ADD COLUMN "age" integer NULL;
Use the arrow keys to navigate: ↓ ↑ → ←
? Are you sure?:
▸ Apply
Abort
That’s it! After making changes to your schema, all you have to do is apply these changes again and Atlas will handle the rest.
Complete workflow
With the declarative strategy, one can visualize the complete workflow of using Atlas with sqlc as follows:
- Update the schema (
schema.sql
)- Optionally update the queries (
query.sql
)
- Optionally update the queries (
- Run sqlc to generate the updated code
- Run Atlas during the development and migration process, referencing the
schema.sql
as the desired state.
Wrapping up
In this guide we saw how Atlas can be used with sqlc in a declarative way, making the schema management process a breeze. If you don’t like the approach of handling the migration in a declarative way, Atlas has support for versioned migrations as well.