2026-07-25 19:24:19 +02:00
|
|
|
// Command dumpschema regenerates docs/DATABASE.md from the real, live
|
|
|
|
|
// database schema -- it opens a fresh temp database through store.Open
|
|
|
|
|
// (the exact same code path geniusrund itself uses) and introspects
|
|
|
|
|
// sqlite_master, so the generated documentation can never drift from what
|
|
|
|
|
// the app actually creates. Run it after any change to
|
|
|
|
|
// internal/store/schema.sql:
|
|
|
|
|
//
|
|
|
|
|
// go run ./cmd/dumpschema
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2026-08-04 16:33:26 +02:00
|
|
|
"log/slog"
|
2026-07-25 19:24:19 +02:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-08-04 16:33:26 +02:00
|
|
|
applog "geniusrun/backend/internal/log"
|
2026-07-25 19:24:19 +02:00
|
|
|
"geniusrun/backend/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type schemaEntry struct {
|
|
|
|
|
typ string // "table" or "view"
|
|
|
|
|
name string
|
|
|
|
|
tblName string
|
|
|
|
|
sql string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
2026-08-04 16:33:26 +02:00
|
|
|
slog.SetDefault(applog.NewLogger("info", os.Stdout))
|
2026-07-25 19:24:19 +02:00
|
|
|
tmpDir, err := os.MkdirTemp("", "geniusrun-dumpschema")
|
|
|
|
|
must(err)
|
|
|
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
|
|
|
|
|
|
db, err := store.Open(filepath.Join(tmpDir, "schema-check.db"))
|
|
|
|
|
must(err)
|
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
|
|
rows, err := db.Query(`
|
|
|
|
|
SELECT type, name, tbl_name, sql FROM sqlite_master
|
|
|
|
|
WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'
|
|
|
|
|
ORDER BY rowid`)
|
|
|
|
|
must(err)
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
var tables, views []schemaEntry
|
|
|
|
|
indexesByTable := map[string][]schemaEntry{}
|
|
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var e schemaEntry
|
|
|
|
|
must(rows.Scan(&e.typ, &e.name, &e.tblName, &e.sql))
|
|
|
|
|
switch e.typ {
|
|
|
|
|
case "table":
|
|
|
|
|
tables = append(tables, e)
|
|
|
|
|
case "view":
|
|
|
|
|
views = append(views, e)
|
|
|
|
|
case "index":
|
|
|
|
|
indexesByTable[e.tblName] = append(indexesByTable[e.tblName], e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
must(rows.Err())
|
|
|
|
|
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
b.WriteString("# geniusrun database schema\n\n")
|
|
|
|
|
b.WriteString("Generated from the live schema via `go run ./cmd/dumpschema` -- do not hand-edit. ")
|
|
|
|
|
b.WriteString("The source of truth is `backend/internal/store/schema.sql`; regenerate this file after changing it.\n\n")
|
|
|
|
|
|
|
|
|
|
b.WriteString("## Tables\n\n")
|
|
|
|
|
for _, t := range tables {
|
|
|
|
|
fmt.Fprintf(&b, "- [`%s`](#%s)\n", t.name, t.name)
|
|
|
|
|
}
|
|
|
|
|
b.WriteString("\n")
|
|
|
|
|
|
|
|
|
|
for _, t := range tables {
|
|
|
|
|
fmt.Fprintf(&b, "## `%s`\n\n", t.name)
|
|
|
|
|
fmt.Fprintf(&b, "```sql\n%s;\n```\n\n", t.sql)
|
|
|
|
|
if idxs := indexesByTable[t.name]; len(idxs) > 0 {
|
|
|
|
|
b.WriteString("Indexes:\n\n```sql\n")
|
|
|
|
|
for _, idx := range idxs {
|
|
|
|
|
fmt.Fprintf(&b, "%s;\n", idx.sql)
|
|
|
|
|
}
|
|
|
|
|
b.WriteString("```\n\n")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(views) > 0 {
|
|
|
|
|
b.WriteString("## Views\n\n")
|
|
|
|
|
for _, v := range views {
|
|
|
|
|
fmt.Fprintf(&b, "### `%s`\n\n", v.name)
|
|
|
|
|
fmt.Fprintf(&b, "```sql\n%s;\n```\n\n", v.sql)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outPath := "../docs/DATABASE.md"
|
|
|
|
|
must(os.WriteFile(outPath, []byte(b.String()), 0644))
|
refactor(log): unified schema — mandatory type/class/method, request_id removed
Every log record now carries type ('http' for the request middleware
line, 'wrapper' for garmin execute() calls and forwarded wrapper.py
stderr, 'app' for everything else), class (Go type with package, or the
package/module for free functions), and method (the emitting Go/Python
function -- wrapper.py stamps it automatically, so its msg prefixes are
gone). msg is optional and omitted when empty; the redundant messages
('HTTP request', 'wrapper call', 'garmin wrapper stderr') are dropped,
the HTTP verb moves to http_method, source=garmin-wrapper is replaced by
type=wrapper, and the request_id middleware plumbing (applog
WithLogger/FromContext) is removed. applog.App(class, method) is the
tagging helper for app records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 19:45:30 +02:00
|
|
|
applog.App("main", "main").Info("wrote schema doc", "path", outPath)
|
2026-07-25 19:24:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func must(err error) {
|
|
|
|
|
if err != nil {
|
refactor(log): unified schema — mandatory type/class/method, request_id removed
Every log record now carries type ('http' for the request middleware
line, 'wrapper' for garmin execute() calls and forwarded wrapper.py
stderr, 'app' for everything else), class (Go type with package, or the
package/module for free functions), and method (the emitting Go/Python
function -- wrapper.py stamps it automatically, so its msg prefixes are
gone). msg is optional and omitted when empty; the redundant messages
('HTTP request', 'wrapper call', 'garmin wrapper stderr') are dropped,
the HTTP verb moves to http_method, source=garmin-wrapper is replaced by
type=wrapper, and the request_id middleware plumbing (applog
WithLogger/FromContext) is removed. applog.App(class, method) is the
tagging helper for app records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 19:45:30 +02:00
|
|
|
applog.App("main", "must").Error("dumpschema failed", "error", err)
|
2026-08-04 16:33:26 +02:00
|
|
|
os.Exit(1)
|
2026-07-25 19:24:19 +02:00
|
|
|
}
|
|
|
|
|
}
|