fintual-backend-devops-go / go-service / migrate.go
migrate.go
Raw
package main

import (
	"context"
	"crypto/sha256"
	_ "embed"
	"fmt"
	"strings"

	"github.com/jackc/pgx/v5"
)

//go:embed schema.sql
var schema string

func schemaHash() string { return fmt.Sprintf("%x", sha256.Sum256([]byte(schema))) }

// Serving never changes schema. Existing untracked tables require review.
func migrate(ctx context.Context, conn *pgx.Conn) error {
	tx, err := conn.Begin(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx)
	if _, err = tx.Exec(ctx, "SELECT pg_advisory_xact_lock(486290431)"); err != nil {
		return err
	}
	var tracked bool
	if err = tx.QueryRow(ctx, "SELECT to_regclass('public.content_schema_migrations') IS NOT NULL").Scan(&tracked); err != nil {
		return err
	}
	if tracked {
		var hash string
		if err = tx.QueryRow(ctx, "SELECT sha256 FROM content_schema_migrations WHERE version=1 AND (SELECT count(*) FROM content_schema_migrations)=1").Scan(&hash); err != nil {
			return err
		}
		if hash != schemaHash() {
			return fmt.Errorf("schema checksum mismatch; refusing migration")
		}
		return tx.Commit(ctx)
	}
	var exists bool
	if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name IN ('blog_user','blog_tag','blog_post','blog_post_tags','blog_comment'))`).Scan(&exists); err != nil {
		return err
	}
	if exists {
		return fmt.Errorf("untracked blog tables already exist; refusing automatic adoption")
	}
	sql := strings.Replace(schema, "BEGIN;", "", 1)
	sql = strings.TrimSuffix(strings.TrimSpace(sql), "COMMIT;")
	if _, err = tx.Exec(ctx, sql); err != nil {
		return err
	}
	if _, err = tx.Exec(ctx, `CREATE TABLE content_schema_migrations(version integer PRIMARY KEY, sha256 text NOT NULL, applied_at timestamptz NOT NULL DEFAULT now())`); err != nil {
		return err
	}
	if _, err = tx.Exec(ctx, "INSERT INTO content_schema_migrations(version,sha256) VALUES(1,$1)", schemaHash()); err != nil {
		return err
	}
	return tx.Commit(ctx)
}