Add to an existing project
Import the live schema, apply a reviewed baseline through chkit’s normal migration journal, then make a separate schema change. pull alone does not establish migration history. This walkthrough preserves an existing table, its rows, and an ordinary view.
You need Bun 1.3.5+, an existing TypeScript project, and a ClickHouse HTTP endpoint. Run commands from the project root. Commit or otherwise save your existing application before starting; these steps add database tooling alongside it.
1. Choose a database explicitly
Section titled “1. Choose a database explicitly”Obtain the endpoint, username, password, and database from whoever operates your database. Export the supplied values into your shell; do not assume default, an empty password, or a local endpoint when credentials are missing. If you want a disposable database to practice with, use the fixture below instead.
Optional: create a populated local practice database
This fixture needs Docker and uses its own port, user, and database. The password is only for this disposable local instance.
docker run -d --name chkit-guide-adopt \ -p 127.0.0.1:18123:8123 \ -e CLICKHOUSE_USER=guide \ -e CLICKHOUSE_PASSWORD=guide-local-only \ -e CLICKHOUSE_DB=analytics \ -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \ clickhouse/clickhouse-server:26.3
export CLICKHOUSE_URL=http://localhost:18123export CLICKHOUSE_USER=guideexport CLICKHOUSE_PASSWORD=guide-local-onlyexport CLICKHOUSE_DB=analytics
# Wait until the server is ready before creating the fixture.until curl --fail --silent "$CLICKHOUSE_URL/ping"; do sleep 1; done
docker exec -i chkit-guide-adopt clickhouse-client \ --user guide --password guide-local-only --multiquery <<'SQL'CREATE TABLE analytics.events ( id UInt64, user_id UInt64, event_type String, occurred_at DateTime64(3), properties String DEFAULT '{}') ENGINE = MergeTree ORDER BY id;INSERT INTO analytics.eventsSELECT number + 1, number % 17, if(number % 2 = 0, 'PageView', 'Signup'), toDateTime64('2026-01-01 00:00:00', 3) + toIntervalSecond(number), concat('{"n":', toString(number), '}')FROM numbers(1000);CREATE VIEW analytics.event_counts ASSELECT event_type, count() AS total FROM analytics.events GROUP BY event_type;SQLFor an empty practice project, first run mkdir chkit-adoption && cd chkit-adoption && bun init -y.
2. Install and configure
Section titled “2. Install and configure”bun add --dev chkit @chkit/core @chkit/plugin-pullmkdir -p src/db/schemaCreate clickhouse.config.ts. Use the same explicit database for the connection and the pull filter. The schema files produced by pull also name this database; changing only the connection database does not retarget those definitions.
import { defineConfig } from '@chkit/core'import { pull } from '@chkit/plugin-pull'
function required(name: string): string { const value = process.env[name] if (!value) throw new Error(`Set ${name} to the supplied connection value`) return value}
const database = required('CLICKHOUSE_DB')const password = process.env.CLICKHOUSE_PASSWORDif (password === undefined) throw new Error('Set CLICKHOUSE_PASSWORD (explicitly empty if supplied that way)')
export default defineConfig({ schema: './src/db/schema/**/*.ts', outDir: './chkit', clickhouse: { url: required('CLICKHOUSE_URL'), username: required('CLICKHOUSE_USER'), password, database, }, plugins: [pull({ databases: [database], outFile: './src/db/schema/pulled.ts' })],})Exported shell variables work with Bun and Node. If your project stores credentials in .env, ensure your chosen runner loads that file; do not commit it. Commit the dependency lockfile.
The database user needs metadata reads for introspection, permission to create/read/write chkit’s _chkit_migrations journal in the target database, and privileges for the migrations you intend to apply. SELECT on application tables alone is insufficient: introspection also reads system.tables, system.columns, system.data_skipping_indices, system.projections, and system.dictionaries. Ask your administrator to scope visibility to the intended database. CREATE DATABASE is unnecessary when adopting a pre-existing database and the reviewed SQL does not provision one.
3. Record the existing state and pull it
Section titled “3. Record the existing state and pull it”For the practice fixture, save the original rows, object identities, and view results. For your own database, adapt the columns and object names and use your application’s preservation checks.
mkdir -p .adoption-checksbunx chkit query "SELECT id, user_id, event_type, occurred_at, properties FROM analytics.events ORDER BY id" --json > .adoption-checks/rows-before.jsonbunx chkit query "SELECT name, uuid, create_table_query FROM system.tables WHERE database = 'analytics' AND name IN ('events', 'event_counts') ORDER BY name" --json > .adoption-checks/objects-before.jsonbunx chkit query "SELECT * FROM analytics.event_counts ORDER BY event_type" --json > .adoption-checks/view-before.jsonbunx chkit pullInspect src/db/schema/pulled.ts: the table columns, defaults, engine, keys, and view query must match the live objects. Remove any starter definitions that would duplicate the imported objects. Pull imports schema, not application data. Avoid committing exports of sensitive rows from a real database.
4. Establish a reviewed baseline
Section titled “4. Establish a reviewed baseline”bunx chkit generate --name baselineOpen the emitted chkit/migrations/*_baseline.sql. For this fixture it should create the database, table, and view using IF NOT EXISTS, with no DROP, TRUNCATE, or INSERT statements. If the database already exists and your user has no provisioning privilege, remove the CREATE DATABASE IF NOT EXISTS statement from this unapplied file before proceeding. Keep the table/view definitions so the baseline can bootstrap the same schema in another pre-provisioned empty database.
IF NOT EXISTS leaves an existing object untouched; it does not verify that object’s definition. That is why you reviewed pull’s output and must run live checks after application. Do not replace this step with a fabricated journal entry or a snapshot rebuild.
bunx chkit migrate --json # inspect the pending baselinebunx chkit migrate --applybunx chkit status --jsonbunx chkit drift --jsonbunx chkit check --jsonbunx chkit generate --dryrun --jsonExpect one applied migration, no pending migration, no drift, a passing check, and zero generated operations. chkit records the baseline checksum in analytics._chkit_migrations; no journal file belongs in git.
Re-run the three preservation queries, writing rows-after.json, objects-after.json, and view-after.json under .adoption-checks. Compare the result rows, excluding query timing/statistics:
bun -e 'for (const name of ["rows", "objects", "view"]) { const before = await Bun.file(`.adoption-checks/${name}-before.json`).json() const after = await Bun.file(`.adoption-checks/${name}-after.json`).json() if (JSON.stringify(before.data) !== JSON.stringify(after.data)) throw new Error(`${name} changed`)}console.log("Baseline preserved all rows, object identities/definitions, and view results")'5. Ship the first actual change
Section titled “5. Ship the first actual change”In the pulled events table’s columns array, add:
{ name: 'ingest_source', type: 'String', default: 'legacy' },Generate a new migration and review it:
bunx chkit generate --name add_ingest_sourcebunx chkit migrate --jsonThe change should be an ALTER TABLE analytics.events ADD COLUMN ... DEFAULT 'legacy'; it must not recreate the table. Apply only after confirming that plan:
bunx chkit migrate --applybunx chkit status --jsonbunx chkit drift --jsonbunx chkit check --jsonbunx chkit generate --dryrun --jsonbunx chkit query "SELECT count() AS rows, uniqExact(id) AS ids, countIf(ingest_source = 'legacy') AS legacy_rows FROM analytics.events" --jsonFor the fixture all three counts are 1000; status now reports two applied migrations and no pending migrations. Repeat the original-column rows query and view query and compare their .data with the saved originals. The table’s UUID and the view’s UUID/query must remain unchanged; the table’s own CREATE query now includes the added column.
Run your existing application’s tests. Commit the maintained schema, both reviewed migration files, chkit/meta/snapshot.json, config, and lockfile together. Never edit an applied migration; add another migration for the next change.
Next steps
Section titled “Next steps”- The migration workflow explains snapshots, checksums, and deployment across environments.
- Zero-downtime structural migrations changes sorting/partitioning of a populated table with a verified atomic cutover while writers keep inserting.
- Pull plugin covers filters, supported objects, and credential redaction.
- For AI agents covers autonomous setup and missing credentials.
When finished with the optional local fixture, remove only its container: docker rm -f chkit-guide-adopt.