Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
76c39ab
feat(appkit): add appkit db migrate
ditadi Oct 12, 2026
9ad0388
fix(appkit): let appkit db migrate recover from a failed migration
ditadi Oct 19, 2026
ef0c399
refactor(appkit): resolve the .env branch kind once and share the bra…
ditadi Oct 19, 2026
96b9381
feat(appkit): warn when appkit db migrate writes a migration that bre…
ditadi Oct 19, 2026
0d1dfaf
refactor(appkit): run the database tooling on the modular workspace c…
ditadi Oct 20, 2026
c2f1db9
Merge branch 'readiness/stack667-02' into readiness/stack667-03
ditadi Oct 30, 2026
fb984c3
refactor(appkit): move database tooling to @databricks/appkit/beta/da…
ditadi Oct 30, 2026
5f2b040
fix(appkit): bind session branches to the developer
ditadi Oct 30, 2026
1f2490e
fix(appkit): refuse writes when databricks.yml binds a branch it does…
ditadi Oct 30, 2026
5f07267
fix(appkit): give one command at a time the checkout's migration history
ditadi Oct 30, 2026
3480221
docs(appkit): describe appkit db migrate guards and the history lock
ditadi Oct 30, 2026
e5a1ac4
fix(appkit): count variable-overrides.json branches as deploy targets
ditadi Oct 30, 2026
8960476
fix(appkit): refuse include patterns the deploy boundary cannot match
ditadi Oct 30, 2026
ed02075
fix(appkit): give the busy history lock message its recovery step
ditadi Oct 30, 2026
6f5eeb8
chore(appkit): acknowledge db tooling bundle size growth
ditadi Oct 30, 2026
99df0e4
fix(lint): keep the modular SDK import ban in the appkit overrides
ditadi Nov 9, 2026
a8e0420
test(appkit): a NOT NULL column added without a default breaks the pr…
ditadi Nov 9, 2026
2935385
fix(appkit): flag a NOT NULL column added without a default as breaking
ditadi Nov 9, 2026
7ba6f5b
test(appkit): a discard killed mid-way never leaves a journal naming …
ditadi Nov 9, 2026
c63ced7
fix(appkit): put the journal back before deleting a discarded migrati…
ditadi Nov 9, 2026
bf9a8ec
test: a migrate that fails after writing a migration still reports wh…
ditadi Nov 9, 2026
ce46a8a
fix: report what a kept migration breaks when migrate fails after wri…
ditadi Nov 9, 2026
e268364
test(appkit): a variable reference in place of an app's resources is …
ditadi Nov 9, 2026
2e6fb2f
fix(appkit): treat a variable reference in place of an app's resource…
ditadi Nov 9, 2026
e6d5e5c
fix(appkit): keep UnappliedMigrationError internal to the migrate too…
ditadi Nov 9, 2026
31aeb80
Merge branch 'fix/stack667-02' into fix/stack667-03
ditadi Nov 9, 2026
7e13884
test(appkit): appkit db migrate must not apply when tables are left i…
ditadi Nov 9, 2026
d665018
fix(appkit): appkit db migrate refuses to apply when tables are left …
ditadi Nov 9, 2026
7c74e29
docs: say appkit db migrate also refuses when tables are left in public
ditadi Nov 9, 2026
46a543a
refactor(appkit): keep one binding per postgres resource in the bundl…
ditadi Nov 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ coverage

.databricks

# `appkit db` holds it while a command changes the migration history
**/config/database/.appkit-db.lock

.claude/scheduled_tasks.lock
internal

