◐ Off-By-One · answer catalog

go-sqlite-db-split-workspace-lookup

1 answer(s)godocker

go-sqlite-db-split-workspace-lookup

📦 Source in repository (JSON)

Answer

Root cause. The service keeps two separate sqlite files — cron.db (owns cron_jobs) and workspaces.db (owns workspaces). serve.go opens both, but CronHandler was constructed with only the cron handle and queried SELECT … FROM workspaces on it. SQLite namespaces tables per file, so the cron handle reports SQL logic error: no such table: workspaces (1) → HTTP 500.

Fix — three parts:

1. Handler gains a workspaceDB field (cron.go):

type CronHandler struct {
    db          *sql.DB // cron.db — owns cron_jobs
    workspaceDB *sql.DB // workspaces.db — shared, opened earlier in serve.go
}

func NewCronHandler(db, workspaceDB *sql.DB) *CronHandler {
    return &CronHandler{db: db, workspaceDB: workspaceDB}
}

2. New resolveWorkspaceID() helper queries the shared DB, never the cron handle, and maps sql.ErrNoRows to a sentinel:

var errWorkspaceNotFound = errors.New("workspace not found")

func (h *CronHandler) resolveWorkspaceID(ctx context.Context, slug string) (int64, error) {
    var id int64
    err := h.workspaceDB.QueryRowContext(ctx,
        `SELECT id FROM workspaces WHERE slug = ?`, slug).Scan(&id)
    if errors.Is(err, sql.ErrNoRows) {
        return 0, errWorkspaceNotFound
    }
    return id, err
}

RunDueJobs now calls it: wsID, err := h.resolveWorkspaceID(ctx, j.WorkspaceSlug) and returns 500 with a descriptive message only on real errors.

3. Wiring in serve.go — the shared workspaces handle is opened earlier and passed through (works in main() and tests alike):

func serve(cronDBPath, workspacesDBPath string) *CronHandler {
    cronDB := mustOpenDB(cronDBPath)
    workspacesDB := mustOpenDB(workspacesDBPath) // opened earlier, shared by all handlers
    return NewCronHandler(cronDB, workspacesDB)
}

Regression test (cron_test.go) — two real temp sqlite files via t.TempDir(): seeds cron_jobs into cron.db, workspaces into workspaces.db, then asserts (a) resolveWorkspaceID("acme") == 42, (b) unknown slug → errWorkspaceNotFound sentinel, (c) end-to-end RunDueJobs over httptest returns 200 with ran job 1 workspace=42, and (d) the setup itself is valid by confirming workspaces is genuinely absent from the cron handle.


Evidence & signatures

Verified in a standalone module (`/tmp/dbsplit`, Go 1.26, `modernc.org/sqlite` pure-Go driver):

1. **Bug reproduced first:** with `resolveWorkspaceID` reverted to query `h.db`, the regression test failed with the exact production error:
   ```
   cron_test.go:71: resolveWorkspaceID(acme) = SQL logic error: no such table: workspaces (1)
   --- FAIL: TestCronHandler_SplitDBWorkspaceLookup
   ```
2. **Fix applied:** test passes; `gofmt -l` clean, `go vet` clean.
3. **Full suite + race detector:** `go test -race -count=1 ./...` → `ok dbsplit 1.030s` — **3 tests, 3 passed**.
4. **Edge cases tested:**
   - Unknown slug returns the `errWorkspaceNotFound` sentinel (checked via `errors.Is`), not a raw sqlite error.
   - Orphan due job (workspace deleted) → HTTP 500 with a clear "workspace not found" message, no panic.
   - No due jobs → 200 with empty body.
   - `TestServe_Wiring` proves `serve()` threads both non-nil handles into the handler.
   - Split integrity cross-check: querying `workspaces` from the cron handle still errors — confirming the test would catch any regression back to single-handle behavior.

---
{"model": "deepseek-v4-flash", "problem_class": "go-sqlite-db-split-workspace-lookup", "result": "passed", "tests": 3}
Generated from the verified corpus · MIT licensedBack to the catalog