Para hacer migraciones de esquema en ClickHouse con chkit, declaras tus tablas en TypeScript o Python, chkit genera el SQL de la migración a partir del diff, lo aplica y hace fallar tu CI si la base en vivo se desvía de lo declarado. Probamos la versión 0.2.0-beta.11 de punta a punta, incluidas las partes que deben fallar.
yoDEV Tested · 22 de octubre de 2026 Probamos chkit 0.2.0-beta.11 (TypeScript con Bun) y chkit-py 0.2.0 contra clickhouse-server 26.9.25.15 en Linux. Los resultados que reportamos como nuestros salen de esa prueba.
¿Qué es chkit?
chkit es schema as code para ClickHouse, con licencia MIT: describes tablas, vistas, vistas materializadas y diccionarios en código, y la CLI calcula los CREATE y ALTER necesarios. En su Show HN se presenta como “like Drizzle”, y el flujo tiene esa forma: un archivo de esquema, un paso generate que escribe SQL que puedes leer, un paso migrate que lo aplica y una tabla de registro que guarda qué se ejecutó.
Se distribuye en varios paquetes de npm: chkit (la CLI), @chkit/core (el DSL de esquema, la configuración y el motor de diff) y plugins para importar un esquema existente, generar tipos y hacer backfills. También hay una versión para Python, chkit-py.
Dos cosas que debes saber antes de adoptarlo.
Está en beta. El README afirma que la CLI y el DSL de esquema son estables y que chkit ya corre cargas de producción, pero el equipo se reserva pequeños cambios incompatibles en la experiencia de uso y en las APIs internas antes de la 1.0. Todo lo que ves en este artículo está fijado a 0.2.0-beta.11, publicada en npm el 12 de octubre de 2026.
Lo construye un proveedor. chkit viene del equipo de ObsessionDB, un servicio de ClickHouse administrado, y el README recomienda ObsessionDB para producción. La herramienta en sí es MIT y funciona con cualquier ClickHouse. La relación se nota en chkit init, que te ofrece registrarte en ObsessionDB antes de explicarte cómo conectar tu propio servidor.
¿Cómo se instala chkit?
Con Bun, fijando la versión que probamos (el comando del README es el mismo sin las versiones @):
bun add -d chkit@0.2.0-beta.11 @chkit/core@0.2.0-beta.11
bunx chkit init
init crea clickhouse.config.ts y un esquema de ejemplo en src/db/schema/example.ts. La configuración generada lee la conexión de cuatro variables de entorno y solo define el bloque clickhouse cuando CLICKHOUSE_URL está definida:
export CLICKHOUSE_URL=http://localhost:8123
export CLICKHOUSE_USER=default
export CLICKHOUSE_PASSWORD=
export CLICKHOUSE_DB=default
Las cuatro tienen que estar definidas; una contraseña vacía está permitida, una sin definir no. El esquema de ejemplo además se niega a cargar sin CLICKHOUSE_DB, así que una terminal mal configurada falla de forma visible en lugar de generar migraciones para la base equivocada.
Para Python es pip install chkit-py, y el comando sigue siendo chkit. Más abajo verás en qué se diferencia.
¿Cómo se define el esquema de base de datos de ClickHouse en TypeScript?
Se define con la función table() de @chkit/core. Esta es la tabla que genera init:
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)
Los tipos de columna son los propios de ClickHouse escritos como texto, así que no hay una capa de traducción de tipos que aprender. Un detalle que nos sorprendió: un default escrito como texto plano es un valor literal. En nuestra primera prueba escribimos default: "''" para un valor vacío y obtuvimos DEFAULT '''''', es decir, el texto de dos caracteres ''. Las expresiones van en { expression: '...' } (la forma antigua del README, 'fn:now64(3)', sigue funcionando y es equivalente). La referencia del DSL lo documenta, y generate rechaza el error más común: una llamada a función escrita como texto, como 'now64(3)', en una columna que no puede guardar texto.
¿Cómo hacer migraciones de esquema en ClickHouse?
Ejecuta generate con un nombre y luego migrate:
bunx chkit generate --name init
bunx chkit migrate --apply
bunx chkit status
generate escribe un archivo SQL con marca de tiempo en chkit/migrations/ y actualiza chkit/meta/snapshot.json. Este es el archivo que obtuvimos:
-- 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 sentencia lleva una etiqueta de riesgo, y el SQL es DDL normal de ClickHouse que puedes revisar en un pull request. migrate --apply lo ejecutó y lo registró en la tabla _chkit_migrations, con el nombre del archivo, un checksum SHA-256 del archivo y la versión de chkit que lo aplicó.
Agregar una columna al esquema y ejecutar generate --name add-country produjo una sola sentencia:
-- 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);
¿Cómo detectar schema drift en ClickHouse?
Con chkit drift, que inspecciona la base en vivo y la compara con el snapshot. Para probarlo hicimos lo que alguien siempre termina haciendo: agregar una columna a mano, por fuera de chkit.
ALTER TABLE default.events ADD COLUMN debug_flag UInt8
chkit drift la detectó:
Expected objects: 1
Actual objects: 1
Drifted: yes
Table shape drift:
- default.events
extraColumns=debug_flag
reasonCodes=extra_column
Con --json devuelve lo mismo de forma estructurada, con una entrada tableDrift por tabla y un código de motivo para cada tipo de diferencia: columnas que faltan o sobran, tipos o valores por defecto cambiados, settings, índices, TTL, motor, clave primaria, ORDER BY, PARTITION BY y proyecciones.
Ojo: drift siempre termina con código 0. Informa, no bloquea. El comando que hace fallar un build es chkit check.
Las tablas que chkit no administra se tratan distinto. Creamos a mano una tabla scratch, y drift la listó como objeto inesperado pero igual indicó Drifted: no, y check pasó. Es a propósito: chkit puede compartir una base con tablas que no le pertenecen. Si chkit es dueño de toda la base, la política check.failOnExtraObjects hace que los objetos sobrantes también fallen.
¿Cómo usar chkit check en CI con GitHub Actions?
chkit check termina con código 1 si hay migraciones pendientes, si alguien editó un archivo de migración ya aplicado o si el esquema en vivo se desvió. Provocamos los tres casos:
- Tras agregar la columna
countrypero antes de migrar:Failed checks: pending_migrations, schema_drift, código 1. - Después de migrar:
Check status: ok, código 0. - Tras la columna
debug_flagagregada a mano:Failed checks: schema_drift, código 1. - Tras agregar una línea a un archivo de migración ya aplicado:
Failed checks: checksum_mismatch, código 1.chkit statusnombró el archivo; revertir el cambio lo resolvió.
El workflow de GitHub Actions para pull requests que trae la documentación es:
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 activa todas las políticas sin importar el archivo de configuración, que es lo que quieres en CI. Un requisito que el ejemplo no muestra: check necesita llegar a un servidor ClickHouse, porque el registro de migraciones vive ahí. Sin conexión se detiene con “clickhouse config is required for check”. El ejemplo de despliegue de la misma guía pasa CLICKHOUSE_URL, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD y CLICKHOUSE_DB desde los secrets; el job de validación también los necesita.
¿Qué hace chkit con las migraciones destructivas?
Las bloquea, salvo que digas lo contrario. Quitamos la columna source del esquema y generate escribió:
-- operation: alter_table_drop_column key=table:default.events:column:source risk=danger
ALTER TABLE default.events DROP COLUMN IF EXISTS `source`;
Al ejecutar migrate --apply sin terminal, como en CI, se negó con un código de advertencia (drop_column_irreversible), el motivo, el impacto y una recomendación, y nos pidió repetir con --allow-destructive. Con esa opción, se aplicó.
Una diferencia con la documentación: dice que una migración destructiva bloqueada termina con código 3 en ejecuciones no interactivas. En nuestras pruebas terminó con 3 solo con --json; en modo texto, con la entrada cerrada y también con CI=true, terminó con 1. Ambos hacen fallar el job, así que nada se cuela, pero si tu pipeline separa el código 3 para mandar los cambios destructivos a un job con aprobación manual, como sugiere la documentación, usa --json.
¿chkit genera tipos de TypeScript desde las tablas de ClickHouse?
Sí, con @chkit/plugin-codegen. Con el plugin registrado en clickhouse.config.ts (codegen({ outFile: './src/generated/chkit-types.ts' })), chkit codegen escribió:
export type DefaultEventsRow = {
id: string
country: string
ingested_at: string
}
UInt64 y DateTime64 salen como string, que coincide con cómo ClickHouse los devuelve en JSON. El plugin también puede generar esquemas de Zod, y chkit codegen --check hace fallar el CI cuando el archivo generado quedó desactualizado.
¿Funciona chkit con Python?
Sí, con una diferencia que conviene conocer. pip install chkit-py instala la versión 0.2.0, que la documentación marca como alfa. init, generate, migrate --apply y check funcionaron en nuestra prueba.
La diferencia está en el drift. Por defecto, chkit drift en Python compara el snapshot con tus archivos de esquema locales, no con la base en vivo. Tras agregar la misma columna a mano, drift en Python indicó Drifted: no, y chkit check --strict terminó con código 0. Al agregar --live, ambos la detectaron: drift --live reportó extra_column y check --live falló con schema_drift, código 1.
Al 22 de octubre de 2026, --live solo aparece en la página de Kafka de la documentación, y el workflow de Python de la guía de CI ejecuta chkit check --strict --json sin esa opción, mientras afirma que las opciones son las mismas que en TypeScript. Si usas la versión de Python en CI, agrega --live.
¿Es chkit como Drizzle ORM?
En el flujo de trabajo, sí; en el alcance, no. Drizzle ORM es el ORM de TypeScript con el que muchos equipos definen su esquema y generan migraciones para bases relacionales. chkit copia ese ciclo (esquema en código, generate, migrate) y lo lleva a ClickHouse, pero no es un ORM: no construye consultas desde tu código. Si tu equipo ya escribe el esquema de Postgres en Drizzle y el DDL de ClickHouse es lo único que sigue a mano, ese es el hueco que llena.
¿Qué otras herramientas de migración de base de datos hay para ClickHouse?
La propia base de conocimiento de ClickHouse lista una docena. La mayoría son ejecutores imperativos que aplican archivos SQL versionados en orden: golang-migrate, Goose, Flyway, Liquibase y ejecutores específicos de ClickHouse para Node.js y Python. Atlas y Ptah son declarativos como chkit y también detectan drift.
Lo que aporta chkit es el enfoque centrado en TypeScript: el esquema vive en el mismo lenguaje que tu aplicación, los tipos se generan a partir de él y una sola CLI cubre generate, migrate, drift y la validación en CI. Y si lo que buscas es explorar esa base desde el navegador, ya vimos cómo desplegar LibreDB Studio con Docker o Helm.
¿Vale la pena usar chkit?
Si usas ClickHouse junto a un código en TypeScript y tus cambios de esquema hoy viven en archivos SQL sueltos o en la memoria de alguien, chkit merece una tarde. El SQL generado se lee bien, la protección contra cambios destructivos funciona y la detección de drift encontró una columna agregada a mano que de otra forma habría pasado desapercibida.
Fija la versión, porque la 1.0 todavía puede traer pequeños cambios incompatibles. Y si trabajas en Python, trata esa versión como la alfa que dice ser y usa --live.
¿Y tú? ¿Qué cambio de esquema hecho a mano en producción te costó más descubrir?
- Archivos SQL versionados con un ejecutor de migraciones
- Una herramienta declarativa (Atlas, chkit u otra)
- A mano, directo en la consola
- Prefiero otra opción (cuéntanos cuál)
Comenta abajo o, si este artículo te llegó por correo, responde directamente al email: tu respuesta se publica aquí.
Relacionado: Alternativa a DBeaver en el navegador: cómo desplegar LibreDB Studio con Docker o Helm