package app import ( "context" "database/sql" "fmt" "net/url" "strings" ) func (a *App) ensureProjectDatabase(ctx context.Context, p *Project) error { if !a.cfg.DBProvisionEnabled { return nil } adminDB, err := sql.Open("pgx", a.cfg.DBProvisionAdminURL) if err != nil { return fmt.Errorf("db provisioning connect failed: %w", err) } defer adminDB.Close() if err := adminDB.PingContext(ctx); err != nil { return fmt.Errorf("db provisioning ping failed: %w", err) } if p.DBName == "" { p.DBName = fmt.Sprintf("app_%d", p.ID) } if p.DBUser == "" { p.DBUser = fmt.Sprintf("appu_%d", p.ID) } if p.DBPassword == "" { pw, err := newDeployToken() if err != nil { return err } p.DBPassword = pw } if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %s) THEN CREATE ROLE %s LOGIN PASSWORD %s; ELSE ALTER ROLE %s LOGIN PASSWORD %s; END IF; END $$;`, sqlQuoteLiteral(p.DBUser), sqlQuoteIdent(p.DBUser), sqlQuoteLiteral(p.DBPassword), sqlQuoteIdent(p.DBUser), sqlQuoteLiteral(p.DBPassword))); err != nil { return fmt.Errorf("create/alter role failed: %w", err) } var dbExists bool if err := adminDB.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = $1)`, p.DBName).Scan(&dbExists); err != nil { return fmt.Errorf("check database existence failed: %w", err) } if !dbExists { if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`CREATE DATABASE %s OWNER %s TEMPLATE template0`, sqlQuoteIdent(p.DBName), sqlQuoteIdent(p.DBUser))); err != nil { return fmt.Errorf("create database failed: %w", err) } } if _, err := a.db.ExecContext(ctx, `UPDATE projects SET db_name=$1, db_user=$2, db_password=$3 WHERE id=$4`, p.DBName, p.DBUser, p.DBPassword, p.ID); err != nil { return fmt.Errorf("persist db credentials failed: %w", err) } return nil } func (a *App) dropProjectDatabase(ctx context.Context, p Project) error { if !a.cfg.DBProvisionEnabled { return nil } if strings.TrimSpace(p.DBName) == "" || strings.TrimSpace(p.DBUser) == "" { return nil } adminDB, err := sql.Open("pgx", a.cfg.DBProvisionAdminURL) if err != nil { return fmt.Errorf("db provisioning connect failed: %w", err) } defer adminDB.Close() if err := adminDB.PingContext(ctx); err != nil { return fmt.Errorf("db provisioning ping failed: %w", err) } // Terminate active sessions so DROP DATABASE succeeds. _, _ = adminDB.ExecContext(ctx, `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, p.DBName) if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`DROP DATABASE IF EXISTS %s`, sqlQuoteIdent(p.DBName))); err != nil { return fmt.Errorf("drop database failed: %w", err) } if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`DROP ROLE IF EXISTS %s`, sqlQuoteIdent(p.DBUser))); err != nil { return fmt.Errorf("drop role failed: %w", err) } return nil } func (a *App) projectDatabaseURL(p Project) (string, error) { if p.DBName == "" || p.DBUser == "" || p.DBPassword == "" { return "", nil } base, err := url.Parse(a.cfg.DBProvisionAdminURL) if err != nil { return "", err } base.User = url.UserPassword(p.DBUser, p.DBPassword) base.Path = "/" + p.DBName return base.String(), nil } func sqlQuoteIdent(s string) string { return `"` + strings.ReplaceAll(s, `"`, `""`) + `"` } func sqlQuoteLiteral(s string) string { return `'` + strings.ReplaceAll(s, `'`, `''`) + `'` }