Skip to content

feat(appkit): add appkit db init, clean and seed - #672

Draft
ditadi wants to merge 10 commits into
stack/database-dev-flow/04-db-introspectfrom
stack/database-dev-flow/05-db-init
Draft

ditadi wants to merge 10 commits into
stack/database-dev-flow/04-db-introspectfrom
stack/database-dev-flow/05-db-init

Conversation

@ditadi

@ditadi ditadi commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

🥞 Stacked PR (generated by Stack)

Use this link to review incremental changes.


Stacked on #670. The design is #677.

What did you change, and why?

Each checkout needs its own Lakebase branch for local development, set up without prompts so an agent or a script can run it. This PR adds appkit db init, appkit db clean and appkit db seed.

appkit db init uses your dev-<you>-<hash> branch, or with --session a session-<user hash>-<id> branch that expires after 7 days (--ttl changes that). It forks a new branch from the project's default branch. It makes sure the branch has a read-write endpoint that suspends after 5 idle minutes and your Postgres role with DATABRICKS_SUPERUSER, and points .env at the branch. It hands you the app schema when the fork copied it owned by another Lakebase role, such as the app's service principal.

Then init starts the schema. The default is migrate with schema.ts and introspect without it, and --schema clean and --schema none are the other starts. When migrate or introspect needs drizzle-kit and the app lacks it, init installs the pinned version first. init refuses the project's default branch, protected branches and any branch the deploy boundary covers, before it changes anything.

clean drops the app schema, migration journal included, in one transaction. On public it drops only what your role owns. clean --delete-branch deletes this checkout's session branch. seed runs config/database/seed.sql in one transaction. clean and seed run only on your dev or session branch. .env is written with mode 0600 through a temporary file, and a symlinked .env is refused.

This revision changes these behaviors:

  • init checks its planned branch against the whole deploy boundary from feat(appkit): add appkit db migrate #666. A databricks.yml reference the checkout's files cannot resolve refuses init before any API call or .env write.
  • init holds one history lock for its whole run, across the clean, migrate or introspect it starts. clean holds the lock too. clean, and init with --schema migrate or --schema clean, refuse a missing schema.ts before taking the lock, so a refused run leaves no empty config/database.
  • init --schema none reads no schema.ts, takes no lock, and hands off nothing. The schema and its owner stay as the fork copied them.
  • Schema public is never handed off. It belongs to the database, so AppKit never deletes its owner or reassigns its objects.
  • bundle-size-baseline.json is regenerated. The packed tarball grows from 1,364,290 to 1,448,119 bytes (+6.14%), over the 5% budget, because ./beta/database-tooling grows from 28 KB to 45 KB gzipped.

