feat(db): migrations create, list, and apply - #136
Conversation
Adds `bunny db migrations` for running plain SQL migration files against a Bunny Database. Files live in `migrations/` (falling back to `drizzle/`) and are named `NNNN_<slug>.sql`; the filename is the migration's identity and its numeric prefix is the apply order. Applied migrations are recorded in `__bunny_migrations`, which existing introspection excludes already, so it stays out of `db studio` and the REST layer. Each file runs through `client.migrate()` together with its tracking row, so a migration either lands and is recorded or neither happens, and foreign keys stay deferred for table rebuilds. `list` reports applied, pending, modified, and missing state without creating the tracking table. `apply` stops at the first failure and confirms only when a TTY is attached. `splitStatements` now keeps `CREATE TRIGGER ... BEGIN ... END;` bodies intact, which `db shell <file>.sql` benefits from too. Credential resolution moves to `db/credentials.ts`, shared by shell, studio, and migrations apply instead of a third copy.
|
@codex review |
🦋 Changeset detectedLatest commit: 01b1c91 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Greptile SummaryAdds a database migration workflow and hardens shared database tooling.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/cli/src/commands/db/credentials.ts | Centralizes credential resolution, enforces encrypted URLs, and binds ambient or generated tokens to the expected endpoint. |
| packages/cli/src/commands/db/migrations/engine.ts | Implements migration discovery, checksums, history classification, SQL preparation, and atomic application with a tracking row. |
| packages/cli/src/commands/db/migrations/apply.ts | Orchestrates migration preflight, drift checks, confirmation, application, and structured reporting. |
| packages/database-shell/src/parser.ts | Expands SQL splitting to support trigger blocks, comments, SQLite quote forms, and explicit truncated-input errors. |
| packages/database-shell/src/shell.test.ts | Adds regression coverage for the previously reported trigger parsing and lexical edge cases. |
Sequence Diagram
sequenceDiagram
participant User
participant CLI as bunny db migrations
participant Credentials as Credential resolver
participant Parser as SQL splitter
participant DB as Bunny Database
User->>CLI: apply migration files
CLI->>Credentials: resolve URL and token
Credentials-->>CLI: encrypted endpoint-bound credentials
CLI->>DB: read migration history
CLI->>Parser: validate and split pending SQL
Parser-->>CLI: prepared statements
loop Each pending migration
CLI->>DB: atomic migrate(statements + tracking row)
DB-->>CLI: applied or rolled back
end
CLI-->>User: migration report
Reviews (9): Last reviewed commit: "Merge branch 'main' into db-migrations" | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e496926392
ℹ️ 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".
Parser: a trigger body statement ending in `CASE ... END;` was mistaken for the trigger's own terminator, shredding a valid trigger into three fragments. Nesting is now counted across `BEGIN` and `CASE` openers, with quoted strings and identifiers scrubbed first so a column named `end` doesn't skew the count. Credentials: an explicit database ID no longer falls through to `.env`, which could target a different database than the one named on the command line. A generated token is now bound to the resolved database's host, so `--url` without `--token` is refused on mismatch instead of sending a full-access token to an unverified endpoint, and the check runs before the token is created. Apply: `ensureMigrationsTable()` moved after the dry-run exit and the confirmation, so a preview or a declined run writes nothing. The failed migration is now counted as still pending, since its tracking row rolls back with it. Also wraps the pre-confirmation read in `readApplied()`, turning a bad URL or token into a hinted error instead of an unexpected-error exit.
Parser: block comments were not tokenized at all, so `/* END */` inside a trigger body counted as a structural closer, and more broadly a `;` or a quote inside any block comment split or corrupted the statement around it. Block comments are now skipped like `--` comments, which fixes both. Credentials: a hostname match let a plaintext URL receive a token. A token the user did not pass on the command line now requires an encrypted target, rejecting `http:`, `ws:`, and `libsql://host:port?tls=0`, which the libSQL client downgrades to plaintext. This covers the token read from `.env` as well as a generated one, since neither was paired with the URL by the user. The scheme check runs before any lookup or prompt, so an unusable URL fails immediately rather than after picking a database. An explicit `--token` alongside a plaintext `--url` is still allowed: that pairing is deliberate, and it covers a local sqld over http.
An encrypted `--url` on a foreign host still received the token from `.env`, because the plaintext guard added in the previous commit was the only check on that path. Last round I assumed host ownership couldn't be verified there without an API call, which was wrong: the `.env` URL is the pairing the user established, so comparing against it is a local check. The `.env` token is now reused only for a `--url` on the same host as the `.env` URL. Anything else falls through to the API path, where a fresh token is created and checked against the database's canonical URL, so the stored credential is never the one that travels. Comparing against `.env` rather than the API keeps the offline case working: both values in `.env` with `--url` naming the same host still needs no network call. Folding the encryption check into the same predicate removes the separate `.env`-specific error path; a plaintext override now falls through to the existing "must be encrypted" refusal.
| @@ -1,42 +1,114 @@ | |||
| /** Statements whose body is a `BEGIN ... END` block, so inner semicolons don't terminate them. */ | |||
| const BLOCK_BODY_START = /^CREATE\s+(?:TEMP\s+|TEMPORARY\s+)?TRIGGER\b/i; | |||
There was a problem hiding this comment.
When a migration or SQL file contains CREATE TRIGGER IF NOT EXISTS ... BEGIN ...; END;, trigger-body detection does not preserve the body, so the first inner semicolon creates malformed fragments that fail in client.migrate or client.batch.
Knowledge Base Used: Database Shell
Docs-only conflicts in AGENTS.md and README.md: keep main's content additions (sandbox stat, openapi-client subpath entrypoints, agent-skill core module, LICENSE in the published files list, skills command examples) in this branch's punctuation style.
Adds
bunny db migrationsfor running plain SQL migration files against a Bunny Database.