Skip to content

chore: Initial database scaffolding #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
###############################################################################
# COPY PASTA OF .gitignore
###############################################################################
node_modules
node_modules
vendor
4 changes: 1 addition & 3 deletions .github/workflows/coder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ jobs:
with:
go-version: "^1.17"

# Check that go is available
# TODO: Implement actual test run
- run: go version
- run: go test -v ./...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, our CI is actually doing something now 🎉


test-js:
name: "test/js"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
###############################################################################

node_modules
vendor
.eslintcache
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
# https://github.com/prettier/prettier/issues/8506
# https://github.com/prettier/prettier/issues/8679
###############################################################################
node_modules
node_modules
vendor
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Runs migrations to output a dump of the database.
database/dump.sql: $(wildcard database/migrations/*.sql)
go run database/dump/main.go

# Generates Go code for querying the database.
.PHONY: database/generate
database/generate: database/dump.sql database/query.sql
cd database && sqlc generate && rm db_tmp.go
cd database && gofmt -w -r 'Querier -> querier' *.go
cd database && gofmt -w -r 'Queries -> sqlQuerier' *.go

fmt/prettier:
@echo "--- prettier"
# Avoid writing files in CI to reduce file write activity
Expand All @@ -9,4 +20,4 @@ endif
.PHONY: fmt/prettier

fmt: fmt/prettier
.PHONY: fmt
.PHONY: fmt
75 changes: 75 additions & 0 deletions database/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Package database connects to external services for stateful storage.
//
// Query functions are generated using sqlc.
//
// To modify the database schema:
// 1. Add a new migration using "create_migration.sh" in database/migrations/
// 2. Run "make database/generate" in the root to generate models.
// 3. Add/Edit queries in "query.sql" and run "make database/generate" to create Go code.
package database

import (
"context"
"database/sql"
"errors"

"golang.org/x/xerrors"
)

// Store contains all queryable database functions.
// It extends the generated interface to add transaction support.
type Store interface {
querier

InTx(context.Context, func(Store) error) error
}

// DBTX represents a database connection or transaction.
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
}

// New creates a new database store using a SQL database connection.
func New(sdb *sql.DB) Store {
return &sqlQuerier{
db: sdb,
sdb: sdb,
}
}

type sqlQuerier struct {
sdb *sql.DB
db DBTX
}

// InTx performs database operations inside a transaction.
func (q *sqlQuerier) InTx(ctx context.Context, fn func(Store) error) error {
if q.sdb == nil {
return nil
}
tx, err := q.sdb.Begin()
if err != nil {
return xerrors.Errorf("begin transaction: %w", err)
}
defer func() {
rerr := tx.Rollback()
if rerr == nil || errors.Is(rerr, sql.ErrTxDone) {
// no need to do anything, tx committed successfully
return
}
// couldn't roll back for some reason, extend returned error
err = xerrors.Errorf("defer (%s): %w", rerr.Error(), err)
}()
err = fn(&sqlQuerier{db: tx})
if err != nil {
return xerrors.Errorf("execute transaction: %w", err)
}
err = tx.Commit()
if err != nil {
return xerrors.Errorf("commit transaction: %w", err)
}
return nil
}
19 changes: 19 additions & 0 deletions database/db_memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package database

import "context"

// NewInMemory returns an in-memory store of the database.
func NewInMemory() Store {
return &memoryQuerier{}
}

type memoryQuerier struct{}

// InTx doesn't rollback data properly for in-memory yet.
func (q *memoryQuerier) InTx(ctx context.Context, fn func(Store) error) error {
return fn(q)
}

func (q *memoryQuerier) ExampleQuery(ctx context.Context) error {
return nil
}
2 changes: 2 additions & 0 deletions database/dump.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Code generated by 'make database/generate'. DO NOT EDIT.

89 changes: 89 additions & 0 deletions database/dump/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"bytes"
"context"
"database/sql"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"

"github.com/coder/coder/database"
"github.com/coder/coder/database/postgres"
)

func main() {
connection, closeFn, err := postgres.Open()
if err != nil {
panic(err)
}
defer closeFn()
db, err := sql.Open("postgres", connection)
if err != nil {
panic(err)
}
err = database.Migrate(context.Background(), "postgres", db)
if err != nil {
panic(err)
}
cmd := exec.Command(
"pg_dump",
"--schema-only",
connection,
"--no-privileges",
"--no-owner",
"--no-comments",

// We never want to manually generate
// queries executing against this table.
"--exclude-table=schema_migrations",
)
cmd.Env = []string{
"PGTZ=UTC",
"PGCLIENTENCODING=UTF8",
}
var output bytes.Buffer
cmd.Stdout = &output
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
panic(err)
}

for _, sed := range []string{
// Remove all comments.
"/^--/d",
// Public is implicit in the schema.
"s/ public\\./ /",
// Remove database settings.
"s/SET.*;//g",
// Remove select statements. These aren't useful
// to a reader of the dump.
"s/SELECT.*;//g",
// Removes multiple newlines.
"/^$/N;/^\\n$/D",
} {
cmd := exec.Command("sed", "-e", sed)
cmd.Stdin = bytes.NewReader(output.Bytes())
output = bytes.Buffer{}
cmd.Stdout = &output
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
panic(err)
}
}

dump := fmt.Sprintf("-- Code generated by 'make database/generate'. DO NOT EDIT.\n%s", output.Bytes())
_, mainPath, _, ok := runtime.Caller(0)
if !ok {
panic("couldn't get caller path")
}
err = ioutil.WriteFile(filepath.Join(mainPath, "..", "..", "dump.sql"), []byte(dump), 0644)
if err != nil {
panic(err)
}
}
48 changes: 48 additions & 0 deletions database/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package database

import (
"context"
"database/sql"
"embed"
"errors"

"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
"golang.org/x/xerrors"
)

//go:embed migrations/*.sql
var migrations embed.FS

// Migrate runs SQL migrations to ensure the database schema is up-to-date.
func Migrate(ctx context.Context, dbName string, db *sql.DB) error {
sourceDriver, err := iofs.New(migrations, "migrations")
if err != nil {
return xerrors.Errorf("create iofs: %w", err)
}
dbDriver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return xerrors.Errorf("wrap postgres connection: %w", err)
}
m, err := migrate.NewWithInstance("", sourceDriver, dbName, dbDriver)
if err != nil {
return xerrors.Errorf("migrate: %w", err)
}
err = m.Up()
if err != nil {
if errors.Is(err, migrate.ErrNoChange) {
// It's OK if no changes happened!
return nil
}
return xerrors.Errorf("up: %w", err)
}
srcErr, dbErr := m.Close()
if srcErr != nil {
return xerrors.Errorf("close source: %w", err)
}
if dbErr != nil {
return xerrors.Errorf("close database: %w", err)
}
return nil
}
30 changes: 30 additions & 0 deletions database/migrate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//go:build linux

package database_test

import (
"context"
"database/sql"
"testing"

"github.com/coder/coder/database"
"github.com/coder/coder/database/postgres"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)

func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}

func TestMigrate(t *testing.T) {
t.Parallel()

connection, closeFn, err := postgres.Open()
require.NoError(t, err)
defer closeFn()
db, err := sql.Open("postgres", connection)
require.NoError(t, err)
err = database.Migrate(context.Background(), "postgres", db)
require.NoError(t, err)
}
Empty file.
Empty file.
11 changes: 11 additions & 0 deletions database/migrations/create_migration.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"

if [ -z "$1" ]; then
echo "First argument is the migration name!"
exit 1
fi

migrate create -ext sql -dir . -seq $1

echo "After making adjustments, run \"make database/generate\" to generate models."
5 changes: 5 additions & 0 deletions database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading