molecule-core/workspace-server/internal/db/postgres.go
Hongming Wang d8026347e5 chore: open-source restructure — rename dirs, remove internal files, scrub secrets
Renames:
- platform/ → workspace-server/ (Go module path stays as "platform" for
  external dep compat — will update after plugin module republish)
- workspace-template/ → workspace/

Removed (moved to separate repos or deleted):
- PLAN.md — internal roadmap (move to private project board)
- HANDOFF.md, AGENTS.md — one-time internal session docs
- .claude/ — gitignored entirely (local agent config)
- infra/cloudflare-worker/ → Molecule-AI/molecule-tenant-proxy
- org-templates/molecule-dev/ → standalone template repo
- .mcp-eval/ → molecule-mcp-server repo
- test-results/ — ephemeral, gitignored

Security scrubbing:
- Cloudflare account/zone/KV IDs → placeholders
- Real EC2 IPs → <EC2_IP> in all docs
- CF token prefix, Neon project ID, Fly app names → redacted
- Langfuse dev credentials → parameterized
- Personal runner username/machine name → generic

Community files:
- CONTRIBUTING.md — build, test, branch conventions
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1

All Dockerfiles, CI workflows, docker-compose, railway.toml, render.yaml,
README, CLAUDE.md updated for new directory names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 00:24:44 -07:00

82 lines
2.4 KiB
Go

package db
import (
"database/sql"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
_ "github.com/lib/pq"
)
var DB *sql.DB
func InitPostgres(databaseURL string) error {
var err error
DB, err = sql.Open("postgres", databaseURL)
if err != nil {
return fmt.Errorf("open postgres: %w", err)
}
DB.SetMaxOpenConns(25)
DB.SetMaxIdleConns(5)
if err := DB.Ping(); err != nil {
return fmt.Errorf("ping postgres: %w", err)
}
log.Println("Connected to Postgres")
return nil
}
// RunMigrations applies every forward migration file in migrationsDir on
// platform boot.
//
// Issue #211 — DO NOT glob `*.sql`. That matches both `.up.sql` and `.down.sql`,
// and sort.Strings orders "d" before "u", so every boot used to run the
// rollback BEFORE the forward migration for any pair, wiping data from any
// table the pair recreates (020_workspace_auth_tokens was the canary — every
// restart wiped live tokens, regressing AdminAuth to fail-open bypass for
// every subsequent request).
//
// The fix: only run files that are either `.up.sql` or plain `.sql` (legacy
// pre-pair migrations like 009_activity_logs.sql). Never touch `.down.sql`
// — those are intentional rollbacks, only to be run by operators manually
// via psql when a real rollback is required.
//
// NOTE: this runner still re-applies every migration on every boot. That
// works for idempotent `CREATE TABLE IF NOT EXISTS` + `ALTER TABLE ... IF NOT
// EXISTS` statements but means non-idempotent DDL will fail on restart.
// Migration authors must write idempotent SQL. A real schema_migrations
// tracking table would be better; tracked as follow-up.
func RunMigrations(migrationsDir string) error {
allFiles, err := filepath.Glob(filepath.Join(migrationsDir, "*.sql"))
if err != nil {
return fmt.Errorf("glob migrations: %w", err)
}
// Forward-only filter — skip *.down.sql explicitly.
files := make([]string, 0, len(allFiles))
for _, f := range allFiles {
base := filepath.Base(f)
if strings.HasSuffix(base, ".down.sql") {
continue
}
files = append(files, f)
}
sort.Strings(files)
for _, f := range files {
log.Printf("Applying migration: %s", filepath.Base(f))
content, err := os.ReadFile(f)
if err != nil {
return fmt.Errorf("read %s: %w", f, err)
}
if _, err := DB.Exec(string(content)); err != nil {
return fmt.Errorf("exec %s: %w", filepath.Base(f), err)
}
}
log.Printf("Applied %d migrations", len(files))
return nil
}