<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>chkit Docs | Blog</title><description>Public documentation for chkit, the ClickHouse schema and migration CLI.</description><link>https://chkit.obsessiondb.com/</link><language>en</language><item><title>The era of OLAP deserves the tooling relational has had for years</title><link>https://chkit.obsessiondb.com/blog/olap-deserves-better-tooling/</link><guid isPermaLink="true">https://chkit.obsessiondb.com/blog/olap-deserves-better-tooling/</guid><description>If you build on Postgres or MySQL, you have not hand-written a migration in years. ClickHouse should not be the exception. Here is why we built ch-kit, after running ClickHouse at near-petabyte scale.

</description><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you build on Postgres or MySQL, you have not hand written a migration in years. You change a TypeScript schema, a tool diffs it, generates the SQL, applies it, and fails your CI if production has drifted. Drizzle, Prisma, and Flyway made that the default. It is boring, and boring is exactly what you want from the thing that changes your database.&lt;/p&gt;
&lt;p&gt;Now switch to ClickHouse®. Suddenly you are back in 2010: hand written DDL, diffs done by eye, a &lt;code dir=&quot;auto&quot;&gt;migrations/&lt;/code&gt; folder that may or may not match what is actually running, and a sinking feeling every time someone runs an &lt;code dir=&quot;auto&quot;&gt;ALTER&lt;/code&gt; in production. OLAP is becoming the default for analytics, but the tooling around it never caught up to what the relational world treats as table stakes.&lt;/p&gt;
&lt;p&gt;We felt this acutely. In our previous venture, Numia, we ran ClickHouse at near-petabyte scale for real-time blockchain analytics. We hit two walls. The first was scaling the database like serious infrastructure. The second was the complete absence of tooling to manage the schema as code. We built &lt;a href=&quot;https://obsessiondb.com&quot;&gt;ObsessionDB&lt;/a&gt;, managed ClickHouse with decoupled storage and compute, to solve the first. &lt;strong&gt;ch-kit&lt;/strong&gt; is how we are solving the second, and it is open source.&lt;/p&gt;
&lt;p&gt;This post is about the second wall, and what schema-as-code for ClickHouse actually looks like once you have it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem-with-hand-written-ddl&quot;&gt;The problem with hand written DDL&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A ClickHouse schema is not just a list of columns. A single &lt;code dir=&quot;auto&quot;&gt;MergeTree&lt;/code&gt; table carries an engine, a sorting key, a partitioning expression, TTL rules, skip indexes, projections, codecs, and table settings. Materialized views point at target tables. Some of those properties can be altered in place. Some cannot, and changing them means a drop and recreate, which on a large table is not a thing you discover by accident.&lt;/p&gt;
&lt;p&gt;When all of that lives in raw SQL files, three failures show up over and over:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Drift.&lt;/strong&gt; Someone runs a manual &lt;code dir=&quot;auto&quot;&gt;ALTER&lt;/code&gt; to fix an incident at 3am. The fix never makes it back into a migration file. Now your repository and your production database disagree, and nothing tells you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unsafe changes.&lt;/strong&gt; A migration drops a column. The reviewer did not notice. It runs in CI on a Friday. The data is gone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No source of truth.&lt;/strong&gt; The schema is whatever the sum of every migration file happens to produce. To know the current shape of a table, you read the database, not your code.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Relational tooling solved all three. ch-kit solves them for ClickHouse.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;define-your-schemas-in-typescript&quot;&gt;Define your schemas in TypeScript&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;In ch-kit, tables, views, and materialized views are TypeScript values. Here is a real table definition, with the features you actually use in production:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { schema, table } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@chkit/core&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;events&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;database: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;analytics&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;events&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;columns:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, type: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;UInt64&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;org_id&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, type: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;source&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, type: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;LowCardinality(String)&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;payload&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, type: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, nullable: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;received_at&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, type: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;DateTime64(3)&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, default: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;fn:now64(3)&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;engine: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;MergeTree()&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;primaryKey:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;orderBy:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;org_id&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;received_at&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;partitionBy: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;toYYYYMM(received_at)&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ttl: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;received_at + INTERVAL 90 DAY&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;settings: { index_granularity: &lt;/span&gt;&lt;span&gt;8192&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;indexes:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;idx_source&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, expression: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;source&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, type: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, maxRows: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, granularity: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;schema&lt;/span&gt;&lt;span&gt;(events)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is the desired state of your database, expressed as code, type checked, reviewable in a pull request, and split across as many files as you want. Materialized views and refreshable views are first class, including the &lt;code dir=&quot;auto&quot;&gt;to&lt;/code&gt; target table and refresh schedules. Compression codecs are typed too, so a column codec chain like &lt;code dir=&quot;auto&quot;&gt;[{ kind: &apos;DoubleDelta&apos; }, { kind: &apos;ZSTD&apos;, level: 3 }]&lt;/code&gt; is validated rather than stringly typed.&lt;/p&gt;
&lt;p&gt;You do not write SQL to define this. You write SQL for your queries, because ch-kit is not an ORM and has no opinion about how you read your data. It only owns the schema, the migrations, and the guardrails.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migrations-are-diffs-not-artifacts-you-maintain&quot;&gt;Migrations are diffs, not artifacts you maintain&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;As we’ve mentioned already multiple times, with ch-kit you just change the TypeScript, and the rest is handled for you. Migrations are part of it.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;generate&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_events_table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;With &lt;code dir=&quot;auto&quot;&gt;generate&lt;/code&gt;, ch-kit loads your TS definitions, compares them against the previous snapshot, computes an ordered plan, and writes a migration SQL file plus an updated snapshot. If nothing changed, no file is created. Every operation in the plan is tagged with a risk level:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Risk&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;th&gt;Examples&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;safe&lt;/code&gt;&lt;/td&gt;&lt;td&gt;No data loss&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;CREATE TABLE&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;ADD COLUMN&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;caution&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Review recommended&lt;/td&gt;&lt;td&gt;settings changes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;danger&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Destructive&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;DROP TABLE&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;DROP COLUMN&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Here is a detail that matters and that most ad hoc setups get wrong. ch-kit knows which schema properties are structural and which are alterable. Changing columns, indexes, projections, settings, or TTL is an &lt;code dir=&quot;auto&quot;&gt;ALTER&lt;/code&gt; in place. Changing the engine, &lt;code dir=&quot;auto&quot;&gt;primaryKey&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;orderBy&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;partitionBy&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;uniqueKey&lt;/code&gt; is structural, which means a drop and recreate. ch-kit makes that distinction explicit in the plan instead of letting you find out in production that your “small” change rewrites the whole table.&lt;/p&gt;
&lt;p&gt;Renames are tracked, not guessed. Mark a column or table with &lt;code dir=&quot;auto&quot;&gt;renamedFrom&lt;/code&gt; (or pass &lt;code dir=&quot;auto&quot;&gt;--rename-table&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;--rename-column&lt;/code&gt;) and ch-kit emits a real rename instead of a destructive drop and add:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;columns: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;user_email&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, type: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, renamedFrom: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;applying-changes-without-losing-data&quot;&gt;Applying changes without losing data&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Once you have your migrations defined with &lt;code dir=&quot;auto&quot;&gt;generate&lt;/code&gt;, you can preview the impact by running &lt;code dir=&quot;auto&quot;&gt;chkit migrate&lt;/code&gt;. This will dry run and show you the plan. If you like what you see, just add &lt;code dir=&quot;auto&quot;&gt;--apply&lt;/code&gt; at the end for the actual run:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;migrate&lt;/span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;# shows the plan&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;migrate&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--apply&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# applies pending migrations&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Three guardrails run on every apply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Destructive operations are blocked.&lt;/strong&gt; Any migration containing a &lt;code dir=&quot;auto&quot;&gt;risk=danger&lt;/code&gt; operation, a &lt;code dir=&quot;auto&quot;&gt;DROP TABLE&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;DROP COLUMN&lt;/code&gt;, requires explicit approval. In an interactive terminal you get a prompt. In CI, the command exits with code 3 and refuses to run unless you pass &lt;code dir=&quot;auto&quot;&gt;--allow-destructive&lt;/code&gt; or set &lt;code dir=&quot;auto&quot;&gt;safety.allowDestructive&lt;/code&gt; in config. The error payload tells you exactly which migration and which operation, with an impact description and a recommendation. Your data does not disappear because a reviewer was tired.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checksums are verified.&lt;/strong&gt; Every applied migration is recorded in a &lt;code dir=&quot;auto&quot;&gt;_chkit_migrations&lt;/code&gt; journal table in ClickHouse along with a SHA-256 hash of the file content. If a migration file is edited after it was applied, the next run aborts before touching anything. Migration history is immutable, and ch-kit enforces it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It is built for CI.&lt;/strong&gt; The CLI detects non-interactive environments (&lt;code dir=&quot;auto&quot;&gt;CI=true&lt;/code&gt;, no TTY) and never blocks on a prompt. Every command supports &lt;code dir=&quot;auto&quot;&gt;--json&lt;/code&gt; with a stable output envelope, so you can parse results with &lt;code dir=&quot;auto&quot;&gt;jq&lt;/code&gt; and build approval gates.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;dont-let-your-project-drift&quot;&gt;Don’t let your project drift!&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;ch-kit also implements a &lt;code dir=&quot;auto&quot;&gt;drift&lt;/code&gt; command that introspects the live database and compares it, object by object and column by column, against your snapshot:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;drift&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It reports missing objects, extra objects, changed columns, and mismatches on settings, TTL, indexes, &lt;code dir=&quot;auto&quot;&gt;ORDER BY&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;PARTITION BY&lt;/code&gt;, primary keys, and projections. It is detailed enough that the 3am manual &lt;code dir=&quot;auto&quot;&gt;ALTER&lt;/code&gt; shows up the next morning as a concrete, named difference instead of a vague feeling that something is off.&lt;/p&gt;
&lt;p&gt;In CI you turn that signal into a gate with &lt;code dir=&quot;auto&quot;&gt;chkit check&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;check&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--strict&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;check&lt;/code&gt; evaluates three policies, all on by default: fail on pending migrations, fail on checksum mismatch, fail on drift. The &lt;code dir=&quot;auto&quot;&gt;--strict&lt;/code&gt; flag forces them on regardless of config, so a permissive local setting never leaks into your pipeline. A full pull request gate is a few lines:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Check schema consistency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;npx chkit check --strict --json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Verify generated types&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;npx chkit codegen --check --json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code dir=&quot;auto&quot;&gt;codegen --check&lt;/code&gt; step is the other half. ch-kit generates TypeScript row types (and optional Zod schemas) from the same definitions, so your application types and your database schema cannot quietly diverge either.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;works-with-any-clickhouse&quot;&gt;Works with any ClickHouse&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;ch-kit is not a lock-in play. We run ObsessionDB, a managed ClickHouse offering that competes with ClickHouse Cloud, Altinity, and the rest. But we love ClickHouse, and we believe the tooling around it should be available to everyone using the technology, because that is what strengthens the ecosystem. So ch-kit runs against ObsessionDB, ClickHouse Cloud, Altinity, or any other managed or self-hosted ClickHouse.&lt;/p&gt;
&lt;p&gt;The only managed-specific touch is that &lt;code dir=&quot;auto&quot;&gt;Shared*&lt;/code&gt; engine prefixes (&lt;code dir=&quot;auto&quot;&gt;SharedMergeTree&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;SharedReplacingMergeTree&lt;/code&gt;, and so on) are normalized away during comparison, so managed engines do not show up as false drift. If you run ObsessionDB, the first-party plugin keeps your schema on the right &lt;code dir=&quot;auto&quot;&gt;Shared*&lt;/code&gt; engines automatically, from the exact same TypeScript you use locally. If you do not, those engine variants are stripped for your target. Same schema, different backends.&lt;/p&gt;
&lt;p&gt;It is MIT licensed, written in TypeScript, runs on Node 20+ or Bun, and needs ClickHouse 24.x or newer. It is beta: the CLI surface and the schema DSL are stable and run our own production workloads today, and we may still make small breaking changes before 1.0.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;start-from-where-you-already-are&quot;&gt;Start from where you already are&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Most teams adopting ch-kit are not starting from an empty database. They already have ClickHouse in production, with tables that matter and data they cannot lose. Either way, the first step is the same: get a project scaffolded.&lt;/p&gt;
&lt;p&gt;If you are starting from scratch, &lt;code dir=&quot;auto&quot;&gt;npm create chkit@latest&lt;/code&gt; gives you a working project from a curated example:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;chkit@latest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If you are adding ch-kit to an existing TypeScript project, &lt;code dir=&quot;auto&quot;&gt;chkit init&lt;/code&gt; writes the project config (&lt;code dir=&quot;auto&quot;&gt;clickhouse.config.ts&lt;/code&gt;, with the ClickHouse connection block reading from environment variables) and a starter schema file. It is idempotent, so running it on an existing project leaves your files untouched.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;my-app/&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# before: your existing TypeScript project&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# ├── package.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# ├── tsconfig.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# └── src/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#     └── index.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;init&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Created&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clickhouse.config.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Created&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;src/db/schema/example.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# after: chkit added exactly two files, nothing else touched&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# my-app/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# ├── clickhouse.config.ts          ← config: ClickHouse connection + plugins&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# ├── package.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# ├── tsconfig.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# └── src/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#     ├── index.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#     └── db/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#         └── schema/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#             └── example.ts        ← starter schema: an `events` table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If you are starting fresh, edit the generated example schema to match the tables you want, then move on to &lt;code dir=&quot;auto&quot;&gt;generate&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you already run ClickHouse, the next command is &lt;code dir=&quot;auto&quot;&gt;chkit pull&lt;/code&gt;. It introspects your live database and writes a deterministic TypeScript schema file from what is actually there:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 1) install the pull plugin&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-D&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@chkit/plugin-pull&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 2) register it in clickhouse.config.ts:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#      import { pull } from &apos;@chkit/plugin-pull&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#      plugins: [pull({ outFile: &apos;./src/db/schema/pulled.ts&apos; })]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 3) introspect your live database into TypeScript&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pull&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--out-file&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;./src/db/schema/pulled.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is the step that turns an existing production database into version-controlled code. You do not transcribe table definitions by hand and hope you got the &lt;code dir=&quot;auto&quot;&gt;ORDER BY&lt;/code&gt; and the codecs right. ch-kit reads them. The output is deterministic, so it produces clean, reviewable diffs instead of churn, and you can scope it to specific databases with &lt;code dir=&quot;auto&quot;&gt;--database&lt;/code&gt; or preview it with &lt;code dir=&quot;auto&quot;&gt;--dryrun&lt;/code&gt; before writing anything.&lt;/p&gt;
&lt;p&gt;Once you have pulled, prove the result immediately:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;drift&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If your code matches production, drift reports nothing. That zero-drift result is the moment your database stops being the source of truth and your repository takes over. From here, every future change starts in TypeScript:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;generate&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;init&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;migrate&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--apply&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chkit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;drift&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;want-more-check-the-rest-of-the-features&quot;&gt;Want more? Check the rest of the features!&lt;/h2&gt;&lt;/div&gt;
&lt;aside aria-label=&quot;Tip&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Tip&lt;/p&gt;&lt;div&gt;&lt;p&gt;We have only covered the initial bits of what ch-kit can do here. The &lt;a href=&quot;https://chkit.obsessiondb.com&quot;&gt;docs&lt;/a&gt; go into more detail on getting started no matter your case, and on everything else the tooling can do.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;The relational world stopped hand writing migrations a long time ago. OLAP should not be the exception. If you run ClickHouse and you are still managing schema by hand, we built ch-kit so you do not have to.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href=&quot;https://github.com/obsessiondb/chkit&quot;&gt;https://github.com/obsessiondb/chkit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href=&quot;https://chkit.obsessiondb.com/getting-started/&quot;&gt;https://chkit.obsessiondb.com/getting-started/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install:&lt;/strong&gt; &lt;a href=&quot;https://www.npmjs.com/package/chkit&quot;&gt;https://www.npmjs.com/package/chkit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you do not use TypeScript but Python, do not worry: we will publish a Python port in the coming weeks. And since ch-kit is open source, if you feel something is missing, you are more than welcome to open an issue or a PR.&lt;/p&gt;
&lt;p&gt;Stay tuned!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;ClickHouse® is a registered trademark of ClickHouse, Inc.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>announcement</category><category>clickhouse</category></item></channel></rss>