Skip to content

feat(appkit): add appkit db migrate - #666

Draft
ditadi wants to merge 4 commits into
stack/database-dev-flow/02-default-app-schemafrom
stack/database-dev-flow/03-db-migrate
Draft

ditadi wants to merge 4 commits into
stack/database-dev-flow/02-default-app-schemafrom
stack/database-dev-flow/03-db-migrate

Conversation

@ditadi

@ditadi ditadi commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #655. The design is #677.

What did you change, and why?

Why: #654 applies committed migrations when the deployed app starts. Something has to write those migrations and try them first. A developer or an agent has to be able to do that in a loop: change schema.ts, run one command, read the error, try again, with no hand-editing of drizzle's files.

Change:

  • appkit db migrate generates the next migration from config/database/schema.ts with drizzle-kit, regenerates shared/appkit-types/database.d.ts, and applies pending migrations to the branch .env points at. The command stays hidden from --help until the last PR of the stack.
  • It applies only to a branch AppKit created for this developer (dev-…) or this checkout (session-…). It refuses the project's default branch, protected branches, another developer's branch, and every branch databricks.yml deploys to. On those it still generates, and says why nothing was applied. The guard parses the Lakebase responses it decides on, so a branch with no status fails instead of reading as "not default, not protected".
  • One function resolves the .env branch to its kind (default, protected, deploy target, dev, session, other), and one rule says which kinds take writes. The later commands in the stack read the same kind. .env counts as configured only with PGHOST, PGDATABASE and LAKEBASE_ENDPOINT, the three the pool needs.
  • When the generated migration drops, renames or retypes something, or makes a column NOT NULL, the command lists those statements and warns. Apps keeps instances of the previous release serving while the new one migrates on start, so those statements break them, and a code rollback cannot undo a migration. The warning says to deploy code that works with both schemas first.
  • When the migration this run wrote fails on the branch (for example, a NOT NULL column on a table with rows), the command removes it and restores the journal. Without that, the failed migration would run first on every later try, and a fix in schema.ts would stack behind it. The error names the SQLSTATE, the removed file, and what to change. If an earlier committed migration fails instead, the new one stays.
  • Before connecting, it checks that PGHOST is a host of LAKEBASE_ENDPOINT. A mismatch, a missing endpoint, or a failed lookup refuses without connecting. The runtime check in the Lakebase connector only warns and is unchanged.
  • drizzle-kit is an optional peer dependency pinned to 0.31.10. If it is missing, is another version, has no ./api export, or loads a different drizzle-orm from AppKit's, generating fails with the npm install command that fixes it.
  • The logic lives in @databricks/appkit/database-tooling, which the shared CLI loads lazily. It is internal to the CLI and has no semver guarantee. oxlint stops runtime code from importing drizzle-kit or the tooling in any form, including dynamic imports, so a deployed app never loads drizzle-kit.
  • shared cannot depend on appkit, so the CLI types the tooling with DatabaseTooling in tooling-contract.ts, and appkit's typecheck checks the real exports against it.
  • An ambiguous rename cannot be answered without a prompt, so outside a TTY it throws and writes nothing.
  • Safe to deploy alone: it adds a CLI command and changes nothing a deployed app runs.

How do you know it works?

Testing:

  • migrations.test.ts: migrations come from the real drizzle-kit and are read back by drizzle-orm's migrator. Renames refuse outside a TTY, a discarded migration restores the previous journal byte for byte, and only drops, renames, type changes and new NOT NULLs count as breaking the previous release.
  • migrate.test.ts: generate, apply, the reasons nothing was applied, a dropped column reported as breaking, and both failure cases: the generated migration failing, and an earlier one failing. The CLI's migrate.test.ts checks the printed warning.
  • migrate.pg.test.ts runs against Postgres when APPKIT_TEST_PG_URL is set. A NOT NULL column on a table with a row fails with 23502 and is removed. The fixed schema.ts then applies, and the row keeps its data. With the removal disabled, this test fails.
  • guard.test.ts, lakebase-api.test.ts and context.test.ts: each refused branch kind, responses without the fields the guard needs, a host of another branch, one endpoint lookup per connection, and a pool closed even when the command fails.
  • drizzle-kit-dependency.test.ts: the missing, mismatched and 1.x drizzle-kit cases, a different drizzle-orm, and the peer pin in package.json.
  • contract.test.ts: a field added to the contract, or a parameter narrowed in appkit, fails typecheck. I checked both by hand.
  • Not run against a live Lakebase branch in this revision.

@ditadi
ditadi added this pull request to stack #667 October 13, 2026 13:34
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit ⚠️ over budget

