Skip to content

Schema DSL Reference

Schema files are TypeScript files that export definitions using functions from @chkit/core. All exported definitions are collected when chkit loads schema files matched by the schema glob in your configuration.

import { schema, table, view, materializedView } from '@chkit/core'

Groups definitions into a single array for export.

schema(...definitions: SchemaDefinition[]): SchemaDefinition[]
export default schema(users, events)

You can also export definitions individually — any exported value with a valid kind is discovered automatically.

Creates a table definition.

table(input: Omit<TableDefinition, 'kind'>): TableDefinition

Minimal example:

import { schema, table } from '@chkit/core'
const users = table({
database: 'app',
name: 'users',
columns: [
{ name: 'id', type: 'UInt64' },
{ name: 'email', type: 'String' },
],
engine: 'MergeTree()',
primaryKey: ['id'],
orderBy: ['id'],
})
export default schema(users)

Comprehensive example (all features):

const events = table({
database: 'analytics',
name: 'events',
columns: [
{ name: 'id', type: 'UInt64' },
{ name: 'org_id', type: 'String' },
{ name: 'source', type: 'LowCardinality(String)' },
{ name: 'payload', type: 'String', nullable: true },
{ name: 'received_at', type: 'DateTime64(3)', default: 'fn:now64(3)' },
{ name: 'status', type: 'String', default: 'pending', comment: 'Event processing status' },
],
engine: 'MergeTree()',
primaryKey: ['id'],
orderBy: ['org_id', 'received_at', 'id'],
partitionBy: 'toYYYYMM(received_at)',
ttl: 'received_at + INTERVAL 90 DAY',
settings: { index_granularity: 8192 },
indexes: [
{ name: 'idx_source', expression: 'source', type: 'set', maxRows: 0, granularity: 1 },
],
projections: [
{ name: 'p_recent', query: 'SELECT id ORDER BY received_at DESC LIMIT 10' },
],
comment: 'Raw ingested events',
})
FieldTypeDescription
databasestringClickHouse database name
namestringTable name
columnsColumnDefinition[]Column definitions (see Columns)
enginestringEngine clause, e.g. 'MergeTree()', 'ReplacingMergeTree(ver)'
primaryKeystring[]Primary key columns
orderBystring[]ORDER BY columns
FieldTypeDescription
partitionBystringPartition expression, e.g. 'toYYYYMM(created_at)'
uniqueKeystring[]Unique key columns
ttlstringTTL expression, e.g. 'created_at + INTERVAL 90 DAY'
settingsRecord<string, string | number | boolean>Table-level settings
indexesSkipIndexDefinition[]Skip indexes (see Skip indexes)
projectionsProjectionDefinition[]Projections (see Projections)
commentstringTable comment
renamedFrom{ database?: string; name: string }Previous identity for rename tracking (see Rename support)
pluginsTablePluginsPer-table plugin configuration (see Plugin configuration)

Each entry in the columns array is a ColumnDefinition.

Column name.

Any ClickHouse type string. Parameterized types like DateTime64(3), Decimal(18, 4), Enum8('a' = 1, 'b' = 2), and FixedString(32) are supported.

Primitive types recognized by the DSL type system: String, UInt8, UInt16, UInt32, UInt64, UInt128, UInt256, Int8, Int16, Int32, Int64, Int128, Int256, Float32, Float64, Bool, Boolean, Date, DateTime, DateTime64.

When true, the column type is wrapped in Nullable(...) in the generated SQL.

{ name: 'payload', type: 'String', nullable: true }
// SQL: `payload` Nullable(String)

default (string | number | boolean, optional)

Section titled “default (string | number | boolean, optional)”

Default value for the column.

  • String values are single-quoted in SQL: default: 'pending' produces DEFAULT 'pending'
  • Number/boolean values are rendered literally: default: 0 produces DEFAULT 0
  • fn: prefix — for function-call defaults, prefix the string with fn: to emit a raw SQL expression:
{ name: 'received_at', type: 'DateTime64(3)', default: 'fn:now64(3)' }
// SQL: `received_at` DateTime64(3) DEFAULT now64(3)

Column-level comment rendered in SQL.

Previous column name for rename tracking. See Rename support.

Each entry in the indexes array is a SkipIndexDefinition. The shared base fields are:

FieldTypeDescription
namestringIndex name
expressionstringIndexed expression
type'minmax' | 'set' | 'bloom_filter' | 'tokenbf_v1' | 'ngrambf_v1'Index type
granularitynumberIndex granularity

Type-specific fields:

TypeRequired fieldsOptional fieldsNotes
minmaxNo arguments
setmaxRows: numbermaxRows: 0 stores all unique values (ClickHouse 26+ requires set(0) rather than bare set)
bloom_filterfalsePositiveRate: numberDefaults to 0.025 when omitted
tokenbf_v1sizeBytes, hashFunctions, randomSeed (all number)Maps to tokenbf_v1(size_bytes, n_hash, seed)
ngrambf_v1ngramSize, sizeBytes, hashFunctions, randomSeed (all number)Maps to ngrambf_v1(n, size_bytes, n_hash, seed)
indexes: [
{ name: 'idx_source', expression: 'source', type: 'set', maxRows: 0, granularity: 1 },
{ name: 'idx_ts', expression: 'received_at', type: 'minmax', granularity: 3 },
{
name: 'idx_body',
expression: 'body',
type: 'tokenbf_v1',
sizeBytes: 256,
hashFunctions: 2,
randomSeed: 0,
granularity: 1,
},
]

