Second checkpoint
This commit is contained in:
70
internal/cli/migrate.go
Normal file
70
internal/cli/migrate.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"gis/internal/config"
|
||||
"gis/migrations"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
_ "github.com/jackc/pgx/v5/stdlib" // registers the "pgx" database/sql driver
|
||||
"github.com/pressly/goose/v3"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate <command> [args]",
|
||||
Short: "Run database migrations (up, down, status, reset, redo, fresh, version)",
|
||||
Long: "Run goose migrations from the embedded migration files.\n\n" +
|
||||
"In addition to the standard goose commands, `fresh` drops every object in\n" +
|
||||
"the public schema and re-applies all migrations from scratch.\n\n" +
|
||||
"Examples:\n" +
|
||||
" gis migrate up\n" +
|
||||
" gis migrate down\n" +
|
||||
" gis migrate status\n" +
|
||||
" gis migrate fresh\n" +
|
||||
" gis migrate up-to 00002",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", cfg.DB.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
goose.SetBaseFS(migrations.FS)
|
||||
if err := goose.SetDialect("postgres"); err != nil {
|
||||
return fmt.Errorf("set dialect: %w", err)
|
||||
}
|
||||
|
||||
command := args[0]
|
||||
if command == "fresh" {
|
||||
return migrateFresh(cmd.Context(), db, cfg.DB.Schema)
|
||||
}
|
||||
return goose.RunContext(cmd.Context(), command, db, ".", args[1:]...)
|
||||
},
|
||||
}
|
||||
|
||||
// migrateFresh drops the configured schema (every table, type, and the goose
|
||||
// version table) and re-applies all migrations. This is a destructive
|
||||
// development convenience, equivalent to "drop everything and rerun".
|
||||
func migrateFresh(ctx context.Context, db *sql.DB, schema string) error {
|
||||
// Identifiers cannot be parameterized, so quote the schema name to guard
|
||||
// against injection and to handle non-lowercase/special identifiers.
|
||||
quoted := pgx.Identifier{schema}.Sanitize()
|
||||
stmt := fmt.Sprintf(`DROP SCHEMA IF EXISTS %s CASCADE; CREATE SCHEMA %s;`, quoted, quoted)
|
||||
if _, err := db.ExecContext(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("reset schema %q: %w", schema, err)
|
||||
}
|
||||
if err := goose.UpContext(ctx, db, "."); err != nil {
|
||||
return fmt.Errorf("re-apply migrations: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
37
internal/cli/root.go
Normal file
37
internal/cli/root.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Package cli defines the cobra command tree for the gis binary: serve, worker,
|
||||
// and migrate.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "gis",
|
||||
Short: "GIS application server, worker, and migration tool",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
|
||||
// Execute runs the root command, exiting non-zero on error.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd, workerCmd, migrateCmd)
|
||||
}
|
||||
|
||||
// signalContext returns a context cancelled on SIGINT or SIGTERM.
|
||||
func signalContext() (context.Context, context.CancelFunc) {
|
||||
return signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
}
|
||||
24
internal/cli/serve.go
Normal file
24
internal/cli/serve.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"gis/internal/app"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Run the HTTP server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx, cancel := signalContext()
|
||||
defer cancel()
|
||||
|
||||
application, err := app.New(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer application.Close()
|
||||
|
||||
return application.Server().Run(ctx)
|
||||
},
|
||||
}
|
||||
53
internal/cli/worker.go
Normal file
53
internal/cli/worker.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gis/internal/app"
|
||||
"gis/internal/messaging/rabbitmq"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var publishExample bool
|
||||
|
||||
var workerCmd = &cobra.Command{
|
||||
Use: "worker",
|
||||
Short: "Run the RabbitMQ consumers (dataset parsing + example)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx, cancel := signalContext()
|
||||
defer cancel()
|
||||
|
||||
application, err := app.New(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer application.Close()
|
||||
|
||||
if publishExample {
|
||||
if err := rabbitmq.PublishExample(ctx, application.Publisher()); err != nil {
|
||||
return err
|
||||
}
|
||||
application.Log.Info("published example message")
|
||||
}
|
||||
|
||||
// Run every consumer concurrently; cancel all if one fails.
|
||||
g, gctx := errgroup.WithContext(ctx)
|
||||
for _, c := range application.Consumers() {
|
||||
c := c
|
||||
g.Go(func() error { return c.Run(gctx) })
|
||||
}
|
||||
|
||||
// Graceful shutdown (context cancelled) is not an error.
|
||||
if err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
workerCmd.Flags().BoolVar(&publishExample, "publish-example", false, "publish one example message before consuming")
|
||||
}
|
||||
Reference in New Issue
Block a user