Repository navigation
feat(cli): db --remote for execute and studio - #213
Conversation
…tudio - Add PlatformClient.getDatabaseUrl(projectId) that GETs /projects/:id/database-url and delegates errors to throwPlatformApiError. - Add --remote boolean flag to db execute and db studio subcommand arg shapes (DbArgs union + parseDbArgs + help text). - Unit tests: get-database-url.test.ts covers success, 404, 403, and no-body invariants; db-args.test.ts extended for --remote on execute and studio. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- D1 path POSTs to ${proxyUrl}/d1/query with token, binding (auto-discovered
via getResources, defaulting to DB), and cfAccessHeaders; errors on --file
- PG path calls getDatabaseUrl to fetch the connection string, then connects
directly with pg.Client; surfaces a friendly message on 404
- Safety guard prompts on destructive statements (non-SELECT/EXPLAIN/WITH) when
stdin is a TTY; skipped on non-TTY and when --yes is implied by non-interactive
- isSafeStatement / confirmDestructiveRemote helpers keep the logic clean
- Updated execute subcommand description in db-args.ts to reflect --remote
- 22 unit tests in db-execute-remote.test.ts covering D1 and PG paths,
error cases (404, non-OK proxy, missing token, unlinked project), TTY prompt,
table rendering, and regression for the non-remote local path
- Add a WITH (CTE) test case to confirm the safety prompt is skipped for statements starting with WITH, alongside the existing SELECT and EXPLAIN cases. - Add a test that stubs CF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET and asserts the CF-Access-Client-Id / CF-Access-Client-Secret headers are forwarded on the POST request to the D1 proxy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e prompt Move the D1 --file unsupported check to the very top of the remote branch, before reading the file or calling confirmDestructiveRemote. Add a regression test that asserts the confirmation prompt is never reached when --file is passed to a D1 remote execute. Also adds three deferral/limitation comments: - isSafeStatement: note that leading SQL comments are not stripped - confirmDestructiveRemote: note --yes flag is intentionally deferred - D1 binding resolution: TODO for --binding multi-binding support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- runStudio now accepts an optional `remote` flag - PG remote: fetches stored connection URL via getDatabaseUrl, passes it to writeDrizzleConfig + drizzle-kit studio (same as local PG path) - D1 remote: errors with a friendly "not yet supported" message pointing users to `void db execute --remote` for ad-hoc queries - help.ts HELP_TREE: updated execute and studio entries to document --remote flag with options and examples - db-args.ts: updated studio subcommand description to mention --remote and the D1 limitation - docs/reference/cli.md: fully documents --remote for both execute and studio (dialects, confirmation prompt, 404 case, D1 limitation) - Tests: db-studio-remote.test.ts covering PG remote happy path, 404, non-404 re-throw, auth/project errors, D1 remote rejection, and local regression guards (9 tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Type `mockConfirm` with its actual parameter shape so the clack mock
wrapper can forward the argument without a variadic spread, and so that
`mock.calls[0][0]` resolves to `{ message: string }` instead of the
out-of-bounds `undefined` that the empty-tuple signature produced.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix cheat sheet: update `void db studio` row description; add missing `void db execute` row - Shorten studio one-liner: drop "D1 remote not yet supported" caveat (belongs in HELP_TREE and the error message) - Use backticks for \`void db execute --remote\` command reference in D1 studio error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove WITH from isSafeStatement safe-list (writable CTEs are indistinguishable without a real parser; fail-closed by prompting for all WITH queries) - Reject multi-statement SQL: strip trailing semicolon then treat any remaining internal `;` as unsafe - Reject `void db export --remote` with an actionable error before it silently falls through to the local export path - Update WITH CTE test to expect the prompt to fire (semantic change: now unsafe) - Add db-args regression tests for export --remote rejection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
✅ Deploy Preview for staging-void-cloud ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d0bfa8368c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ANALYZE as safe - `isSafeStatement`: treat `EXPLAIN ANALYZE` as destructive — in PostgreSQL it actually executes the underlying DML to collect runtime statistics, so the previous `EXPLAIN` prefix match was a safety hole. Only plain `EXPLAIN` (without `ANALYZE`) is now allowed through. - D1 remote execute: when `getResources()` fails or returns no D1 resource, fall back to the binding name configured in `void.json` (`inference.bindings.db`) instead of always hardcoding `'DB'`. Projects with a custom D1 binding name no longer silently send the wrong binding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 276ed57. Configure here.
…iption The `void db execute` description was updated in this PR from 'Run SQL against the local database' to 'Run SQL against the database (local by default; …)'; update the integration test assertion to match the new wording. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forward-port the `confirmExisting` UX (prompt before overwriting an existing deployment) from main into this branch so the test file matches the expected behavior. Add `packages/void/test/unit/link.test.ts` with a `picocolors` no-op mock so that assertions on the `outro` message work on CI where `FORCE_COLOR` makes `pc.blue()` emit ANSI escape codes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
void/packages/void/src/cli/db-args.ts
Line 73 in 3b12973
Because remote is registered as a global parse option, commands that do not handle it now accept it silently; for example void db seed --remote --file seed.sql parses as a normal local seed and resets/seeds the local database, while the caller likely intended the deployed database. This is especially confusing because export --remote is explicitly rejected, but other unsupported db subcommands (push, reset, seed, etc.) ignore the flag instead of failing fast.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
# Conflicts: # packages/void/src/cli/prompt.ts # packages/void/test/unit/link.test.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4e9e698642
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…l fetch isSafeStatement now also scans the PostgreSQL parenthesized option list (EXPLAIN (ANALYZE) / EXPLAIN (ANALYZE TRUE) / EXPLAIN (ANALYZE FALSE)) for ANALYZE and treats those forms as destructive, alongside the existing bare EXPLAIN ANALYZE check. Fail-closed: even ANALYZE FALSE is rejected since we don't parse arbitrary option expressions. Extract fetchRemoteDatabaseUrl helper so runExecute and runStudio share a single getDatabaseUrl call site with the same 404-to-friendly-error path.
There was a problem hiding this comment.
💡 Codex Review
void/packages/void/src/cli/db-args.ts
Line 73 in acf7c5c
Adding remote to the shared parseArgs option set makes every void db subcommand accept it unless that branch explicitly rejects it; for example, void db reset --remote now parses successfully and proceeds through the local reset path, silently resetting the local database instead of reporting that remote reset is unsupported. Gate parsed.values.remote in the subcommands that don't implement remote behavior, similar to the explicit export --remote rejection.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
PostgreSQL treats /* */ and -- ... comments as whitespace before parsing, so EXPLAIN /*x*/ ANALYZE DELETE FROM users (or with a line comment, or a leading block comment) is equivalent to EXPLAIN ANALYZE and executes the DELETE. The previous regex required literal whitespace between EXPLAIN and ANALYZE, so these commented variants bypassed the destructive prompt. isSafeStatement now strips block and line comments before applying the existing keyword checks. Side benefit: leading comments on safe queries (e.g. -- header\nSELECT 1) are now correctly classified as safe, resolving the previously-documented UX nuisance. Naive comment stripping doesn't understand string literals; documented in the JSDoc but harmless for fail-closed classification.
* feat(api): add project_database_credentials table and helpers
Adds the project_database_credentials table (one row per project, PK on
project_id, ON DELETE CASCADE) to store encrypted database connection
strings. Includes the drizzle migration (0019), an updated snapshot, and
three db.ts helpers: upsertProjectDatabaseUrl, getProjectDatabaseUrl, and
deleteProjectDatabaseUrl.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(api): align project_database_credentials helpers with peer pattern
- `upsertProjectDatabaseUrl`: add `.returning()` and return
`Promise<ProjectDatabaseCredentials>` instead of `Promise<void>`,
matching `upsertProjectSecret`.
- `getProjectDatabaseUrl`: narrow return type from an inline object
literal to `Promise<ProjectDatabaseCredentials | null>`, matching
`getProjectSecret`.
- Import `ProjectDatabaseCredentials` from `./types.ts`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(api): encrypt+persist database URL at set-url time, expose GET endpoint
- POST /:id/database and PUT /:id/database-url now encrypt the connection
string with AES-GCM (using encryptProjectSecret) and upsert it into
project_database_credentials before calling Cloudflare Hyperdrive, so
the URL is never unrecoverable if Hyperdrive succeeds but DB write fails.
- New GET /:id/database-url decrypts and returns { url } to the project
owner; returns 404 with database_url_not_stored error when no row exists.
- Integration tests cover: round-trip via POST and PUT, last-write-wins
upsert, ownership 403, and structured 404.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(api): return 403 (not 404) on database-url ownership mismatch
Split the combined `!project || wrong-owner` guard in GET /:id/database-url
into two separate checks — 404 when the project does not exist, 403 when it
exists but belongs to a different user. Matches the pattern used by all other
ownership-gated endpoints in projects.ts (e.g. /resources, /logs).
Update the corresponding test assertion from 404 to 403.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump framework (getDatabaseUrl + db --remote args)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump framework (db execute --remote)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump framework (db execute --remote test coverage)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump framework (db execute --remote ordering fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump framework (db studio --remote + docs)
Points framework submodule at de247c3 which adds --remote support to
`void db studio` (PG only), updates help.ts and db-args.ts descriptions,
adds full docs in reference/cli.md, and includes 9 new unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump framework (test type fixes)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump framework (db --remote polish)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(api): persist database URL only after Cloudflare accepts it
CF is the source of truth for production routing; the stored credential
is the URL CF accepted and is now serving. Previously both POST
/:id/database and PUT /:id/database-url wrote to project_database_credentials
before calling Cloudflare, so a CF rejection would leave a stale row
that GET /:id/database-url (used by the CLI for remote access) would
serve — diverging the CLI from what production Hyperdrive actually routes.
The fix calls CF first and only persists on success. If the upsert fails
after CF succeeds (rare), a 500 is returned instructing the user to
re-run `void db set-url`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump framework (remote-db safety hardening)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(api): update orphaned Hyperdrive before persisting URL in isAlreadyExistsError paths
In both POST /:id/database and PUT /:id/database-url, the isAlreadyExistsError
recovery branch retrieved an existing Hyperdrive ID (via getHyperdriveByName) but
never pushed the submitted connectionString to CF before storing it locally. This
could cause GET /:id/database-url to return a URL that diverged from what CF was
actually routing.
Fix: call updateHyperdrive with the found ID and submitted connectionString before
falling through to the persist block, ensuring CF and the stored credential stay in
sync. Also add updateHyperdrive to the POST handler's dynamic import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style(api): apply vp fmt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump framework to main (db --remote merged via #213)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

Summary
void db execute --remote <sql>for both dialects: D1 routes through the platform proxy (POST /d1/query, JWT-authed); PG fetches the stored connection string via the newGET /:id/database-urlendpoint and connects directly withpg.Client.void db studio --remotefor PG only (opens Drizzle Studio against the deployed Postgres). D1 errors out with a pointer todb execute --remote.PlatformClient.getDatabaseUrl(projectId).--remoteis never the default.docs/reference/cli.mdupdated for both subcommands.Safety hardening
isSafeStatementfails closed:WITHremoved from the safe-list (writable CTEs are indistinguishable from read-only without a parser); multi-statement SQL (SELECT 1; DELETE ...) is treated as unsafe.void db export --remotenow errors immediately instead of silently dumping local data.--filerejection fires BEFORE the destructive-statement prompt.Test plan
pnpm test:unit packages/void/test/unit/db-execute-remote.test.ts packages/void/test/unit/db-studio-remote.test.ts packages/void/test/unit/db-args.test.ts packages/void/test/unit/get-database-url.test.ts— 69/69 passpnpm -C packages/void exec tsgo --noEmit— cleanvoid db execute --remote 'SELECT 1',void db studio --remote,void db execute --remote --file dump.sql→ rejection,void db export --remote→ rejection)Companion PR
Platform: voidzero-dev/void-platform#170 — adds the
GET /:id/database-urlendpoint and persists the connection string at provision time. This framework PR depends on those API changes.Note
High Risk
Runs arbitrary SQL against production (D1 via proxy, PG via fetched connection strings) with only a heuristic safety prompt in TTY; non-TTY/CI can execute destructive statements without confirmation.
Overview
Adds
--remotetovoid db executeandvoid db studioso you can target the deployed database instead of only local dev.For
void db execute --remote, D1 projects send SQL through the Void proxy (POST …/d1/query) with your auth token and an auto-resolved D1 binding (platform resources, thenvoid.jsoninference, thenDB). PostgreSQL projects call the newPlatformClient.getDatabaseUrl(GET …/database-url) and run the query with a directpgconnection; a missing stored URL surfaces avoid db set-urlhint on 404.void db studio --remoteis PostgreSQL-only: it loads the remote URL from the platform and launches Drizzle Studio. Remote D1 Studio is explicitly unsupported (error points users atdb execute --remote).Safety and UX: a fail-closed
isSafeStatementheuristic skips the TTY confirmation for plainSELECT/ non-ANALYZEEXPLAIN, but still prompts (or blocks cancel) for destructive SQL in interactive terminals; CI/non-TTY runs proceed without a prompt.void db export --remotenow errors instead of silently exporting local data; D1--file+--remoteis rejected before file read or confirmation.CLI parsing, help tree, cheat sheet in
docs/reference/cli.md, and unit/integration tests are updated accordingly (depends on platformGET /:id/database-urlfrom the companion PR).Reviewed by Cursor Bugbot for commit ad6ffbf. Bugbot is set up for automated code reviews on this repo. Configure here.