0%
October 28, 2023

Goose and Sqlc for Database Migration and Query Function Generation

go

postgresql

sql

Installations

  • go install github.com/pressly/goose/v3/cmd/goose@latest
    • For mac and linux users, run
      go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
      and from now on sqlc generate will be available in your shell.
    • For windows user, the sqlc package above wouldn't work, instead we run
      docker run --rm -v "%cd%:/src" -w /src sqlc/sqlc generate
      in cmd.

Workflow for Database Migrations in Postgresql

In case the reader use mySQL, the only difference will be the sqlc.yml.

sql/scehma
sql/schema/001_users.sql
-- +goose Up

CREATE TABLE users (
    id UUID PRIMARY KEY,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL,
    name TEXT NOT NULL
);

-- +goose Down
DROP TABLE users;
sql/schema/002_users_apikey.sql
-- +goose Up
ALTER TABLE users ADD COLUMN api_key VARCHAR(64) UNIQUE NOT NULL DEFAULT (
  encode(sha256(random()::text::bytea), 'hex')
);

-- +goose Down
ALTER TABLE users DROP COLUMN api_key;
sql/schema/003_feeds.sql
-- +goose Up

CREATE TABLE feeds (
    id UUID PRIMARY KEY,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL,
    name TEXT NOT NULL,
    url TEXT UNIQUE NOT NULL,
    user_id UUID REFERENCES users(id) ON DELETE CASCADE
);

-- +goose Down
DROP TABLE users;
sql/schema/004_feeds_uuid_nonull.sql
-- +goose Up
ALTER TABLE feeds ALTER COLUMN user_id SET NOT NULL;

-- +goose Down
ALTER TABLE feeds ALTER COLUMN user_id DROP NOT NULL;
Apply Changes in DB Migrations by Goose
  • After each migration is inserted, we run sh db_migrate_up.sh, where

    # db_migrate_up.sh
    DB_URL=postgresql://pguser:pguser@127.0.0.1:5432/rssagg
    
    cd sql/schema
    goose postgres $DB_URL up
    read -p "Press any key to leave ..."
Create sqlc.yaml (Only do it Once)
  • Create a sqlc.yaml at the root project level:

    version: "2"
    sql:
      - schema: "sql/schema"
        queries: "sql/queries"
        engine: "postgresql"
        gen:
          go:
            out: "internal/database"
  • For mySQL the yaml fiile is more or less the same, which can be copied form documentation.

sql/queries
sql/queries/users.sql
-- name: CreateUser :one
INSERT INTO users(id, created_at, updated_at, name, api_key)
VALUES ($1, $2, $3, $4,
  encode(sha256(random()::text::bytea), 'hex')
)
RETURNING *;

-- name: GetUserByAPIKey :one
SELECT * FROM users WHERE api_key = $1;
sql/quries/feeds.sql
-- name: CreateFeed :one
INSERT INTO feeds (id, created_at, updated_at, name, url, user_id)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *;
Execute sqlc generate
  • After table migration is done, we create correponding schema as struct, queries as functions in go:
    sqlc generate
    or in windows cmd prompt (or create a .bat file):
    docker run --rm -v "%cd%:/src" -w /src sqlc/sqlc generate
    pause

Outputs from Sqlc Generate

internal/database/db.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.22.0

package database

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,
	}
}
internal/database/models.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.22.0

package database

import (
	"time"

	"github.com/google/uuid"
)

type Feed struct {
	ID        uuid.UUID
	CreatedAt time.Time
	UpdatedAt time.Time
	Name      string
	Url       string
	UserID    uuid.UUID
}

type User struct {
	ID        uuid.UUID
	CreatedAt time.Time
	UpdatedAt time.Time
	Name      string
	ApiKey    string
}
internal/database/users.sql.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.22.0
// source: users.sql

package database

import (
	"context"
	"time"

	"github.com/google/uuid"
)

const createUser = `-- name: CreateUser :one
INSERT INTO users(id, created_at, updated_at, name, api_key)
VALUES ($1, $2, $3, $4,
  encode(sha256(random()::text::bytea), 'hex')
)
RETURNING id, created_at, updated_at, name, api_key
`

type CreateUserParams struct {
	ID        uuid.UUID
	CreatedAt time.Time
	UpdatedAt time.Time
	Name      string
}

func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
	row := q.db.QueryRowContext(ctx, createUser,
		arg.ID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.Name,
	)
	var i User
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.ApiKey,
	)
	return i, err
}

const getUserByAPIKey = `-- name: GetUserByAPIKey :one
SELECT id, created_at, updated_at, name, api_key FROM users WHERE api_key = $1
`

func (q *Queries) GetUserByAPIKey(ctx context.Context, apiKey string) (User, error) {
	row := q.db.QueryRowContext(ctx, getUserByAPIKey, apiKey)
	var i User
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.ApiKey,
	)
	return i, err
}
internal/database/feeds.sql.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.22.0
// source: feeds.sql

package database

import (
	"context"
	"time"

	"github.com/google/uuid"
)

const createFeed = `-- name: CreateFeed :one
INSERT INTO feeds (id, created_at, updated_at, name, url, user_id)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, created_at, updated_at, name, url, user_id
`

type CreateFeedParams struct {
	ID        uuid.UUID
	CreatedAt time.Time
	UpdatedAt time.Time
	Name      string
	Url       string
	UserID    uuid.UUID
}

func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) {
	row := q.db.QueryRowContext(ctx, createFeed,
		arg.ID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.Name,
		arg.Url,
		arg.UserID,
	)
	var i Feed
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Url,
		&i.UserID,
	)
	return i, err
}

Reference