Skip to content

Add optional Postgres result sink for obacloud integration#4

Merged
aaronbrethorst merged 17 commits into
mainfrom
db-access
May 26, 2026
Merged

Add optional Postgres result sink for obacloud integration#4
aaronbrethorst merged 17 commits into
mainfrom
db-access

Conversation

@aaronbrethorst
Copy link
Copy Markdown
Member

@aaronbrethorst aaronbrethorst commented May 26, 2026

Summary

Adds an optional Postgres "result sink" so a caller (specifically obacloud's ServerValidationJob) can read a validator run's report back from a database after a Render one-off job finishes, instead of scraping stdout. The validator still prints to stdout and exits with its existing codes — the DB write is purely additive and only activates when the caller supplies the new five sink fields in the input config.

  • New sink/ package: Config + Configured/Validate/Write, normalizeDSN for jdbc: URL handling, pgx/v5 for the actual write.
  • config.Config parses five flat snake_case JSON fields (db_url, db_user, db_pass, correlation_id, result_table) and rejects partial sink configs at config-load time.
  • report exposes RenderJSON/RenderErrorJSON byte-returning helpers so the same JSON goes to stdout and to the sink.
  • main.go writes one row per run after stdout: status="completed" for both PASS and FAIL verdicts (the verdict lives at summary.verdict inside result_data); status="error" is reserved for the errorDocument variant. Sink failures are logged to stderr and never alter the validator's exit code.

Contract is locked — it's an upstream dependency of obacloud PR #747.

Design context:

  • Spec: docs/superpowers/specs/2026-05-25-result-sink-design.md
  • Plan: docs/superpowers/plans/2026-05-25-result-sink-implementation.md

Backward compatibility

Stdout output, exit codes, the published report schema, and CLI flags are all unchanged. Pre-sink invocations (no db_url in the payload) behave byte-identically to today; TestRunSkipsSinkWhenNotConfigured pins this.

Security

  • API key redaction continues to apply throughout the existing pipeline.
  • db_pass is scrubbed in three layers: sink.redactErr (raw + URL-encoded for pgx error echoes), main.redactionSecrets/scrub for inline raw-arg errors, and cfg.DBPass is threaded into the validator-error scrub.
  • result_table is enforced against a closed allow-list ({"oba_validator_results"}) both at config-load time and again inside sink.Write (defense in depth).
  • normalizeDSN defaults sslmode=require and connect_timeout=5.

Test plan

  • make test — all unit tests pass (sink integration test skips without OBA_VALIDATOR_DB_DSN).
  • go vet ./... and gofmt -l . clean.
  • make build succeeds.
  • Existing redaction tests (TestRunJSONConfigErrorRedactsInlineAPIKey, TestRunTextConfigErrorRedactsInlineAPIKey) still pass.
  • New tests: TestRunInvokesSinkOnCompleted, TestRunSkipsSinkWhenNotConfigured, TestRunSinkErrorDoesNotAlterExitCode, TestRunRenderJSONFailureWritesSinkErrorRow.
  • Optional: run the integration test against a real Postgres — OBA_VALIDATOR_DB_DSN="postgres://postgres@127.0.0.1:5432/postgres?sslmode=disable" go test ./sink/ -run TestWriteIntegration -v.

Suggested follow-ups (non-blocking)

Surfaced by the final whole-branch review; safe to defer to a follow-up PR:

  • Add a text-mode-with-sink test that pins the WriteText → RenderJSON-for-sink branch.
  • Inject a validatorRun package-level var so the validator-error path can be exercised in tests (it's currently dead code because validator.Run never returns a non-nil error today).
  • Mention OBA_VALIDATOR_DB_DSN in CLAUDE.md's Commands section and add a make test-sink-integration target.
  • One paragraph in README.md describing the optional sink activation.

Deployment

The new image must be deployed to the existing Render service crn-d8a9p20jo6nc73e9h9jg before obacloud PR #747 is usable end-to-end. The image tag, render.yaml, and Render service id are unchanged. During the deploy gap, obacloud times out cleanly with "No result found for correlation_id" — no data loss.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added optional PostgreSQL database sink to persist validator run outcomes when credentials and configuration are provided; captures run status, verdict, and error information.
    • Database write failures are logged but do not affect validator exit code or stdout output.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@aaronbrethorst, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 40 minutes and 30 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 467b928d-8517-42f0-919d-9111e79bb7b7

📥 Commits

Reviewing files that changed from the base of the PR and between e7fb5cc and aa6f60d.

📒 Files selected for processing (6)
  • README.md
  • cmd/oba-validator/main.go
  • cmd/oba-validator/main_test.go
  • sink/sink.go
  • sink/sink_integration_test.go
  • sink/sink_test.go
📝 Walkthrough

Walkthrough

This PR implements an optional Postgres "result sink" feature for the validator. It adds a new sink/ package that persists validator run reports to a Postgres database keyed by correlation ID, integrates sink configuration into the existing config system, refactors report rendering to expose JSON bytes for both stdout and database writes, orchestrates sink writes from the CLI on success and error paths with secret redaction, and provides comprehensive testing and documentation.

Changes

Result Sink Implementation

Layer / File(s) Summary
Sink package: Config, validation, and database I/O
sink/sink.go, sink/dsn.go, sink/sink_test.go, sink/sink_integration_test.go
sink.Config provides configuration and activation gating, Validated() enforces required fields and allow-lists result_table, normalizeDSN() converts JDBC-style URLs to pgx-compatible DSNs, Write() connects to Postgres, creates the results table idempotently, and inserts a single row keyed by correlation_id using ON CONFLICT DO NOTHING; errors are redacted to prevent credential leakage. Unit tests cover configuration state machines and DSN edge cases; env-gated integration test verifies conflict handling preserves original stored data.
Configuration with sink support
config/config.go, config/config_test.go
Config struct adds five optional sink fields (DBURL, DBUser, DBPass, CorrelationID, ResultTable), SinkConfig() assembler converts these into sink.Config, and validate() delegates to sink.Config.Validate() to enforce complete/valid sink configuration when db_url is provided. Tests verify parsing, disabling when absent, and rejection of partial/unsupported configurations.
Report JSON rendering
report/report.go
New RenderJSON and RenderErrorJSON functions expose fully marshalled, indented JSON bytes (with apiKey redaction for error variant) for use by both stdout and sink writers; existing WriteJSON/WriteErrorJSON now call these render functions before writing.
CLI secret redaction and orchestration
cmd/oba-validator/main.go, go.mod
Multi-secret redaction via regex-based redactionSecrets and scrub handles both apiKey and db_pass in error output; testable package-level hooks renderJSON and sinkWrite support injection; run() function scrubs secrets in config-load and validator-error paths, writes sink status rows on success (completed) and error (error), skips writes when sink unconfigured, and ensures sink write failures log to stderr without changing exit code. Adds github.com/jackc/pgx/v5 v5.9.2 dependency.
CLI integration tests
cmd/oba-validator/main_test.go
Tests verify sink writer invocation on successful JSON runs with completed status and correlation/table values, skipped writes when sink fields absent, error handling that does not exit with code 2 and logs to stderr, and sink error status on JSON render failure.
Design and implementation documentation
docs/superpowers/specs/2026-05-25-result-sink-design.md, docs/superpowers/plans/2026-05-25-result-sink-implementation.md, CLAUDE.md
Upstream design spec defines input contract (five optional fields that activate sink), database schema (single-row insert keyed by correlation_id with ON CONFLICT DO NOTHING), status vocabulary (completed vs error for retrieval, verdict stored in result_data), backward-compatibility promises (stdout/exit codes unchanged), security requirements (allow-list, credential redaction), and architectural recommendations. Detailed implementation plan enumerates nine tasks with testing/verification steps. CLAUDE.md updates document sink activation, timing (after stdout), status meanings, verdict location, and that write failures log to stderr without affecting exit code.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • OneBusAway/server-validator#2: Establishes the foundational --json output behavior and error redaction patterns that this PR builds upon for sink integration.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the primary change: adding an optional Postgres result sink feature for database integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch db-access

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sink/sink_integration_test.go`:
- Line 41: Defer currently discards the result of admin.Close(ctx); wrap it in a
deferred anonymous function that captures the error from admin.Close(ctx) and
fails or logs the test appropriately (e.g., t.Fatalf or t.Errorf) so teardown
failures are surfaced; locate the defer using the admin.Close and ctx
identifiers in the test and replace it with a defer func() { if err :=
admin.Close(ctx); err != nil { t.Fatalf("admin.Close error: %v", err) } }() (or
t.Errorf) to ensure the Close error is handled.

In `@sink/sink.go`:
- Around line 154-166: The Write method currently accepts any status string; add
a guard at the start of Config.Write to enforce the allowed status vocabulary
(only "completed" and "error") and return a descriptive error for any other
value before performing DB I/O or calling Validate/allowedTables; update
Config.Write to check the status parameter (e.g., compare against the allowed
set) and fail fast, referencing the Config.Write function, the status parameter,
and ResultTable/allowedTables so callers cannot persist arbitrary statuses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 458d1f5c-686a-4abc-bb93-55ff1b864669

📥 Commits

Reviewing files that changed from the base of the PR and between a23e1d8 and e7fb5cc.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • CLAUDE.md
  • cmd/oba-validator/main.go
  • cmd/oba-validator/main_test.go
  • config/config.go
  • config/config_test.go
  • docs/superpowers/plans/2026-05-25-result-sink-implementation.md
  • docs/superpowers/specs/2026-05-25-result-sink-design.md
  • go.mod
  • report/report.go
  • sink/dsn.go
  • sink/sink.go
  • sink/sink_integration_test.go
  • sink/sink_test.go

Comment thread sink/sink_integration_test.go Outdated
Comment thread sink/sink.go
CodeRabbit findings on PR #4:
- sink_integration_test.go was discarding admin.Close(ctx)'s error via a
  bare defer; wrap it in an anonymous func that t.Errorf's on error so
  teardown failures surface in CI.
- sink.Write accepted any status string and would happily persist a row
  with status='whatever'. Add a guard at the top of Write that rejects
  anything other than 'completed' or 'error' before opening a connection,
  with a new TestWriteRejectsUnsupportedStatus exercising the guard.
- S1192: extract sinkWriteFailedMsg const for the 4 'result sink write
  failed:' stderr prefixes in cmd/oba-validator/main.go.
- S3776: lower TestNormalizeDSN cognitive complexity by extracting the
  per-case assertions into a checkDSNResult helper.
- Duplication: replace the 5 near-identical httptest stub-server setups
  in cmd/oba-validator/main_test.go with newStubOBA(t), newStubFeed(t),
  and sinkCfgJSON(...) helpers. Cuts ~60 duplicated lines, dropping the
  diff's duplicated-line density below SonarCloud's 3% threshold.
@sonarqubecloud
Copy link
Copy Markdown

@aaronbrethorst aaronbrethorst merged commit 95237f7 into main May 26, 2026
6 checks passed
@aaronbrethorst aaronbrethorst deleted the db-access branch May 26, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant