Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

## v0.0.2

### Added

- Added `cmd/migrate-gen`, a stable CLI entrypoint for generating worker infrastructure migrations with the same defaults and table-name overrides as `migrations.Config`.

### Documentation

- Documented the quick CLI flow and the package-level migration API in `README.md` and `migrations` package docs.
46 changes: 33 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The module intentionally favors simple, database-native coordination:

```text
.
├── cmd/migrate-gen/ # Stable CLI for generating worker infrastructure migrations
├── worker.go / config.go # Worker orchestrator and configuration
├── dispatcher/ # PollDispatcher and NotifyDispatcher
├── postgres/ # PostgreSQL DAL for registration, leadership, assignment, checkpoints, gap-skip audit
Expand Down Expand Up @@ -78,7 +79,7 @@ The module is intended to be used alongside `github.com/eventsalsa/store` and it
You need:

- the event store tables from `github.com/eventsalsa/store`
- the worker metadata tables from `github.com/eventsalsa/worker/migrations`
- the worker metadata tables generated from `github.com/eventsalsa/worker/cmd/migrate-gen`

### 2. Build an event store

Expand Down Expand Up @@ -305,7 +306,26 @@ If a stale-gap decision later proves too aggressive for a consumer, the recovery

## Migration generation

Use the `migrations` package to generate the worker infrastructure SQL.
For the quickest path, use the stable `cmd/migrate-gen` entrypoint.

```bash
go run github.com/eventsalsa/worker/cmd/migrate-gen \
-output ./db/migrations \
-filename 002_worker_tables.sql
```

The CLI defaults match `migrations.DefaultConfig()`, and you can override table names to line up with `worker.With*Table(...)` options:

```bash
go run github.com/eventsalsa/worker/cmd/migrate-gen \
-output ./db/migrations \
-worker-nodes-table infra.worker_nodes \
-consumer-assignments-table infra.consumer_assignments \
-consumer-checkpoints-table infra.consumer_checkpoints \
-consumer-gap-skips-table infra.consumer_gap_skips
```

For more advanced integration, use the `migrations` package directly from your own program.

```go
package main
Expand All @@ -317,17 +337,17 @@ import (
)