Expand Down
44 changes: 44 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,50 @@
"rules": {
"no-restricted-imports": "off"
}
},
{
"files": ["packages/appkit/src/**"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["@databricks/sdk-*", "@databricks/sdk-*/**"],
"message": "Import the Databricks SDK only through the wrapper in packages/shared/src/workspace-client (modular.ts builds the modular @databricks/sdk-* clients). Add a re-export there if you need a new symbol."
},
{
"group": ["drizzle-kit", "drizzle-kit/**"],
"message": "drizzle-kit is an optional dev dependency that deployed apps do not install. Only packages/appkit/src/database/tooling may import it."
},
{
"group": [
"**/tooling",
"**/tooling/**",
"@databricks/appkit/beta/database-tooling"
],
"message": "The database tooling may import drizzle-kit, so only the CLI loads it. Runtime code must not import it."
}
]
}
]
}
},
{
"files": ["packages/appkit/src/database/tooling/**"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["@databricks/sdk-*", "@databricks/sdk-*/**"],
"message": "Import the Databricks SDK only through the wrapper in packages/shared/src/workspace-client (modular.ts builds the modular @databricks/sdk-* clients). Add a re-export there if you need a new symbol."
}
]
}
]
}
}
],
"ignorePatterns": [
Expand Down
80 changes: 49 additions & 31 deletions bundle-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@
{
"name": "@databricks/appkit",
"tarball": {
"packed": 1297373,
"unpacked": 4453717
"packed": 1364290,
"unpacked": 4666959
},
"dist": {
"total": {
"raw": 4438327,
"gzip": 1520942
"raw": 4651188,
"gzip": 1599394
},
"js": {
"raw": 1320443,
"gzip": 468638
"raw": 1391995,
"gzip": 496182
},
"types": {
"raw": 474763,
"gzip": 172113
"raw": 478827,
"gzip": 174184
},
"maps": {
"raw": 2632326,
"gzip": 876373
"raw": 2769571,
"gzip": 925210
},
"css": {
"raw": 0,
Expand All @@ -31,22 +31,22 @@
"raw": 10795,
"gzip": 3818
},
"fileCount": 895
"fileCount": 935
},
"entries": [
{
"id": ".",
"gzip": 111090,
"composition": {
"initialGzip": 108517,
"lazyGzip": 2573,
"initialGzip": 108516,
"lazyGzip": 2574,
"totalGzip": 111090,
"own": 351738,
"nodeModules": null,
"chunks": [
{
"label": "index.js",
"gzip": 103847,
"gzip": 103846,
"kind": "initial"
},
{
Expand All @@ -56,25 +56,25 @@
},
{
"label": "remote-tunnel-manager.js",
"gzip": 2573,
"gzip": 2574,
"kind": "lazy"
}
]
}
},
{
"id": "./beta",
"gzip": 105499,
"gzip": 110783,
"composition": {
"initialGzip": 105014,
"initialGzip": 110298,
"lazyGzip": 485,
"totalGzip": 105499,
"own": 316145,
"totalGzip": 110783,
"own": 331023,
"nodeModules": null,
"chunks": [
{
"label": "beta.js",
"gzip": 84463,
"gzip": 89747,
"kind": "initial"
},
{
Expand Down Expand Up @@ -125,13 +125,31 @@
]
}
},
{
"id": "./beta/database-tooling",
"gzip": 28557,
"composition": {
"initialGzip": 28557,
"lazyGzip": 0,
"totalGzip": 28557,
"own": 84002,
"nodeModules": null,
"chunks": [
{
"label": "index.js",
"gzip": 28557,
"kind": "initial"
}
]
}
},
{
"id": "./testing",
"gzip": 79051,
"gzip": 79050,
"composition": {
"initialGzip": 45993,
"lazyGzip": 33058,
"totalGzip": 79051,
"initialGzip": 45995,
"lazyGzip": 33055,
"totalGzip": 79050,
"own": 228781,
"nodeModules": null,
"chunks": [
Expand All @@ -142,7 +160,7 @@
},
{
"label": "index.js",
"gzip": 10286,
"gzip": 10288,
"kind": "initial"
},
{
Expand All @@ -152,12 +170,12 @@
},
{
"label": "index.js",
"gzip": 28625,
"gzip": 28624,
"kind": "lazy"
},
{
"label": "remote-tunnel-manager.js",
"gzip": 2587,
"gzip": 2585,
"kind": "lazy"
},
{
Expand Down Expand Up @@ -188,17 +206,17 @@
},
{
"id": "./type-generator",
"gzip": 26634,
"gzip": 26635,
"composition": {
"initialGzip": 26634,
"initialGzip": 26635,
"lazyGzip": 0,
"totalGzip": 26634,
"totalGzip": 26635,
"own": 76220,
"nodeModules": null,
"chunks": [
{
"label": "index.js",
"gzip": 26634,
"gzip": 26635,
"kind": "initial"
}
]
Expand Down
66 changes: 63 additions & 3 deletions docs/docs/plugins/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ Earlier releases put tables in `public` by default. An app that created its
tables there fails setup after the upgrade, and the error says the tables are in
`public`. With `APPKIT_DB_MIGRATE_ON_START=true`, startup refuses before any
migration runs when `defineSchema()` was given no `schemaName`, none of the
declared tables exist in `app`, and some exist in `public`. Otherwise the first
declared tables exist in `app`, and some exist in `public`; `npx appkit db migrate`
refuses to apply on a dev branch in the same case. Otherwise the first
migration would create empty copies in `app`, the check would pass, and the app
would serve the empty tables. Keep the tables in `public` with:

Expand All @@ -114,13 +115,72 @@ export const schema = defineSchema(

To use `app` instead, move the tables first
(`ALTER TABLE public.notes SET SCHEMA app`). When the tables in `public` belong
to another app, pass `{ schemaName: "app" }` explicitly and startup does not
refuse.
to another app, pass `{ schemaName: "app" }` explicitly and neither startup nor
`migrate` refuses.

Migrations applied on start into `public` also need
`GRANT CREATE ON SCHEMA public` to the app's service principal, because it does
not own `public`.

## Generate and apply migrations locally

:::note
`appkit db` is hidden from `appkit --help` while the database development flow is
in beta. Its logic ships in `@databricks/appkit/beta/database-tooling`, which
only the AppKit CLI of the same release imports. It has no semver guarantee.
:::

`npx appkit db migrate` compares `config/database/schema.ts` with the latest
committed snapshot. When they differ, it writes
`config/database/migrations/NNNN_migration.sql`. It regenerates
`shared/appkit-types/database.d.ts`, then applies pending migrations to the
branch `.env` names. Commit the migrations with `schema.ts`.

`migrate` applies migrations only to your dev branch or to this checkout's
session branch. A session branch name includes a hash of the user who created
it, so a `.env` copied from another developer does not make their session branch
yours.

### Branches `databricks.yml` deploys to

`migrate` never applies migrations to a branch that `databricks.yml` deploys to.
It reads every target, every `postgres` resource of every app, the
`postgres_branch` variable, and the files that `include:` names. It also reads
the variable values `databricks bundle` keeps for a target in
`.databricks/bundle/<target>/variable-overrides.json`. A branch named there
counts as well as the one `databricks.yml` names, because a deploy from this
checkout uses the file's value.

If a branch or database reference has no value in those files, `migrate` cannot
tell which branch the reference names. Examples are a target that leaves
`postgres_branch` unset and `${workspace.current_user.short_name}`. The same
holds for a `${var.…}` reference in place of an app's `resources` list, one of
its entries, or a `postgres` block, because the files do not say what it binds.
While any such reference exists, `migrate` applies migrations to no branch. The message names
the target and where the reference is. To continue, give the reference a value
in `databricks.yml`, or in the `variable-overrides.json` the message names. A
`variable-overrides.json` that is not a JSON object stops `migrate` too.

The check reads only this checkout's files. It does not see values passed at
deploy time with `--var` or `BUNDLE_VAR_*`, or resources defined in Python. It
protects branches from the CLI, and does not replace Lakebase permissions or
protected branches.

### One command at a time

While `migrate` runs, it holds `config/database/.appkit-db.lock`. A second
`appkit db` command in the same checkout fails and names the process that holds
the lock. The lock goes when the command finishes or exits.

A command stopped by a signal, such as SIGKILL, can leave the file behind. The
next command then names the holder. When that process no longer runs, the
message says the holder stopped. When its pid runs on this machine, the message
says to wait for it. A pid can also belong to an unrelated process, for example
after a reboot reused it. In either case, if no `appkit db` command runs in the
checkout, delete `config/database/.appkit-db.lock` and run the command again.
AppKit never deletes a lock it does not hold. Scaffolded apps list the file in
`.gitignore`.

## Schema discovery and overrides

`database()` and `database({})` use the same defaults. During setup, the plugin
Expand Down
3 changes: 2 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"@databricks/sdk-statementexecution",
"@databricks/sdk-warehouses",
"@mlflow/core",
"autoevals"
"autoevals",
"drizzle-kit"
]
},
"packages/appkit-ui": {
Expand Down
11 changes: 11 additions & 0 deletions packages/appkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
"development": "./src/type-generator/index.ts",
"default": "./dist/type-generator/index.js"
},
"./beta/database-tooling": {
"types": "./dist/database/tooling/index.d.ts",
"development": "./src/database/tooling/index.ts",
"default": "./dist/database/tooling/index.js"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"development": "./src/testing/index.ts",
Expand Down Expand Up @@ -126,6 +131,7 @@
"peerDependencies": {
"@mlflow/core": "0.4.0",
"autoevals": "0.3.0",
"drizzle-kit": "0.31.10",
"vitest": ">=3"
},
"peerDependenciesMeta": {
Expand All @@ -135,6 +141,9 @@
"autoevals": {
"optional": true
},
"drizzle-kit": {
"optional": true
},
"vitest": {
"optional": true
}
Expand All @@ -149,6 +158,7 @@
"@types/ws": "8.18.1",
"@vitejs/plugin-react": "5.1.1",
"autoevals": "0.3.0",
"drizzle-kit": "0.31.10",
"vitest": "3.2.4"
},
"overrides": {
Expand All @@ -161,6 +171,7 @@
"./beta": "./dist/beta.js",
"./dist/shared/src/plugin": "./dist/shared/src/plugin.d.ts",
"./type-generator": "./dist/type-generator/index.js",
"./beta/database-tooling": "./dist/database/tooling/index.js",
"./testing": {
"types": "./dist/testing/index.d.ts",
"default": "./dist/testing/index.js"
Expand Down
2 changes: 1 addition & 1 deletion packages/appkit/src/connectors/lakebase/endpoint-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ async function compareHosts(
}

/** Read every host the endpoint serves, read-write and read-only. */
function endpointHosts(response: unknown): string[] {
export function endpointHosts(response: unknown): string[] {
if (!response || typeof response !== "object") return [];
const status = Reflect.get(response, "status");
if (!status || typeof status !== "object") return [];
Expand Down
Loading