Sudo-free local runner for postgres benchmarks + large-v1 dump fix - #73
Conversation
Postgres benchmarks (livesqlbench-*, bird-interact-lite-exp) had no turnkey local path: the DB-load logic lives only in the cloud worker, and the system postgres isn't provisioned with the bird_interact role or the task DBs (and no sudo to it). This adds a private, sudo-free workflow so a local `bird-interact --dataset <postgres benchmark>` just runs. - scripts/setup_local_postgres.py: initdb + run a separate PostgreSQL cluster owned by the current user under <main_checkout>/.local_pg/ on a spare port (default 5544). Creates the bird_interact (LOGIN) and root (dump OWNER) roles under loopback trust auth, and createdb + `psql -f` loads each benchmark's pg_dumps/<db>/*.sql. Idempotent via per-db markers. - scripts/fetch_local_annotations.py: sync stable *.task.json from GCS into the local annotations store so --require-annotation passes. - scripts/run_local_postgres.py: one-shot orchestrator — load auth dotenv, provision+load postgres, sync annotations, then exec bird-interact with --data/--db-path auto-derived; everything after `--` is passthrough. - tests/scripts/test_local_postgres_helpers.py: pins the pure helpers (BIRD_PG_* export shape, cluster location, required roles, dotenv parser). - CLAUDE.md: documents the workflow + gotchas (root-role dumps, the benign `livesqlbench_postgresql` host error that the cloud also hits, effort no-op for raw claude_sdk). .gitignore: ignore /.local_pg/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single-table livesqlbench-large dump (residential_data_large, disaster_relief_large: 1 CREATE TABLE + FK ALTERs to tables it never creates) previously loaded "successfully" — plain `psql -f` returns 0 on per-statement errors, and the loader touched the done-marker regardless. The missing tables then surfaced only as cryptic per-task `dry_run_error`s at eval time. load_databases now captures psql stderr, counts ERROR lines, checks the resulting public-schema table count, and on any load error OR a zero-table result WARNs loudly (naming the dump + first missing relation) and does NOT mark the DB done, so a re-provision retries once the dump is re-fetched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The livesqlbench-large-v1 Google Drive zip ships one dump file PER TABLE (postgre_table_dumps_large/<db>_template/<table>.sql — each with CREATE TABLE + INSERTs + its own FK constraints) plus PARTIAL `*_full.sql` aggregates (disaster_relief's largest full dump has only 29 of 49 tables). The previous "pick the largest file" heuristic grabbed a single big single-table file, so residential_data_large and disaster_relief_large each loaded with just 1 table — manifesting downstream as cryptic per-task dry_run_errors at eval time. stage_dumps now groups all source files per DB and, when there are several, concatenates them via combine_per_table_dumps: select the single-table files (the complete set, as the upstream init-databases_postgresql_large_v1.sh uses), hoist CREATE TYPE/DOMAIN ahead of the tables that use them, and defer every FOREIGN KEY constraint to the end — so the combined dump loads in ONE clean pass regardless of file order. Verified: residential 48/48 tables, disaster 50/50 tables, 0 load errors (was 1/48 and 1/50). Statement classification skips pg_dump's `-- Name: …; Type: …` comment banners (the bug that made the first cut never defer any FK). Tests cover the classifiers, the type-first/FK-last ordering, single-table selection over aggregates, and COPY-block / meta-command handling in the splitter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 21 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThis PR adds tooling to run Postgres-backed benchmarks locally without sudo: a cluster provisioning/loading script, a GCS annotation sync script, an orchestrator that ties them together with bird-interact, improved per-table pg_dump combining logic, documentation, and unit tests. ChangesLocal Postgres Benchmark Workflow
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant RunLocalPostgres
participant SetupLocalPostgres
participant FetchLocalAnnotations
participant BirdInteract
User->>RunLocalPostgres: run_local_postgres.py --benchmark ...
RunLocalPostgres->>RunLocalPostgres: load_env_file(auth env)
RunLocalPostgres->>SetupLocalPostgres: ensure_cluster, load_databases
SetupLocalPostgres-->>RunLocalPostgres: BIRD_PG_* env vars
RunLocalPostgres->>FetchLocalAnnotations: main(benchmark, instance filter)
FetchLocalAnnotations-->>RunLocalPostgres: fetch status
RunLocalPostgres->>BirdInteract: subprocess.call(bird-interact ...)
BirdInteract-->>User: benchmark results
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (6)
scripts/fetch_local_annotations.py (1)
41-42: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winEmpty instance-id tokens not filtered.
Same pattern as
setup_local_postgres.py: a trailing comma in--instance-idsproduces an empty-string id, leading to a confusing "not found" error rather than a clear validation message.🤖 Prompt for 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. In `@scripts/fetch_local_annotations.py` around lines 41 - 42, The instance ID parsing in fetch_local_annotations.py does not filter out empty tokens, so a trailing comma can produce an empty string and a misleading lookup failure. Update the argument handling around args.instance_ids to strip and discard blank entries before assigning ids, matching the validation pattern used in setup_local_postgres.py, and ensure the code paths that use ids receive only non-empty instance IDs.scripts/setup_local_postgres.py (3)
319-320: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winEmpty instance-id tokens not filtered.
args.instance_ids.split(",")with e.g. a trailing comma yields an empty string in the list, which then fails themissingcheck with a confusinginstance_ids not found in ...: ['']error instead of a clear "empty id" message.run_local_postgres.py's equivalent parsing already filters withif s.strip().🐛 Suggested fix
ids = ([s.strip() for s in args.instance_ids.split(",")] - if args.instance_ids else None) + if args.instance_ids else None) + if ids: + ids = [i for i in ids if i]🤖 Prompt for 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. In `@scripts/setup_local_postgres.py` around lines 319 - 320, The instance ID parsing in the local Postgres setup still keeps empty tokens from args.instance_ids, which leads to confusing downstream missing-ID errors. Update the parsing logic around the ids assignment to filter out blank entries after splitting, matching the existing behavior used in run_local_postgres.py. Keep the fix localized to the args.instance_ids handling so the rest of the validation flow sees only real IDs.
259-266: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win"ERROR:" detection is locale-dependent.
errs = [ln for ln in (r.stderr or "").splitlines() if "ERROR:" in ln]relies onpsqlemitting the EnglishERROR:prefix. If the process env has a non-English locale,libpq/psqlmay localize this prefix (e.g.ERREUR:), causinghad_errorsto stayFalsefor a genuinely broken load — silently defeating the core hardening this PR adds. Pin the locale for the load subprocess.♻️ Suggested fix
r = _psql(bindir, port, p["sock"], "-q", "-o", os.devnull, "-v", "ON_ERROR_STOP=0", "-f", str(sql), db=db, - capture_output=True, text=True, check=False) + capture_output=True, text=True, check=False, + env={**os.environ, "LC_ALL": "C", "LANG": "C"})🤖 Prompt for 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. In `@scripts/setup_local_postgres.py` around lines 259 - 266, The error detection in the _psql load path is relying on the localized "ERROR:" prefix, so pin the subprocess locale to a stable English setting when calling _psql from the setup_local_postgres script. Update the call site that captures r.stderr and computes errs so psql/libpq emits consistent error text regardless of the host locale, preserving had_errors for broken loads.
208-230: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueRaw string interpolation for SQL identifiers/literals.
_db_exists/_table_countbuild queries via f-strings with unescapeddb/role values (flagged by static analysis as SQL-injection-style construction).dbcurrently only comes from trusted local directory names / benchmark metadata, so exploitation risk is minimal, but a stray quote in a db name would still break the query silently.♻️ Suggested hardening
def _db_exists(bindir: Path, port: int, db: str) -> bool: p = _paths() r = _psql( bindir, port, p["sock"], "-tAc", - f"SELECT 1 FROM pg_database WHERE datname='{db}'", + "SELECT 1 FROM pg_database WHERE datname=%s".replace("%s", f"'{db.replace(chr(39), chr(39)*2)}'"), capture_output=True, text=True, ) return r.stdout.strip() == "1"🤖 Prompt for 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. In `@scripts/setup_local_postgres.py` around lines 208 - 230, _raw SQL string interpolation in _db_exists and _table_count should be hardened by avoiding direct f-string construction for SQL identifiers/literals. Update the query-building in these helpers to use a safe quoting/escaping approach or parameterized execution via _psql so db values cannot break the statement. Keep the changes localized to _db_exists and _table_count and preserve their current return behavior.scripts/run_local_postgres.py (1)
47-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePersonal machine path as default.
_DEFAULT_ENV_FILEhardcodes one developer's home directory layout. Harmless since a missing file is a non-fatal no-op and it's overridable via--env-file/BIRD_ENV_FILE, but a generic default (e.g.<repo>/.envor~/.bird-interact/.env) would avoid baking a personal path into shared source.🤖 Prompt for 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. In `@scripts/run_local_postgres.py` around lines 47 - 50, The default env file path in the _DEFAULT_ENV_FILE assignment is hardcoded to a personal Dropbox/PyCharm directory, which should be replaced with a generic shared default. Update the run_local_postgres.py configuration so the default points to a repo-relative or user-neutral location, while still honoring BIRD_ENV_FILE and the --env-file override. Keep the change limited to the _DEFAULT_ENV_FILE initialization and related path fallback behavior.tests/scripts/test_local_postgres_helpers.py (1)
50-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest doesn't verify "under main checkout".
The assertion only checks
cluster_dir().name == ".local_pg"; it doesn't verify the path is actually rooted atpaths.main_checkout_root()as the test name claims.♻️ Suggested strengthening
def test_cluster_dir_is_under_main_checkout(): # Worktree-safe: cluster lives in the main checkout, name is stable. - assert setup_local_postgres.cluster_dir().name == ".local_pg" + from bird_interact_agents import paths + assert setup_local_postgres.cluster_dir() == paths.main_checkout_root() / ".local_pg"🤖 Prompt for 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. In `@tests/scripts/test_local_postgres_helpers.py` around lines 50 - 52, The test test_cluster_dir_is_under_main_checkout only checks the directory name, so it does not prove the cluster path is rooted under the main checkout. Strengthen this assertion by also verifying setup_local_postgres.cluster_dir() resolves relative to paths.main_checkout_root(), using the existing cluster_dir() and main_checkout_root() helpers so the test matches its name and intent.
🤖 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.
Nitpick comments:
In `@scripts/fetch_local_annotations.py`:
- Around line 41-42: The instance ID parsing in fetch_local_annotations.py does
not filter out empty tokens, so a trailing comma can produce an empty string and
a misleading lookup failure. Update the argument handling around
args.instance_ids to strip and discard blank entries before assigning ids,
matching the validation pattern used in setup_local_postgres.py, and ensure the
code paths that use ids receive only non-empty instance IDs.
In `@scripts/run_local_postgres.py`:
- Around line 47-50: The default env file path in the _DEFAULT_ENV_FILE
assignment is hardcoded to a personal Dropbox/PyCharm directory, which should be
replaced with a generic shared default. Update the run_local_postgres.py
configuration so the default points to a repo-relative or user-neutral location,
while still honoring BIRD_ENV_FILE and the --env-file override. Keep the change
limited to the _DEFAULT_ENV_FILE initialization and related path fallback
behavior.
In `@scripts/setup_local_postgres.py`:
- Around line 319-320: The instance ID parsing in the local Postgres setup still
keeps empty tokens from args.instance_ids, which leads to confusing downstream
missing-ID errors. Update the parsing logic around the ids assignment to filter
out blank entries after splitting, matching the existing behavior used in
run_local_postgres.py. Keep the fix localized to the args.instance_ids handling
so the rest of the validation flow sees only real IDs.
- Around line 259-266: The error detection in the _psql load path is relying on
the localized "ERROR:" prefix, so pin the subprocess locale to a stable English
setting when calling _psql from the setup_local_postgres script. Update the call
site that captures r.stderr and computes errs so psql/libpq emits consistent
error text regardless of the host locale, preserving had_errors for broken
loads.
- Around line 208-230: _raw SQL string interpolation in _db_exists and
_table_count should be hardened by avoiding direct f-string construction for SQL
identifiers/literals. Update the query-building in these helpers to use a safe
quoting/escaping approach or parameterized execution via _psql so db values
cannot break the statement. Keep the changes localized to _db_exists and
_table_count and preserve their current return behavior.
In `@tests/scripts/test_local_postgres_helpers.py`:
- Around line 50-52: The test test_cluster_dir_is_under_main_checkout only
checks the directory name, so it does not prove the cluster path is rooted under
the main checkout. Strengthen this assertion by also verifying
setup_local_postgres.cluster_dir() resolves relative to
paths.main_checkout_root(), using the existing cluster_dir() and
main_checkout_root() helpers so the test matches its name and intent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a4e3edd8-765e-4dc0-8276-0c6e42a707ce
📒 Files selected for processing (8)
.gitignoreCLAUDE.mdscripts/download_pg_dumps.pyscripts/fetch_local_annotations.pyscripts/run_local_postgres.pyscripts/setup_local_postgres.pytests/scripts/test_download_pg_dumps_combine.pytests/scripts/test_local_postgres_helpers.py
- setup_local_postgres.start_cluster: reject a port mismatch when a cluster is already running on a different port (else BIRD_PG_PORT is exported for a port nothing listens on). Reads the live port from postmaster.pid. - setup_local_postgres.resolve_dbs_for: full-run path now fails fast when no pg_dumps/<db>/ exist (was: silently "provision 0 DB(s)"), matching the subset path. - Filter blank instance-id tokens (trailing comma) in setup_local_postgres and fetch_local_annotations, so a stray comma no longer yields a misleading "not found" error. - download_pg_dumps: add --force to overwrite existing staged dumps; a plain re-run still skips them, so repairing a partial dump previously required a manual rm. CLAUDE.md updated to say --force. Tests: resolve_dbs_for empty-raise, postmaster.pid port parse, start_cluster port-mismatch guard, and stage_dumps --force overwrite. Full suite: 3866 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…low-up) origin/main advanced with PR #73 (sudo-free local postgres benchmark runner + local/cloud instrumentation parity) and a DEV-1629 follow-up (93d6a8e) that landed after this branch merged the DEV-1629 branch. Only conflict: tests/test_shared_otf_prompts.py v1 SHAs. The DEV-1629 follow-up reworded the v1 slayer tool-inventory line (list `search` = find / `inspect_model` = whole model / `inspect` = a single known column's Description + Sample values) and re-baselined the v1 SHAs. The v1 prompts.py auto-merged to that reworded version, so took origin/main's SHAs (942ce8e9 / 39eecf2f) — verified by recomputing post-merge — and dropped the now-stale "UNCHANGED vs DEV-1629" note, keeping the still-accurate point that the DEV-1591 compact discipline reaches v1 main via build_main_workflow_note (runtime), not the static prompt hashed here. Everything else (PR #73 scripts/tests, _run_capture/run.py parity + atomic attempt-row write, CLAUDE.md local-postgres section) auto-merged cleanly. Full non-integration suite: 3940 passed, 94 skipped, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What & why
Postgres benchmarks (
livesqlbench-*,bird-interact-lite-exp) had no turnkey local path: the DB-load logic lives only in the cloud worker, and the system postgres isn't provisioned with thebird_interactrole or the task DBs (and no sudo to it). This adds a private, sudo-free workflow so a localbird-interact --dataset <postgres benchmark>just runs — and fixes a benchmark-data staging bug that left two large-v1 DBs with only 1 table each.Commits
Local runner (
7d31931)scripts/setup_local_postgres.py—initdb+ run a separate PostgreSQL cluster owned by the current user under<main_checkout>/.local_pg/(gitignored) on a spare port (5544). Createsbird_interact(LOGIN) +root(dumpOWNER) roles under loopback trust auth;createdb+psql -floads each benchmark'spg_dumps/<db>/*.sql. Idempotent (--stop,--recreate).scripts/fetch_local_annotations.py— sync stable*.task.jsonfrom GCS into the local annotations store so--require-annotationpasses.scripts/run_local_postgres.py— one-shot orchestrator: load auth dotenv → provision+load → annotation sync → execbird-interactwith--data/--db-pathauto-derived; everything after--is passthrough..gitignore/.local_pg/.Surface partial dumps (
feeaf7c) — the loader capturedpsqlstderr, checks the resulting table count, and WARNs loudly + refuses to mark a DB done on any load error or a zero-table result (instead of silently "succeeding" and failing later as a cryptic per-taskdry_run_error).Fix large-v1 dump staging (
402d96d) — the large-v1 Google Drive zip ships one dump file per table plus partial*_full.sqlaggregates (disaster_relief's largest is 29/49 tables). The old "pick largest file" heuristic grabbed a single big single-table file → a 1-table DB.download_pg_dumps.combine_per_table_dumpsnow concatenates the single-table files, hoistsCREATE TYPEfirst and defersFOREIGN KEYs last → one clean load. Verified: residential 48/48, disaster 50/50, 0 errors (was 1/48, 1/50).Tests
tests/scripts/test_local_postgres_helpers.py— BIRD_PG_* export shape, cluster location, required roles, dotenv parser, marker-skip-on-error.tests/scripts/test_download_pg_dumps_combine.py— classifiers vs pg_dump comment banners, type-first/FK-last ordering, single-table selection over aggregates, COPY/meta-command splitting.Validation
End-to-end local run of
livesqlbench-large(opus-4-8, raw, one-shot) works against the private cluster; grading produces correct pass/fail verdicts (validated a passing task too).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests