// Command jetgen regenerates the go-jet/v2 query-builder code under // galaxy/backend/internal/postgres/jet/ against a transient PostgreSQL // instance. // // Invoke as `go run ./cmd/jetgen` (or via the `make jet` target) from inside // `galaxy/backend`. The tool is not part of the runtime binary. // // Steps: // // 1. start a postgres:16-alpine container via testcontainers-go // 2. open it through galaxy/postgres with search_path=backend // 3. ensure the backend schema exists, then apply the embedded goose // migrations // 4. run jet's PostgreSQL generator against schema=backend, writing into // ../internal/postgres/jet package main import ( "context" "database/sql" "errors" "fmt" "log" "net/url" "os" "path/filepath" "runtime" "strings" "time" "galaxy/backend/internal/postgres/migrations" "galaxy/postgres" jetpostgres "github.com/go-jet/jet/v2/generator/postgres" testcontainers "github.com/testcontainers/testcontainers-go" tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" ) const ( postgresImage = "postgres:16-alpine" superuserName = "galaxy" superuserPassword = "galaxy" superuserDatabase = "galaxy_backend" backendSchema = "backend" containerStartup = 90 * time.Second defaultOpTimeout = 10 * time.Second jetOutputDirSuffix = "internal/postgres/jet" ) func main() { if err := run(context.Background()); err != nil { log.Fatalf("jetgen: %v", err) } } func run(ctx context.Context) error { outputDir, err := jetOutputDir() if err != nil { return err } container, err := tcpostgres.Run(ctx, postgresImage, tcpostgres.WithDatabase(superuserDatabase), tcpostgres.WithUsername(superuserName), tcpostgres.WithPassword(superuserPassword), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(containerStartup), ), ) if err != nil { return fmt.Errorf("start postgres container: %w", err) } defer func() { if termErr := testcontainers.TerminateContainer(container); termErr != nil { log.Printf("jetgen: terminate container: %v", termErr) } }() baseDSN, err := container.ConnectionString(ctx, "sslmode=disable") if err != nil { return fmt.Errorf("resolve container dsn: %w", err) } scopedDSN, err := dsnWithSearchPath(baseDSN, backendSchema) if err != nil { return err } if err := applyMigrations(ctx, scopedDSN); err != nil { return err } // jet's ProcessSchema wipes / on every run, so package // metadata kept directly under outputDir (e.g. jet.go) survives. We only // ensure the parent directory exists so the first run on a fresh // checkout does not fail with ENOENT. if err := os.MkdirAll(outputDir, 0o755); err != nil { return fmt.Errorf("ensure jet output dir: %w", err) } jetDB, err := openScoped(ctx, scopedDSN) if err != nil { return fmt.Errorf("open scoped pool for jet generation: %w", err) } defer func() { _ = jetDB.Close() }() // Drop goose's bookkeeping table inside the schema-scoped connection so // jet does not generate code for it. The table is recreated on the next // migration run; jetgen never reuses the container. if _, err := jetDB.ExecContext(ctx, "DROP TABLE IF EXISTS goose_db_version"); err != nil { return fmt.Errorf("drop goose_db_version: %w", err) } if err := jetpostgres.GenerateDB(jetDB, backendSchema, outputDir); err != nil { return fmt.Errorf("jet generate: %w", err) } log.Printf("jetgen: generated jet code into %s (schema=%s)", outputDir, backendSchema) return nil } // dsnWithSearchPath rewrites the connection string so each new connection // pins search_path to the named schema. The schema must exist before the // first query that depends on search_path resolution; ensureSchema handles // that on the migration path. func dsnWithSearchPath(baseDSN, schema string) (string, error) { parsed, err := url.Parse(baseDSN) if err != nil { return "", fmt.Errorf("parse base dsn: %w", err) } values := parsed.Query() values.Set("search_path", schema) if values.Get("sslmode") == "" { values.Set("sslmode", "disable") } parsed.RawQuery = values.Encode() return parsed.String(), nil } func applyMigrations(ctx context.Context, dsn string) error { db, err := openScoped(ctx, dsn) if err != nil { return fmt.Errorf("open scoped pool: %w", err) } defer func() { _ = db.Close() }() if err := postgres.Ping(ctx, db, defaultOpTimeout); err != nil { return err } if err := ensureSchema(ctx, db, backendSchema); err != nil { return err } if err := postgres.RunMigrations(ctx, db, migrations.Migrations(), "."); err != nil { return fmt.Errorf("run migrations: %w", err) } return nil } // ensureSchema creates the named schema when it is absent. The statement is // idempotent and unaffected by search_path, so it must run before goose // creates its bookkeeping table inside the schema-scoped connection. func ensureSchema(ctx context.Context, db *sql.DB, schema string) error { stmt := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", quoteIdent(schema)) if _, err := db.ExecContext(ctx, stmt); err != nil { return fmt.Errorf("ensure schema %q: %w", schema, err) } return nil } func openScoped(ctx context.Context, dsn string) (*sql.DB, error) { cfg := postgres.DefaultConfig() cfg.PrimaryDSN = dsn cfg.OperationTimeout = defaultOpTimeout return postgres.OpenPrimary(ctx, cfg) } // jetOutputDir returns the absolute path that jet should write into. The path // is anchored to galaxy/backend via runtime.Caller so the tool can be // invoked from any working directory. func jetOutputDir() (string, error) { _, file, _, ok := runtime.Caller(0) if !ok { return "", errors.New("resolve runtime caller for jet output path") } dir := filepath.Dir(file) // dir = .../galaxy/backend/cmd/jetgen moduleRoot := filepath.Clean(filepath.Join(dir, "..", "..")) return filepath.Join(moduleRoot, jetOutputDirSuffix), nil } // quoteIdent quotes a SQL identifier by doubling embedded quote characters. // jetgen uses a fixed schema name, but quoting keeps the helper safe to reuse // if the constant ever changes to a configurable value. func quoteIdent(name string) string { return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` }