Skip to content

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.

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.

Terminal window
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:18123
export CLICKHOUSE_USER=guide
export CLICKHOUSE_PASSWORD=guide-local-only
export 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.events
SELECT 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 AS
SELECT event_type, count() AS total FROM analytics.events GROUP BY event_type;
SQL

For an empty practice project, first run mkdir chkit-adoption && cd chkit-adoption && bun init -y.

Terminal window
bun add --dev chkit @chkit/core @chkit/plugin-pull
mkdir -p src/db/schema

Create 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_PASSWORD
if (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.

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.

Terminal window
mkdir -p .adoption-checks
bunx chkit query "SELECT id, user_id, event_type, occurred_at, properties FROM analytics.events ORDER BY id" --json > .adoption-checks/rows-before.json
bunx 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.json
bunx chkit query "SELECT * FROM analytics.event_counts ORDER BY event_type" --json > .adoption-checks/view-before.json
bunx chkit pull

Inspect 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.

Terminal window
bunx chkit generate --name baseline

Open 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.

Terminal window
bunx chkit migrate --json # inspect the pending baseline
bunx chkit migrate --apply
bunx chkit status --json
bunx chkit drift --json
bunx chkit check --json
bunx chkit generate --dryrun --json

Expect 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:

Terminal window
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")
'

In the pulled events table’s columns array, add:

{ name: 'ingest_source', type: 'String', default: 'legacy' },

Generate a new migration and review it:

Terminal window
bunx chkit generate --name add_ingest_source
bunx chkit migrate --json

The change should be an ALTER TABLE analytics.events ADD COLUMN ... DEFAULT 'legacy'; it must not recreate the table. Apply only after confirming that plan:

Terminal window
bunx chkit migrate --apply
bunx chkit status --json
bunx chkit drift --json
bunx chkit check --json
bunx chkit generate --dryrun --json
bunx chkit query "SELECT count() AS rows, uniqExact(id) AS ids, countIf(ingest_source = 'legacy') AS legacy_rows FROM analytics.events" --json

For 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.

When finished with the optional local fixture, remove only its container: docker rm -f chkit-guide-adopt.