func main() {
config := &migrations.Config{
OutputFolder: "./db/migrations",
OutputFilename: "002_worker_tables.sql",
WorkerNodesTable: "worker_nodes",
ConsumerAssignmentsTable: "consumer_assignments",
ConsumerCheckpointsTable: "consumer_checkpoints",
}

if err := migrations.GeneratePostgres(config); err != nil {
log.Fatal(err)
}
config := migrations.DefaultConfig()
config.OutputFolder = "./db/migrations"
config.OutputFilename = "002_worker_tables.sql"
config.WorkerNodesTable = "infra.worker_nodes"
config.ConsumerAssignmentsTable = "infra.consumer_assignments"
config.ConsumerCheckpointsTable = "infra.consumer_checkpoints"
config.ConsumerGapSkipsTable = "infra.consumer_gap_skips"

if err := migrations.GeneratePostgres(&config); err != nil {
log.Fatal(err)
}
}
```

Expand Down
65 changes: 65 additions & 0 deletions cmd/migrate-gen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Command migrate-gen generates SQL migration files for worker infrastructure.
//
// Usage:
//
// go run github.com/eventsalsa/worker/cmd/migrate-gen -output migrations -filename init_worker.sql
//
// Or with go generate:
//
// //go:generate go run github.com/eventsalsa/worker/cmd/migrate-gen -output migrations
package main

import (
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"

"github.com/eventsalsa/worker/migrations"
)

func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}

func run(args []string, stdout, stderr io.Writer) int {
config := migrations.DefaultConfig()

flags := flag.NewFlagSet("migrate-gen", flag.ContinueOnError)
flags.SetOutput(stderr)

outputFolder := flags.String("output", config.OutputFolder, "Output folder for migration file")
outputFilename := flags.String("filename", "", "Output filename (default: timestamp-based)")
workerNodesTable := flags.String("worker-nodes-table", config.WorkerNodesTable, "Name of worker nodes table")
consumerAssignmentsTable := flags.String("consumer-assignments-table", config.ConsumerAssignmentsTable, "Name of consumer assignments table")
consumerCheckpointsTable := flags.String("consumer-checkpoints-table", config.ConsumerCheckpointsTable, "Name of consumer checkpoints table")
consumerGapSkipsTable := flags.String("consumer-gap-skips-table", config.ConsumerGapSkipsTable, "Name of consumer gap skips table")

if err := flags.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}

return 2
}

config.OutputFolder = *outputFolder
config.WorkerNodesTable = *workerNodesTable
config.ConsumerAssignmentsTable = *consumerAssignmentsTable
config.ConsumerCheckpointsTable = *consumerCheckpointsTable
config.ConsumerGapSkipsTable = *consumerGapSkipsTable

if *outputFilename != "" {
config.OutputFilename = *outputFilename
}

if err := migrations.GeneratePostgres(&config); err != nil {
fmt.Fprintf(stderr, "Error generating migration: %v\n", err)
return 1
}

fmt.Fprintf(stdout, "Generated PostgreSQL migration: %s\n", filepath.Join(config.OutputFolder, config.OutputFilename))
return 0
}
122 changes: 122 additions & 0 deletions cmd/migrate-gen/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package main

import (
"bytes"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)

func TestRunGeneratesMigrationWithDefaults(t *testing.T) {
outputDir := t.TempDir()
var stdout bytes.Buffer
var stderr bytes.Buffer

exitCode := run([]string{"-output", outputDir}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("run exit code = %d, want 0; stderr=%q", exitCode, stderr.String())
}

entries, err := os.ReadDir(outputDir)
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
if len(entries) != 1 {
t.Fatalf("generated files = %d, want 1", len(entries))
}

outputFilename := entries[0].Name()
if !regexp.MustCompile(`^\d{14}_init_worker_infrastructure\.sql$`).MatchString(outputFilename) {
t.Fatalf("output filename = %q, want timestamped worker migration filename", outputFilename)
}

content, err := os.ReadFile(filepath.Join(outputDir, outputFilename))
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}

sql := string(content)
requiredStrings := []string{
"CREATE TABLE IF NOT EXISTS worker_nodes",
"CREATE TABLE IF NOT EXISTS consumer_assignments",
"CREATE TABLE IF NOT EXISTS consumer_checkpoints",
"CREATE TABLE IF NOT EXISTS consumer_gap_skips",
}
for _, required := range requiredStrings {
if !strings.Contains(sql, required) {
t.Fatalf("generated SQL missing %q", required)
}
}

expectedOutput := "Generated PostgreSQL migration: " + filepath.Join(outputDir, outputFilename) + "\n"
if stdout.String() != expectedOutput {
t.Fatalf("stdout = %q, want %q", stdout.String(), expectedOutput)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
}

func TestRunGeneratesMigrationWithOverrides(t *testing.T) {
outputDir := t.TempDir()
outputFilename := "002_worker_tables.sql"
var stdout bytes.Buffer
var stderr bytes.Buffer

exitCode := run([]string{
"-output", outputDir,
"-filename", outputFilename,
"-worker-nodes-table", "infra.worker_nodes",
"-consumer-assignments-table", "infra.consumer_assignments",
"-consumer-checkpoints-table", "infra.consumer_checkpoints",
"-consumer-gap-skips-table", "infra.consumer_gap_skips",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("run exit code = %d, want 0; stderr=%q", exitCode, stderr.String())
}

content, err := os.ReadFile(filepath.Join(outputDir, outputFilename))
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}

sql := string(content)
requiredStrings := []string{
"CREATE SCHEMA IF NOT EXISTS infra;",
"CREATE TABLE IF NOT EXISTS infra.worker_nodes",
"CREATE TABLE IF NOT EXISTS infra.consumer_assignments",
"CREATE TABLE IF NOT EXISTS infra.consumer_checkpoints",
"CREATE TABLE IF NOT EXISTS infra.consumer_gap_skips",
}
for _, required := range requiredStrings {
if !strings.Contains(sql, required) {
t.Fatalf("generated SQL missing %q", required)
}
}

expectedOutput := "Generated PostgreSQL migration: " + filepath.Join(outputDir, outputFilename) + "\n"
if stdout.String() != expectedOutput {
t.Fatalf("stdout = %q, want %q", stdout.String(), expectedOutput)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
}

func TestRunReturnsParseError(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer

exitCode := run([]string{"-does-not-exist"}, &stdout, &stderr)
if exitCode != 2 {
t.Fatalf("run exit code = %d, want 2", exitCode)
}
if stdout.Len() != 0 {
t.Fatalf("stdout = %q, want empty", stdout.String())
}
if !strings.Contains(stderr.String(), "flag provided but not defined") {
t.Fatalf("stderr = %q, want parse error", stderr.String())
}
}
13 changes: 13 additions & 0 deletions migrations/doc.go
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
// Package migrations provides DDL generation for worker meta tables.
//
// To generate migrations with the stable CLI entrypoint, use:
//
// go run github.com/eventsalsa/worker/cmd/migrate-gen -output migrations
//
// Or add a go generate directive to your code:
//
// //go:generate go run github.com/eventsalsa/worker/cmd/migrate-gen -output ../../migrations
//
// For advanced cases, call GeneratePostgres with a Config value from your own
// program so you can override filenames or table names directly.
package migrations

//go:generate go run ../cmd/migrate-gen -output example_migrations -filename example.sql
Loading