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, dictionary } from '@chkit/core'schema()
Section titled “schema()”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.
table()
Section titled “table()”Creates a table definition.
table(input: Omit<TableDefinition, 'kind'>): TableDefinitionMinimal 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',})Required fields
Section titled “Required fields”| Field | Type | Description |
|---|---|---|
database | string | ClickHouse database name |
name | string | Table name |
columns | ColumnDefinition[] | Column definitions (see Columns) |
engine | string | Engine clause, e.g. 'MergeTree', 'ReplacingMergeTree(ver)' |
primaryKey | string[] | Primary key columns or expressions, e.g. ['toDate(ts)', 'id'] |
orderBy | string[] | ORDER BY columns or expressions, e.g. ['toStartOfHour(ts)', 'id'] |
Optional fields
Section titled “Optional fields”| Field | Type | Description |
|---|---|---|
partitionBy | string | Partition expression, e.g. 'toYYYYMM(created_at)' |
uniqueKey | string[] | Unique key columns |
ttl | string | TTL expression, e.g. 'created_at + INTERVAL 90 DAY' |
settings | Record<string, string | number | boolean> | Table-level settings |
indexes | SkipIndexDefinition[] | Skip indexes (see Skip indexes) |
projections | ProjectionDefinition[] | Projections (see Projections) |
comment | string | Table comment |
renamedFrom | { database?: string; name: string } | Previous identity for rename tracking (see Rename support) |
plugins | TablePlugins | Per-table plugin configuration (see Plugin configuration) |
Columns
Section titled “Columns”Each entry in the columns array is a ColumnDefinition.
name (string, required)
Section titled “name (string, required)”Column name.
type (string, required)
Section titled “type (string, required)”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.
SQL-standard aliases
Section titled “SQL-standard aliases”chkit passes the type string through to ClickHouse verbatim — it does not rewrite it. ClickHouse itself accepts standard SQL type aliases and stores them as its native types, so a table declared with aliases like BIGINT or TEXT is created successfully:
| SQL alias | ClickHouse native type |
|---|---|
TINYINT | Int8 |
SMALLINT | Int16 |
INTEGER / INT | Int32 |
BIGINT | Int64 |
FLOAT / REAL | Float32 |
DOUBLE | Float64 |
TEXT / VARCHAR / CHAR | String |
TIMESTAMP | DateTime |
See the ClickHouse data types reference for the complete alias list.
nullable (boolean, optional)
Section titled “nullable (boolean, optional)”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'producesDEFAULT 'pending' - Number/boolean values are rendered literally:
default: 0producesDEFAULT 0 fn:prefix — for function-call defaults, prefix the string withfn:to emit a raw SQL expression:
{ name: 'received_at', type: 'DateTime64(3)', default: 'fn:now64(3)' }// SQL: `received_at` DateTime64(3) DEFAULT now64(3)comment (string, optional)
Section titled “comment (string, optional)”Column-level comment rendered in SQL.
renamedFrom (string, optional)
Section titled “renamedFrom (string, optional)”Previous column name for rename tracking. See Rename support.
codec (ColumnCodecSpec, optional)
Section titled “codec (ColumnCodecSpec, optional)”Sets the column compression codec, rendered as a CODEC(...) clause. A codec is an object with a kind, or an array forming a chain (zero or more preprocessors followed by exactly one general codec).
columns: [ { name: 'ts', type: 'DateTime64(3)', codec: { kind: 'Delta', size: 4 } }, { name: 'amount', type: 'Float64', codec: { kind: 'ZSTD', level: 3 } }, // chain: preprocessor then general codec { name: 'seq', type: 'UInt64', codec: [{ kind: 'DoubleDelta' }, { kind: 'LZ4HC', level: 9 }] },]General codecs (the compressor; at most one, and it must come last in a chain):
kind | Args | Renders |
|---|---|---|
NONE, LZ4, T64, GCD, ALP | — | CODEC(LZ4) |
LZ4HC | level?: number | CODEC(LZ4HC(9)) |
ZSTD | level?: number | CODEC(ZSTD(3)) |
Preprocessing codecs (placed before the general codec):
kind | Args | Renders |
|---|---|---|
Delta, DoubleDelta, Gorilla | size?: 1 | 2 | 4 | 8 (bytes, defaults to 1) | CODEC(Delta(4)) |
FPC | level: number, floatSize: 4 | 8 | CODEC(FPC(...)) |
Raw escape hatch — for codecs not yet typed (new ClickHouse versions, unusual arg shapes), pass the inner expression through verbatim:
{ name: 'blob', type: 'String', codec: { kind: 'raw', expression: 'T64, LZ4' } }// → CODEC(T64, LZ4)Codec chains are validated (see Validation rules): a chain must be non-empty, contain at most one general codec, and end with the general codec.
Skip indexes
Section titled “Skip indexes”Each entry in the indexes array is a SkipIndexDefinition. The shared base fields are:
| Field | Type | Description |
|---|---|---|
name | string | Index name |
expression | string | Indexed expression |
type | 'minmax' | 'set' | 'bloom_filter' | 'tokenbf_v1' | 'ngrambf_v1' | Index type |
granularity | number | Index granularity |
Type-specific fields:
| Type | Required fields | Optional fields | Notes |
|---|---|---|---|
minmax | — | — | No arguments |
set | maxRows: number | — | maxRows: 0 stores all unique values (ClickHouse 26+ requires set(0) rather than bare set) |
bloom_filter | — | falsePositiveRate: number | Defaults to 0.025 when omitted |
tokenbf_v1 | sizeBytes, hashFunctions, randomSeed (all number) | — | Maps to tokenbf_v1(size_bytes, n_hash, seed) |
ngrambf_v1 | ngramSize, 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, },]Projections
Section titled “Projections”Each entry in the projections array is a ProjectionDefinition, which takes one of two forms.
A SELECT projection stores a rewritten copy of the data.
| Field | Type | Description |
|---|---|---|
name | string | Projection name |
query | string | Projection SELECT query |
An index-only projection stores no SELECT body. It reorders parts by a secondary key so lookups on that key prune instead of scanning.
| Field | Type | Description |
|---|---|---|
name | string | Projection name |
index | string | Expression list to order by, e.g. receiver, sender |
type | string | Projection index type. ClickHouse currently accepts basic |
projections: [ { name: 'p_recent', query: 'SELECT id ORDER BY received_at DESC LIMIT 10' }, { name: 'by_receiver', index: 'receiver, sender', type: 'basic' },]The index expression is rendered the way ClickHouse normalizes it: a single expression is emitted bare (INDEX receiver), several are emitted as a tuple (INDEX (receiver, sender)), redundant parentheses are dropped, and a space follows every argument separator. Writing '(receiver)' and 'receiver' therefore produce the same table, and neither reads as drift.
A projection must be exactly one of the two kinds. Setting both query and index on the same entry is a projection_ambiguous_kind validation error, and an empty index is a projection_empty_index error.
view()
Section titled “view()”Creates a view definition.
view(input: Omit<ViewDefinition, 'kind'>): ViewDefinition| Field | Type | Required | Description |
|---|---|---|---|
database | string | yes | Database name |
name | string | yes | View name |
as | string | yes | SELECT query |
comment | string | no | View comment |
import { view } from '@chkit/core'
const activeUsers = view({ database: 'app', name: 'active_users', as: 'SELECT id, email FROM app.users WHERE active = 1',})materializedView()
Section titled “materializedView()”Creates a materialized view definition.
materializedView(input: Omit<MaterializedViewDefinition, 'kind'>): MaterializedViewDefinition| Field | Type | Required | Description |
|---|---|---|---|
database | string | yes | Database name |
name | string | yes | Materialized view name |
to | { database: string; name: string } | yes | Target table for the view |
refresh | MaterializedViewRefresh | no | Refresh schedule — see Refreshable materialized views |
as | string | yes | SELECT query |
comment | string | no | View 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.
dictionary()
Section titled “dictionary()”Creates a ClickHouse dictionary definition — a key-value lookup structure backed by an external or in-database source, queried with dictGet().
dictionary(input: Omit<DictionaryDefinition, 'kind'>): DictionaryDefinitionimport { dictionary } from '@chkit/core'
const usersDict = dictionary({ database: 'default', name: 'users_dict', attributes: [ { name: 'id', type: 'UInt64' }, { name: 'name', type: 'String' }, { name: 'email', type: 'String', default: '' }, ], primaryKey: ['id'], source: `MYSQL(host 'db' port 3306 user 'reader' password '${process.env.MYSQL_PASSWORD}' db 'app' table 'users')`, layout: `HASHED()`, lifetime: `300`, comment: 'User lookup dictionary',})Required fields
Section titled “Required fields”| Field | Type | Description |
|---|---|---|
database | string | ClickHouse database name |
name | string | Dictionary name |
attributes | DictionaryAttribute[] | Attribute definitions (see Dictionary attributes) |
primaryKey | string[] | Key attribute name(s) — every entry must name a declared attribute |
source | string | Raw SOURCE(...) body, e.g. `MYSQL(host '...' password '...' ...)` |
layout | string | Raw LAYOUT(...) body, e.g. `HASHED()` or `COMPLEX_KEY_HASHED()` |
lifetime | string | Raw LIFETIME(...) body, e.g. `300` or `MIN 300 MAX 360` |
Optional fields
Section titled “Optional fields”| Field | Type | Description |
|---|---|---|
range | { min: string; max: string } | RANGE(MIN ... MAX ...) — required by RANGE_HASHED / COMPLEX_KEY_RANGE_HASHED layouts. Both min and max must name declared attributes |
settings | Record<string, string | number> | Raw SETTINGS(...) key/value pairs, e.g. { dictionary_use_async_executor: 1 } |
comment | string | Dictionary comment |
renamedFrom | { database?: string; name: string } | Previous identity for rename tracking |
Dictionary attributes
Section titled “Dictionary attributes”Each entry in the attributes array is a DictionaryAttribute.
| Field | Type | Description |
|---|---|---|
name | string | Attribute name |
type | string | ClickHouse type |
default | string | number | boolean | DEFAULT value for missing keys. Mutually exclusive with expression |
expression | string | EXPRESSION computed from source columns. Mutually exclusive with default |
hierarchical | boolean | Marks the attribute HIERARCHICAL |
bidirectional | boolean | Marks the attribute BIDIRECTIONAL — enables parent/child lookups in both directions. Only valid alongside hierarchical |
injective | boolean | Marks the attribute INJECTIVE |
isObjectId | boolean | Marks the attribute IS_OBJECT_ID (MongoDB sources) |
Credentials in source
Section titled “Credentials in source”Inline credentials in source (e.g. a MySQL/PostgreSQL password '...') should be interpolated from environment variables at schema-authoring time, the same way you’d handle any other secret in a TypeScript config file:
source: `MYSQL(host 'db' password '${process.env.MYSQL_PASSWORD}' ...)`,ClickHouse redacts inline passwords back to [HIDDEN] on introspection (SHOW CREATE DICTIONARY, system.dictionaries). A real password change diffs and migrates like any other field change. The one exception is a source that still carries the literal [HIDDEN] placeholder written by chkit pull — chkit never knows the real value in that case, so it excludes source from the diff entirely rather than risk rendering [HIDDEN] into DDL — see Pull: credential handling.
No ALTER DICTIONARY
Section titled “No ALTER DICTIONARY”ClickHouse has no ALTER DICTIONARY — every structural change to a dictionary is rendered as a single CREATE OR REPLACE DICTIONARY statement (atomic, dependency-safe). See Structural vs. alterable properties. A pure rename (renamedFrom with no other change) is the one exception — it renders as RENAME DICTIONARY, not a replace; see Dictionary rename.
Type system reference
Section titled “Type system reference”The codegen plugin maps ClickHouse types to TypeScript types using these rules:
| Category | ClickHouse Types | TypeScript Type |
|---|---|---|
| String-like | String, FixedString, Date, Date32, DateTime, DateTime64, UUID, IPv4, IPv6, Enum8, Enum16, Decimal* | string |
| Number | Int8, Int16, Int32, UInt8, UInt16, UInt32, Float32, Float64, BFloat16 | number |
| Large integers | Int64, Int128, Int256, UInt64, UInt128, UInt256 | string (default) or bigint |
| Boolean | Bool, Boolean | boolean |
| Wrappers | Nullable(T) | T | null |
| Wrappers | LowCardinality(T) | same as T |
| Composite | Array(T) | T[] |
| Composite | Map(K, V) | Record<K, V> |
| Composite | Tuple(T1, T2, ...) | [T1, T2, ...] |
| Aggregate | SimpleAggregateFunction(fn, T) | same as T |
| JSON | JSON | Record<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.
Rename support
Section titled “Rename support”chkit tracks renames to avoid destructive drop-and-recreate operations.
Table rename
Section titled “Table rename”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.
Column rename
Section titled “Column rename”Set renamedFrom on a column definition to rename a column:
columns: [ { name: 'user_email', type: 'String', renamedFrom: 'email' },]Dictionary rename
Section titled “Dictionary rename”Set renamedFrom on a dictionary definition to rename a dictionary. This emits a single RENAME DICTIONARY IF EXISTS ... TO ... statement instead of a drop_dictionary + create_dictionary pair:
const lookupDict = dictionary({ database: 'app', name: 'lookup_dict', // new name renamedFrom: { name: 'users_dict' }, // old name // ...})The database field in renamedFrom is optional and defaults to the dictionary’s current database.
Table, column, and dictionary renames can all be overridden by CLI flags: --rename-table, --rename-column, and --rename-dictionary.
Plugin configuration
Section titled “Plugin configuration”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:
| Key | Plugin | Fields | Description |
|---|---|---|---|
backfill | @chkit/plugin-backfill | timeColumn?: string | Time column for backfill WHERE clauses |
The plugins field is ignored by the diff engine — it does not affect migration planning or SQL generation.
Validation rules
Section titled “Validation rules”chkit validates schema definitions and throws a ChxValidationError if any issues are found:
- Duplicate object names — two definitions with the same
kind,database, andname - 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
- Ambiguous projection kind (
projection_ambiguous_kind) — a projection sets bothqueryandindex; use one or the other (see Projections) - Empty projection index (
projection_empty_index) — an index-only projection whoseindexexpression is empty - Primary key references missing column —
primaryKeyincludes a bare column name not incolumns(function expressions liketoDate(ts)are passed through to ClickHouse unchecked) - Order by references missing column —
orderByincludes a bare column name not incolumns(function expressions liketoStartOfHour(ts)are passed through to ClickHouse unchecked) - Empty codec chain (
codec_chain_empty) — acodecarray with no steps; provide at least one codec or omit the field - Multiple general codecs (
codec_chain_multiple_general) — more than one general codec in a chain; only one is allowed - Codec chain must end with a general codec (
codec_chain_must_end_with_general) — preprocessors must precede the single general codec (NONE,LZ4,LZ4HC,ZSTD,T64,GCD,ALP) - Dictionary missing primary key (
dictionary_missing_primary_key) — a dictionary’sprimaryKeyis empty - Dictionary primary key references missing attribute (
dictionary_primary_key_missing_attribute) — aprimaryKeyentry doesn’t name a declared attribute - Dictionary missing source/layout/lifetime (
dictionary_missing_source,dictionary_missing_layout,dictionary_missing_lifetime) — one of these raw-string fields is empty - Dictionary attribute default/expression exclusive (
dictionary_attribute_default_expression_exclusive) — an attribute sets bothdefaultandexpression - Dictionary range references missing attribute (
dictionary_range_missing_attribute) —range.min/range.maxdoesn’t name a declared attribute - Dictionary bidirectional requires hierarchical (
dictionary_bidirectional_requires_hierarchical) — an attribute setsbidirectionalwithouthierarchical
Structural vs. alterable properties
Section titled “Structural vs. alterable properties”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.
Dictionaries have no ALTER at all: any change to attributes, primaryKey, layout, lifetime, source (including a password change), or comment renders as a single CREATE OR REPLACE DICTIONARY (risk=caution) — except a source still carrying the [HIDDEN] introspection placeholder, which is excluded from the diff entirely (see Credentials in source). Removing a dictionary from schema emits DROP DICTIONARY (risk=danger, requires --allow-destructive).