Each entry in the projections array is a ProjectionDefinition.

FieldTypeDescription
namestringProjection name
querystringProjection SELECT query
projections: [
{ name: 'p_recent', query: 'SELECT id ORDER BY received_at DESC LIMIT 10' },
]

Creates a view definition.

view(input: Omit<ViewDefinition, 'kind'>): ViewDefinition
FieldTypeRequiredDescription
databasestringyesDatabase name
namestringyesView name
asstringyesSELECT query
commentstringnoView comment
import { view } from '@chkit/core'
const activeUsers = view({
database: 'app',
name: 'active_users',
as: 'SELECT id, email FROM app.users WHERE active = 1',
})

Creates a materialized view definition.

materializedView(input: Omit<MaterializedViewDefinition, 'kind'>): MaterializedViewDefinition
FieldTypeRequiredDescription
databasestringyesDatabase name
namestringyesMaterialized view name
to{ database: string; name: string }yesTarget table for the view
refreshMaterializedViewRefreshnoRefresh schedule — see Refreshable materialized views
asstringyesSELECT query
commentstringnoView comment
import { materializedView } from '@chkit/core'
const eventCounts = materializedView({
database: 'analytics',
name: 'event_counts_mv',
to: { database: 'analytics', name: 'event_counts' },
as: 'SELECT org_id, count() AS total FROM analytics.events GROUP BY org_id',
})

For a refreshable (scheduled) materialized view, add the refresh field:

const dailyReport = materializedView({
database: 'analytics',
name: 'daily_report_mv',
to: { database: 'analytics', name: 'daily_report' },
refresh: { every: '1 DAY', offset: '2 HOUR' },
as: 'SELECT toDate(ts) AS day, count() AS total FROM analytics.events GROUP BY day',
})

See Refreshable materialized views for the full refresh field reference, including APPEND mode, DEPENDS ON, and the ClickHouse rules that chkit validates.

The codegen plugin maps ClickHouse types to TypeScript types using these rules:

CategoryClickHouse TypesTypeScript Type
String-likeString, FixedString, Date, Date32, DateTime, DateTime64, UUID, IPv4, IPv6, Enum8, Enum16, Decimal*string
NumberInt8, Int16, Int32, UInt8, UInt16, UInt32, Float32, Float64, BFloat16number
Large integersInt64, Int128, Int256, UInt64, UInt128, UInt256string (default) or bigint
BooleanBool, Booleanboolean
WrappersNullable(T)T | null
WrappersLowCardinality(T)same as T
CompositeArray(T)T[]
CompositeMap(K, V)Record<K, V>
CompositeTuple(T1, T2, ...)[T1, T2, ...]
AggregateSimpleAggregateFunction(fn, T)same as T
JSONJSONRecord<string, unknown>

Parameterized types like DateTime('UTC'), Decimal(18, 4), and Enum8('a' = 1) are supported. The bigintMode option in the codegen plugin controls whether large integers map to string or bigint.

chkit tracks renames to avoid destructive drop-and-recreate operations.

Set renamedFrom on a table definition to rename a table:

const users = table({
database: 'app',
name: 'accounts', // new name
renamedFrom: { name: 'users' }, // old name
// ...
})

The database field in renamedFrom is optional and defaults to the table’s current database.

Set renamedFrom on a column definition to rename a column:

columns: [
{ name: 'user_email', type: 'String', renamedFrom: 'email' },
]

Both table and column renames can be overridden by CLI flags --rename-table and --rename-column.

The plugins field on a table definition provides per-table configuration for plugins. Each plugin that supports table-level config augments the TablePlugins interface via TypeScript declaration merging, so the available keys and their types depend on which plugin packages are imported.

import { table } from '@chkit/core'
const events = table({
database: 'app',
name: 'events',
columns: [
{ name: 'event_time', type: 'DateTime' },
{ name: 'id', type: 'UInt64' },
],
engine: 'MergeTree',
orderBy: ['event_time', 'id'],
primaryKey: ['event_time', 'id'],
plugins: {
backfill: { timeColumn: 'event_time' },
},
})

Currently supported plugin keys:

KeyPluginFieldsDescription
backfill@chkit/plugin-backfilltimeColumn?: stringTime column for backfill WHERE clauses

The plugins field is ignored by the diff engine — it does not affect migration planning or SQL generation.

chkit validates schema definitions and throws a ChxValidationError if any issues are found:

  • Duplicate object names — two definitions with the same kind, database, and name
  • Duplicate column names — repeated column name within a table
  • Duplicate index names — repeated index name within a table
  • Duplicate projection names — repeated projection name within a table
  • Primary key references missing columnprimaryKey includes a column not in columns
  • Order by references missing columnorderBy includes a column not in columns

When a property changes, chkit determines whether the table can be altered in place or must be dropped and recreated.

Structural (drop + recreate): engine, primaryKey, orderBy, partitionBy, uniqueKey

Alterable (ALTER in place): columns, indexes, projections, settings, TTL, comment

Views and materialized views always use drop + recreate.