Report only known schema inventory metadata at startup
3 files changed,
+108
−0
cmd/home/main.go+1 −0internal/home/schema_inventory.go+64 −0internal/home/schema_inventory_test.go+43 −0
CI passed — run details
diff --git a/cmd/home/main.go b/cmd/home/main.go| index 0ea67a3..e74c411 100644 |
| --- a/cmd/home/main.go |
| +++ b/cmd/home/main.go |
| @@ -101,6 +101,7 @@ func main() { |
| os.Exit(1) |
| } |
| + home.LogSchemaInventory(context.Background(), d, runtime, logger) |
| app, err := home.NewRuntime(runtime, d, logger) |
| if err != nil { |
| logger.Error("wire app", "err", err) |
diff --git a/internal/home/schema_inventory.go b/internal/home/schema_inventory.go| new file mode 100644 |
| index 0000000..c1a5c65 |
| --- /dev/null |
| +++ b/internal/home/schema_inventory.go |
| @@ -0,0 +1,64 @@ |
| +package home |
| + |
| +import ( |
| + "context" |
| + "database/sql" |
| + "log/slog" |
| + "sort" |
| + "time" |
| + |
| + "github.com/carlosframework/rastrillo/db" |
| +) |
| + |
| +// LogSchemaInventory reports only compiled migration names and counts. Unknown |
| +// database strings never enter logs, and the report grants no operator authority. |
| +func LogSchemaInventory(parent context.Context, d *db.DB, cfg Runtime, logger *slog.Logger) { |
| + if parent == nil || d == nil || logger == nil || (cfg.role != "credential" && cfg.role != "directory") { |
| + return |
| + } |
| + ctx, cancel := context.WithTimeout(parent, 2*time.Second) |
| + defer cancel() |
| + expected := map[string]bool{} |
| + known := map[string]bool{} |
| + for _, m := range cfg.Schema().All() { |
| + expected[m.ID] = true |
| + } |
| + for _, role := range []string{"credential", "directory"} { |
| + for _, m := range (Runtime{role: role}).Schema().All() { |
| + known[m.ID] = true |
| + } |
| + } |
| + rows, err := d.Writer().QueryContext(ctx, "SELECT CASE WHEN typeof(id)='text' AND length(id)<=256 THEN id END FROM rastrillo_migrations LIMIT 1001") |
| + if err != nil { |
| + logger.Info("schema inventory unavailable") |
| + return |
| + } |
| + var extra []string |
| + unknown, count := 0, 0 |
| + for rows.Next() { |
| + var id sql.NullString |
| + if rows.Scan(&id) != nil { |
| + rows.Close() |
| + logger.Info("schema inventory unavailable") |
| + return |
| + } |
| + count++ |
| + if count > 1000 { |
| + break |
| + } |
| + if id.Valid && expected[id.String] { |
| + continue |
| + } |
| + if id.Valid && known[id.String] { |
| + extra = append(extra, id.String) |
| + } else { |
| + unknown++ |
| + } |
| + } |
| + if rows.Err() != nil || rows.Close() != nil { |
| + logger.Info("schema inventory unavailable") |
| + return |
| + } |
| + sort.Strings(extra) |
| + logger.Info("schema inventory", "role", cfg.role, "known_extra", extra, "unclassified", unknown, "truncated", count > 1000) |
| +} |
diff --git a/internal/home/schema_inventory_test.go b/internal/home/schema_inventory_test.go| new file mode 100644 |
| index 0000000..33805b0 |
| --- /dev/null |
| +++ b/internal/home/schema_inventory_test.go |
| @@ -0,0 +1,43 @@ |
| +package home |
| + |
| +import ( |
| + "bytes" |
| + "log/slog" |
| + "strings" |
| + "testing" |
| + |
| + "amadan.net/oficina/home/internal/home/sealtest" |
| +) |
| + |
| +func TestSchemaInventoryEmitsOnlyCompiledNames(t *testing.T) { |
| + pair := newExchangePair(t) |
| + cfg := *pair.directory.runtime |
| + expected := map[string]bool{} |
| + for _, m := range cfg.Schema().All() { |
| + expected[m.ID] = true |
| + } |
| + extra := "" |
| + for _, m := range (Runtime{role: "credential"}).Schema().All() { |
| + if !expected[m.ID] { |
| + extra = m.ID |
| + break |
| + } |
| + } |
| + if extra == "" { |
| + t.Fatal("no cross-role fixture") |
| + } |
| + for _, id := range []string{extra, sealtest.CanaryAddress, strings.Repeat("x", 300)} { |
| + if _, err := pair.directory.db.Writer().Exec("INSERT INTO rastrillo_migrations(id,applied_at,checksum) VALUES(?,'2026-01-01T00:00:00Z','fixture')", id); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + var out bytes.Buffer |
| + logger := slog.New(slog.NewJSONHandler(&out, nil)) |
| + LogSchemaInventory(t.Context(), pair.directory.db, cfg, logger) |
| + if !bytes.Contains(out.Bytes(), []byte(extra)) || !bytes.Contains(out.Bytes(), []byte(`"unclassified":2`)) { |
| + t.Fatal("inventory missed known migration or count") |
| + } |
| + if bytes.Contains(out.Bytes(), []byte(sealtest.CanaryAddress)) || bytes.Contains(out.Bytes(), []byte(strings.Repeat("x", 300))) || bytes.Contains(out.Bytes(), []byte("fixture")) { |
| + t.Fatal("inventory leaked database strings") |
| + } |
| +} |