npm tarball (packed): 1.3 MB (+81 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 1.3 MB (+86 KB) 480 KB (+32 KB)
Type declarations 468 KB (+12 KB) 170 KB (+4.8 KB)
Source maps 2.6 MB (+165 KB) 894 KB (+57 KB)
Other 11 KB 3.7 KB
Total 4.4 MB (+263 KB) 1.5 MB (+94 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 106 KB (+3.9 KB) 2.5 KB (+1 B) 108 KB (+3.9 KB) external 343 KB (+12 KB)
./beta 107 KB (+8.9 KB) 483 B (-2 B) 108 KB (+8.9 KB) external 323 KB (+25 KB)
./database-tooling 26 KB 0 B 26 KB external 76 KB
./testing 45 KB (+3.1 KB) 32 KB (-2 B) 77 KB (+3.1 KB) external 223 KB (+8.7 KB)
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 26 KB (+2.8 KB) 0 B 26 KB (+2.8 KB) external 74 KB (+7.9 KB)

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 101 KB
. utils.js initial 4.6 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 87 KB
./beta modular.js initial 6.0 KB
./beta stream-manager.js initial 5.9 KB
./beta service-context.js initial 4.2 KB
./beta databricks.js initial 3.3 KB
./beta client.js initial 647 B
./beta index.js initial 20 B
./beta supervisor-api.js lazy 192 B
./beta databricks.js lazy 177 B
./beta index.js lazy 114 B
./database-tooling index.js initial 26 KB
./testing manifest.js initial 32 KB
./testing index.js initial 10 KB
./testing wide-event-emitter.js initial 2.9 KB
./testing index.js lazy 28 KB
./testing remote-tunnel-manager.js lazy 2.5 KB
./testing utils.js lazy 1.8 KB
./tsdown index.js initial 520 B
./type-generator index.js initial 26 KB

@databricks/appkit-ui

npm tarball (packed): 397 KB (+497 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 429 KB (+235 B) 147 KB (+108 B)
Type declarations 267 KB (+983 B) 98 KB (+225 B)
Source maps 853 KB (+314 B) 286 KB (+143 B)
CSS 16 KB 3.2 KB
Total 1.5 MB (+1.5 KB) 534 KB (+476 B)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 5.3 KB 49 KB 55 KB 208 KB 14 KB
./js/beta 2.1 KB 0 B 2.1 KB 0 B 5.0 KB
./react 433 KB (+12 B) 49 KB 481 KB (+12 B) 1.3 MB 177 KB (+41 B)
./react/beta 4.9 KB 0 B 4.9 KB 0 B 12 KB

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 5.2 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 2.1 KB
./react index.js initial 431 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 4.9 KB

⚠️ Over budget: a package's shipped tarball, or a browser entry's consumer bundle (deps included), grew by more than 5% (and >10 KB). This check will fail — reduce the size, or acknowledge the increase by updating bundle-size-baseline.json.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 39258084428 -R databricks/appkit -n appkit-template-0.88.1-pr.9eeaebc-stack-database-dev-flow-03-db-migrate-666 -D appkit-pr-666 \
  && unzip -o "appkit-pr-666/appkit-template-0.88.1-pr.9eeaebc-stack-database-dev-flow-03-db-migrate-666.zip" -d "appkit-pr-666" \
  && databricks apps init --template "appkit-pr-666"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

`appkit db migrate` generates a migration from config/database/schema.ts
with drizzle-kit, regenerates shared/appkit-types/database.d.ts, and
applies pending migrations to the branch .env points at. It applies only
to a branch created for this developer (dev-…) or this checkout
(session-…); on any other branch, including the project default and any
branch databricks.yml deploys to, it generates and says why nothing was
applied. It connects only after the Lakebase API confirms that PGHOST is
a host of LAKEBASE_ENDPOINT.

The logic lives in a new @databricks/appkit/database-tooling entry point
that the shared CLI loads lazily, so the runtime never imports drizzle-kit.
drizzle-kit is an optional peer dependency: apps that generate migrations
install it as a dev dependency, and the command says how when it is
missing. `appkit db` stays hidden from --help until the database plugin
is offered by `databricks apps init`.

Signed-off-by: ditadi <victordperd@gmail.com>
A generated migration that fails to apply is removed and the journal
restored, so a fix in schema.ts replaces it instead of stacking behind it.

Also: refuse a drizzle-kit other than the pinned one or one loading another
drizzle-orm, parse the branch and endpoint responses the guard decides on,
type the CLI against a contract appkit's typecheck enforces, and enforce
the drizzle-kit and tooling import boundary with oxlint.

Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi force-pushed the stack/database-dev-flow/03-db-migrate branch from 9f3a34f to 1827ebd Compare October 19, 2026 12:44
…nch pool

resolveEnvBranch returns the branch kind (default, protected, deploy
target, dev, session, other), and takesWrites is the one rule for which
kinds take writes. It needs PGHOST, PGDATABASE and LAKEBASE_ENDPOINT,
like the pool. withBranchPool opens and always closes the pool. The
drizzle-kit v7 file names live only in migrations/paths.ts.

Signed-off-by: ditadi <victordperd@gmail.com>
…aks the previous release

Apps keeps instances of the previous release serving while the new one
migrates on start, and a code rollback cannot undo a migration. When the
generated migration drops, renames, retypes or makes a column NOT NULL,
the report lists those statements and the CLI says to ship code that
works with both schemas first.

Signed-off-by: ditadi <victordperd@gmail.com>

This branch has not been deployed

No deployments
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