Para fazer migrações de esquema em ClickHouse com chkit, você declara suas tabelas em TypeScript ou Python, chkit gera o SQL da migração a partir do diff, aplica e faz falhar seu CI se a base em produção se desviar do declarado. Testamos a versão 0.2.0-beta.11 de ponta a ponta, incluindo as partes que devem falhar.
yoDEV Tested · 22 de outubro de 2026 Testamos chkit 0.2.0-beta.11 (TypeScript com Bun) e chkit-py 0.2.0 contra clickhouse-server 26.9.25.15 em Linux. Os resultados que reportamos como nossos saem desse teste.
O que é chkit?
chkit é schema as code para ClickHouse, com licença MIT: você descreve tabelas, visões, visões materializadas e dicionários em código, e a CLI calcula os CREATE e ALTER necessários. Em seu Show HN se apresenta como “like Drizzle”, e o fluxo tem essa forma: um arquivo de esquema, um passo generate que escreve SQL que você pode ler, um passo migrate que o aplica e uma tabela de registro que guarda o que foi executado.
Se distribui em vários pacotes de npm: chkit (a CLI), @chkit/core (o DSL de esquema, a configuração e o motor de diff) e plugins para importar um esquema existente, gerar tipos e fazer backfills. Também há uma versão para Python, chkit-py.
Duas coisas que você deve saber antes de adotá-lo.
Está em beta. O README afirma que a CLI e o DSL de esquema são estáveis e que chkit já roda cargas de produção, mas o time se reserva pequenas mudanças incompatíveis na experiência de uso e nas APIs internas antes da 1.0. Tudo o que você vê neste artigo está fixado a 0.2.0-beta.11, publicada em npm no 12 de outubro de 2026.
O constrói um provedor. chkit vem do time de ObsessionDB, um serviço de ClickHouse gerenciado, e o README recomenda ObsessionDB para produção. A ferramenta em si é MIT e funciona com qualquer ClickHouse. A relação se nota em chkit init, que oferece se registrar em ObsessionDB antes de explicar como conectar seu próprio servidor.
Como se instala chkit?
Com Bun, fixando a versão que testamos (o comando do README é o mesmo sem as versões @):
bun add -d chkit@0.2.0-beta.11 @chkit/core@0.2.0-beta.11
bunx chkit init
init cria clickhouse.config.ts e um esquema de exemplo em src/db/schema/example.ts. A configuração gerada lê a conexão de quatro variáveis de ambiente e só define o bloco clickhouse quando CLICKHOUSE_URL está definida:
export CLICKHOUSE_URL=http://localhost:8123
export CLICKHOUSE_USER=default
export CLICKHOUSE_PASSWORD=
export CLICKHOUSE_DB=default
As quatro precisam estar definidas; uma senha vazia é permitida, uma sem definir não. O esquema de exemplo além disso se recusa a carregar sem CLICKHOUSE_DB, então um terminal mal configurado falha de forma visível em vez de gerar migrações para a base errada.
Para Python é pip install chkit-py, e o comando continua sendo chkit. Mais abaixo você verá em que se diferencia.
Como se define o esquema de base de dados ClickHouse em TypeScript?
Se define com a função table() de @chkit/core. Esta é a tabela que init gera:
import { schema, table } from '@chkit/core'
const database = process.env.CLICKHOUSE_DB
if (!database?.trim()) {
throw new Error('Set CLICKHOUSE_DB to the intended database before generating migrations.')
}
const events = table({
database,
name: 'events',
engine: 'MergeTree',
columns: [
{ name: 'id', type: 'UInt64' },
{ name: 'source', type: 'String' },
{ name: 'ingested_at', type: 'DateTime64(3)', default: { expression: 'now64(3)' } },
],
primaryKey: ['id'],
orderBy: ['id'],
partitionBy: 'toYYYYMM(ingested_at)',
})
export default schema(events)
Os tipos de coluna são os próprios do ClickHouse escritos como texto, então não há uma camada de tradução de tipos para aprender. Um detalhe que nos surpreendeu: um default escrito como texto plano é um valor literal. Em nossa primeira tentativa escrevemos default: "''" para um valor vazio e obtivemos DEFAULT '''''', ou seja, o texto de dois caracteres ''. As expressões vão em { expression: '...' } (a forma antiga do README, 'fn:now64(3)', continua funcionando e é equivalente). A referência do DSL o documenta, e generate rejeita o erro mais comum: uma chamada a função escrita como texto, como 'now64(3)', em uma coluna que não pode guardar texto.
Como fazer migrações de esquema em ClickHouse?
Execute generate com um nome e depois migrate:
bunx chkit generate --name init
bunx chkit migrate --apply
bunx chkit status
generate escreve um arquivo SQL com marcação de tempo em chkit/migrations/ e atualiza chkit/meta/snapshot.json. Este é o arquivo que obtivemos:
-- chkit-migration-format: v1
-- generated-at: 2026-10-23T00:17:16.148Z
-- cli-version: 0.2.0-beta.11
-- definition-count: 1
-- operation-count: 2
-- rename-suggestion-count: 0
-- risk-summary: safe=2, caution=0, danger=0
-- operation: create_database key=database:default risk=safe
CREATE DATABASE IF NOT EXISTS default;
-- operation: create_table key=table:default.events risk=safe
CREATE TABLE IF NOT EXISTS default.events
(
`id` UInt64,
`source` String,
`ingested_at` DateTime64(3) DEFAULT now64(3)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ingested_at)
PRIMARY KEY (`id`)
ORDER BY (`id`);
Cada sentença leva uma etiqueta de risco, e o SQL é DDL normal do ClickHouse que você pode revisar em um pull request. migrate --apply o executou e registrou na tabela _chkit_migrations, com o nome do arquivo, um checksum SHA-256 do arquivo e a versão de chkit que o aplicou.
Adicionar uma coluna ao esquema e executar generate --name add-country produziu uma única sentença:
-- operation: alter_table_add_column key=table:default.events:column:country risk=safe
ALTER TABLE default.events ADD COLUMN IF NOT EXISTS `country` LowCardinality(String);
Como detectar schema drift em ClickHouse?
Com chkit drift, que inspeciona a base em produção e a compara com o snapshot. Para testá-lo fizemos o que alguém sempre termina fazendo: adicionar uma coluna manualmente, fora de chkit.
ALTER TABLE default.events ADD COLUMN debug_flag UInt8
chkit drift a detectou:
Expected objects: 1
Actual objects: 1
Drifted: yes
Table shape drift:
- default.events
extraColumns=debug_flag
reasonCodes=extra_column
Com --json devolve o mesmo de forma estruturada, com uma entrada tableDrift por tabela e um código de motivo para cada tipo de diferença: colunas que faltam ou sobram, tipos ou valores padrão mudados, settings, índices, TTL, motor, chave primária, ORDER BY, PARTITION BY e projeções.
Olho: drift sempre termina com código 0. Informa, não bloqueia. O comando que faz falhar um build é chkit check.
As tabelas que chkit não administra se tratam de forma diferente. Criamos manualmente uma tabela scratch, e drift a listou como objeto inesperado mas ainda assim indicou Drifted: no, e check passou. É de propósito: chkit pode compartilhar uma base com tabelas que não lhe pertencem. Se chkit é dono de toda a base, a política check.failOnExtraObjects faz com que os objetos sobrantes também falhem.
Como usar chkit check em CI com GitHub Actions?
chkit check termina com código 1 se há migrações pendentes, se alguém editou um arquivo de migração já aplicado ou se o esquema em produção se desvio. Provocamos os três casos:
- Após adicionar a coluna
countrymas antes de migrar:Failed checks: pending_migrations, schema_drift, código 1. - Depois de migrar:
Check status: ok, código 0. - Após a coluna
debug_flagadicionada manualmente:Failed checks: schema_drift, código 1. - Após adicionar uma linha a um arquivo de migração já aplicado:
Failed checks: checksum_mismatch, código 1.chkit statusnomeou o arquivo; reverter a mudança o resolveu.
O workflow do GitHub Actions para pull requests que traz a documentação é:
name: Schema Validation
on: pull_request
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.5'
- run: bun install --frozen-lockfile
- name: Check schema consistency
run: bunx chkit check --strict --json
- name: Verify generated types
run: bunx chkit codegen --check --json
--strict ativa todas as políticas sem importar o arquivo de configuração, que é o que você quer em CI. Um requisito que o exemplo não mostra: check precisa chegar a um servidor ClickHouse, porque o registro de migrações vive lá. Sem conexão se detém com “clickhouse config is required for check”. O exemplo de despliegue da mesma guia passa CLICKHOUSE_URL, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD e CLICKHOUSE_DB desde os secrets; o job de validação também os precisa.
O que chkit faz com as migrações destrutivas?
As bloqueia, a menos que você diga o contrário. Removemos a coluna source do esquema e generate escreveu:
-- operation: alter_table_drop_column key=table:default.events:column:source risk=danger
ALTER TABLE default.events DROP COLUMN IF EXISTS `source`;
Ao executar migrate --apply sem terminal, como em CI, se recusou com um código de aviso (drop_column_irreversible), o motivo, o impacto e uma recomendação, e nos pediu repetir com --allow-destructive. Com essa opção, se aplicou.
Uma diferença com a documentação: diz que uma migração destrutiva bloqueada termina com código 3 em execuções não interativas. Em nossas tentativas terminou com 3 só com --json; em modo texto, com a entrada fechada e também com CI=true, terminou com 1. Ambos fazem falhar o job, então nada se escapa, mas se seu pipeline separa o código 3 para mandar as mudanças destrutivas a um job com aprovação manual, como sugere a documentação, use --json.
chkit gera tipos de TypeScript desde as tabelas de ClickHouse?
Sim, com @chkit/plugin-codegen. Com o plugin registrado em clickhouse.config.ts (codegen({ outFile: './src/generated/chkit-types.ts' })), chkit codegen escreveu:
export type DefaultEventsRow = {
id: string
country: string
ingested_at: string
}
UInt64 e DateTime64 saem como string, que coincide com como ClickHouse os devolve em JSON. O plugin também pode gerar esquemas de Zod, e chkit codegen --check faz falhar o CI quando o arquivo gerado ficou desatualizado.
Funciona chkit com Python?
Sim, com uma diferença que convém conhecer. pip install chkit-py instala a versão 0.2.0, que a documentação marca como alfa. init, generate, migrate --apply e check funcionaram em nossa tentativa.
A diferença está no drift. Por padrão, chkit drift em Python compara o snapshot com seus arquivos de esquema locais, não com a base em produção. Após adicionar a mesma coluna manualmente, drift em Python indicou Drifted: no, e chkit check --strict terminou com código 0. Ao adicionar --live, ambos a detectaram: drift --live reportou extra_column e check --live falhou com schema_drift, código 1.
Ao 22 de outubro de 2026, --live só aparece na página de Kafka da documentação, e o workflow de Python da guia de CI executa chkit check --strict --json sem essa opção, enquanto afirma que as opções são as mesmas que em TypeScript. Se você usa a versão de Python em CI, adicione --live.
chkit é como Drizzle ORM?
No fluxo de trabalho, sim; no alcance, não. Drizzle ORM é o ORM de TypeScript com o qual muitos times definem seu esquema e geram migrações para bases relacionais. chkit copia esse ciclo (esquema em código, generate, migrate) e o leva a ClickHouse, mas não é um ORM: não constrói consultas desde seu código. Se seu time já escreve o esquema de Postgres em Drizzle e o DDL de ClickHouse é o único que continua manualmente, esse é o buraco que preenche.
Que outras ferramentas de migração de base de dados há para ClickHouse?
A própria base de conhecimento de ClickHouse lista uma dúzia. A maioria são executores imperativos que aplicam arquivos SQL versionados em ordem: golang-migrate, Goose, Flyway, Liquibase e executores específicos de ClickHouse para Node.js e Python. Atlas e Ptah são declarativos como chkit e também detectam drift.
O que chkit contribui é o enfoque centrado em TypeScript: o esquema vive na mesma linguagem que sua aplicação, os tipos se geram a partir dele e uma única CLI cobre generate, migrate, drift e a validação em CI. E se o que você busca é explorar essa base desde o navegador, já vimos como desplegar LibreDB Studio com Docker ou Helm.
Vale a pena usar chkit?
Se você usa ClickHouse junto a um código em TypeScript e seus cambios de esquema hoje vivem em arquivos SQL soltos ou na memória de alguém, chkit merece uma tarde. O SQL gerado se lê bem, a proteção contra cambios destrutivos funciona e a detecção de drift encontrou uma coluna adicionada manualmente que de outra forma teria passado desapercebida.
Fixe a versão, porque a 1.0 ainda pode trazer pequenas mudanças incompatíveis. E se você trabalha em Python, trate essa versão como a alfa que diz ser e use --live.
E você? Que cambio de esquema feito manualmente em produção lhe custou mais descobrir?
- Arquivos SQL versionados com um executor de migrações
- Uma ferramenta declarativa (Atlas, chkit ou outra)
- Manualmente, direto no console
- Prefiro outra opção (conte-nos qual)
Comente abaixo ou, se este artigo lhe chegou por email, responda diretamente ao email: sua resposta se publica aqui.
Relacionado: Alternativa a DBeaver no navegador: como desplegar LibreDB Studio com Docker ou Helm