Session branch names carry a hash of your user name (#666). init treats .env as this checkout's session branch only when the name matches your hash, so a .env copied from another developer never leads init to their branch. appkit db stays hidden from appkit --help.

How do you know it works?

  • init.test.ts runs runInit against an in-memory Lakebase. It covers branch creation and reuse, session TTLs, a 403, a 409, the branch limit, and deploy-target and unresolved refusals before any API call. It also covers the hand-off and its failures, --schema none, public, and one lock across init's clean and migrate.
  • env-file.test.ts, clean-seed.test.ts and the CLI's init-clean-seed.test.ts cover the .env writer, the clean and seed refusals, the confirmations and the exit codes.
  • clean.pg.test.ts and hand-off.pg.test.ts run against Postgres. They check what CASCADE takes outside the app schema, a public clean, and the objects and grants that block a role delete.
  • At this PR's head, the database, plugin and CLI db tests passed against a local PostgreSQL 14, typecheck passed, and a clean build matched the new size baseline. The full vitest suite passed at an earlier revision of this PR and at the top of the stack (feat(appkit): add appkit db check #678). The packed tarballs export the same root and ./beta names as main, and ./beta/database-tooling adds only init, clean, seed and session-branch deletion. CI runs the full suite on this PR.
  • This revision was not run against a real Lakebase workspace, so who owns public on a real Lakebase branch was not observed.

`appkit db init` gives a checkout its own Lakebase branch, forked from the
project's default branch: your dev branch, or with --session a branch for
this checkout that expires after 7 days (or --ttl). It asks nothing. It
refuses the default branch, protected branches and branches databricks.yml
deploys to before any change, and when the project is at its branch limit it
lists your branches to delete.

It makes sure the branch has a read-write endpoint that suspends after 5 idle
minutes and a Postgres role for you, points .env at it with an atomic 0600
write, hands you the app schema when the fork copied it owned by another
Lakebase role, installs drizzle-kit when the app lacks it, then starts the
schema by migrating, introspecting or cleaning.

`appkit db clean` drops the app schema (only your objects in public) and
lists what outside it went along; --delete-branch deletes this checkout's
session branch instead. `appkit db seed` runs config/database/seed.sql in one
transaction with the app schema on the search_path. Both run only on a branch
created for you or this checkout.

Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi force-pushed the stack/database-dev-flow/05-db-init branch from c56de21 to 98ef27b Compare October 20, 2026 12:22
@github-actions

github-actions Bot commented Oct 20, 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 41109404809 -R databricks/appkit -n appkit-template-0.89.0-pr.aa5285d-stack-database-dev-flow-05-db-init-672 -D appkit-pr-672 \
  && unzip -o "appkit-pr-672/appkit-template-0.89.0-pr.aa5285d-stack-database-dev-flow-05-db-init-672.zip" -d "appkit-pr-672" \
  && databricks apps init --template "appkit-pr-672"

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.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle size report

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

@databricks/appkit

npm tarball (packed): 1.4 MB (+89 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 1.4 MB (+78 B) 515 KB (+4 B)
Type declarations 478 KB 175 KB (+2 B)
Source maps 2.8 MB (+317 B) 958 KB (+88 B)
Other 11 KB 3.7 KB
Total 4.7 MB (+395 B) 1.6 MB (+94 B)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 106 KB 2.5 KB 108 KB external 343 KB
./beta 108 KB 485 B 108 KB external 323 KB
./beta/database-tooling 45 KB (+16 B) 0 B 45 KB (+16 B) external 134 KB (+50 B)
./testing 45 KB 32 KB 77 KB external 223 KB
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 26 KB 0 B 26 KB external 74 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 88 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 178 B
./beta index.js lazy 115 B
./beta/database-tooling index.js initial 45 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 (+4 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 429 KB 147 KB
Type declarations 267 KB 98 KB (+1 B)
Source maps 853 KB 286 KB
CSS 16 KB 3.2 KB
Total 1.5 MB 534 KB (+1 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 49 KB 481 KB 1.3 MB 177 KB
./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

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

# Conflicts:
#	packages/appkit/src/database/tooling/migrate.ts
#	packages/appkit/src/database/tooling/naming.ts
#	packages/appkit/src/database/tooling/tests/guard.test.ts
#	packages/appkit/src/database/tooling/tests/naming.test.ts
#	packages/appkit/src/database/tooling/tests/targets.test.ts
init still checked its planned branch against the readDeployBranches list that
the deploy boundary replaced. It now asks deployRefusal, like every other
guard, so a binding the checkout's files cannot resolve refuses init before any
API call or .env write. readDeployTarget reads the default target from the same
bundle model: its first resolved app binding, else its postgres_branch and
postgres_database variables. The shared CLI test mocks the beta tooling path.

Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
clean drops the branch's migration journal, and init runs clean, migrate or
introspect after its own checks, so another appkit db command could run in
between. runClean now holds the checkout's history lock, and init holds one lock
for its whole run and hands it to cleanHolding, migrateHolding and
introspectHolding. init --schema none changes no history, so it takes no lock
and creates no config/database.

Signed-off-by: ditadi <victordperd@gmail.com>
init --schema none still handed the app schema to the developer, which deletes
the branch copy of the role that owns it, in apps that may have no database
schema contract at all. It now leaves the schema and its owner as the fork
copied them. Schema public is never handed off either: it belongs to the
database (pg_database_owner on PostgreSQL 15 and later, the bootstrap superuser
before), so AppKit never deletes its owner or reassigns its objects.

Signed-off-by: ditadi <victordperd@gmail.com>
Regenerated with pnpm clean && pnpm build && pnpm size:baseline. Against the
previous baseline the @databricks/appkit packed tarball grows from 1364290 to
1448119 bytes (+83829, +6.14%), over the 5% budget. The growth is the CLI-only
@databricks/appkit/beta/database-tooling entry (45 KB gzipped, from 28 KB) with
the introspect fixes and init, clean and history lock code it ships.
@databricks/appkit-ui is unchanged at 406741 bytes.

Signed-off-by: ditadi <victordperd@gmail.com>
The re-run test ran with --schema none, which skips the hand-off, so no test
reached the check that keeps init from deleting your own role. It now runs the
default migrate start and asserts that the owner query ran for schema app and
no role was deleted.

Signed-off-by: ditadi <victordperd@gmail.com>
init --schema none hands off no schema and starts none, but it still loaded
schema.ts for the schema name, so a half-edited file stopped a branch-only
init. It now loads schema.ts only when the hand-off and the start use it.

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

withHistoryLock creates config/database for its lock file, and init and clean
checked for schema.ts only after taking it, so a refused init or clean left an
empty config/database behind. Both now check first, as runMigrate does.

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