Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 50 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,56 @@
"rules": {
"no-restricted-imports": "off"
}
},
{
"files": ["packages/appkit/src/**"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"@databricks/sdk-experimental",
"@databricks/sdk-experimental/**"
],
"message": "Import the Databricks SDK only through the wrapper in packages/shared/src/workspace-client. 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-experimental",
"@databricks/sdk-experimental/**"
],
"message": "Import the Databricks SDK only through the wrapper in packages/shared/src/workspace-client. 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
57 changes: 57 additions & 0 deletions docs/docs/plugins/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,63 @@ 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}`. 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
Loading