Skip to content

Lint Rules and Checks

The sql lexicon’s rule ids all start with SQL. Rules that hold in every dialect are SQL plus three digits. ClickHouse rules are SQLCH plus three digits and Postgres rules SQLPG plus three digits, each grouped by the hundreds digit:

IdsWhat they areWhere they run
SQL101a post-synth check over the build output, both dialectschant build and chant lint
SQLCH001 to SQLCH003lint rules over the TypeScript sourcechant lint, the editor, and chant serve lsp
SQLCH101 to SQLCH127post-synth checks over the build outputchant build and chant lint
SQLCH200 to SQLCH250the change classifier’s ruleschant sql diff, chant sql plan, clickhouseApply; see Planning and the Change Classifier
SQLPG001 to SQLPG004Postgres lint rules over the TypeScript sourcechant lint, the editor, and chant serve lsp
SQLPG101 to SQLPG126Postgres post-synth checks over the build outputchant build and chant lint
SQLPG200 to SQLPG270the Postgres change classifier’s ruleschant sql diff, chant sql plan, postgresApply; see Postgres Locks and the Change Classifier

The All Rules page is the generated list. This page says what each rule means. Every post-synth check has an entry in the lexicon’s auditCatalog(), which is what chant audit reports it under.

IdSeverityFlagsWhat to change
SQLCH001errorThe DDL in a database, table or view template does not parse, or the template holds a different statement than its tag (a CREATE VIEW in table). Reported at the token, as a line and column in the .ts file. Lint reads every interpolation as a reference, so an error inside text a string interpolation supplies is reported by the build insteadFix the statement, or use the tag for the statement it holds
SQLCH002errorA Nullable column, LowCardinality(Nullable(...)) included, as a key part of ORDER BY or PRIMARY KEY. ClickHouse refuses the table (“Sorting key contains nullable columns”) unless it sets allow_nullable_key = 1. A function of the column is not reportedMake the column non-nullable, or set allow_nullable_key = 1 and accept that NULL sorts as a value of its own
SQLCH003error${events.kind} where ${events.columns.kind} was meant, for an entity the file declares or imports from a project file. kind, props, lexicon, entityType, sqlName and dependsOn are the entity’s own fields, so the tag would splice in their value as SQL text and the statement would mean something elseReach the column through .columns
SQLPG001errorThe DDL in a Postgres template does not parse, the template holds another statement than its tag (a CREATE INDEX in table) or a second CREATE, or an index has no name. Reported at the token, as a line and column in the .ts file; a default that is not a b_expr (DEFAULT 1 IN (1, 2)) is reported at IN. An index’s name is its identity on the server, so an unnamed one, which Postgres would name itself, is refusedFix the statement, use the tag for the statement it holds, and name every index
SQLPG002error${users.kind} where ${users.columns.kind} was meant, as SQLCH003 for Postgres templatesReach the column through .columns
SQLPG003warningAn object named in a string where Postgres reads a regclass: nextval('app.ticket_seq'), currval, setval, or 'app.t'::regclass. A name in a string records no reference, so the build can create the table before the sequenceInterpolate the declared object: nextval(${ticketSeq}) renders nextval('app.ticket_seq'::regclass), as the catalog prints it, and keeps the dependency
SQLPG004errorAn extension template creates an extension the provider named in sql.provider (or in the one provider every profile names) does not allow: CREATE EXTENSION pg_stat_monitor on Neon. The lists are snapshots of each provider’s documentation, dated in providerData(provider).sources; a provider whose list is partial (Supabase) is not reported for a name outside it. Silent when no provider is setUse an extension the provider lists, change sql.provider, or disable the rule on the line if the provider has added the extension since the list was read

What the checks hold a declaration to, in both dialects: a name the pinned catalog answers for (a type, a function, an engine, a codec, an access method, an operator class, a setting) is checked against the target release everywhere it appears; a column name is checked only where it is a bare name in a list (a key, a sort key, an engine argument, an index’s columns, a grant’s column list); an expression (a CHECK, a generated column, a policy, a view’s query, an index expression, PARTITION BY toYYYYMM(att), a TTL, a projection) and a plain-text name of an object the project does not declare (REFERENCES billing.accounts (id), billing.money) are left to the server.

These read the pinned server’s catalog, so a pin move can change their verdicts (see Where the ClickHouse Types Come From).

IdSeverityFlagsWhat to change
SQLCH101errorA table, materialized view or database names an engine the pinned server does not have (system.table_engines, system.database_engines): a misspelling, or an engine a release removedUse an engine the server lists
SQLCH114errorA column CODEC names a codec the pinned server does not have (system.codecs)Use a codec the server lists
SQLCH125errorA codec’s parameters do not fit it: more than it takes (LZ4(1)), a level outside its range (ZSTD(99), LZ4HC(13)), or a byte width other than 1, 2, 4 or 8 (Delta(3)). The ranges are the lexicon’s codec overlay; the experimental codecs whose parameters are undocumented (ALP, SZ3, ZXC, Quantized) are not checkedUse a parameter in range, or drop it for the default
SQLCH117warningA MergeTree SETTINGS entry the pinned server marks obsolete. The server accepts it and ignores it, so the table does not behave as the setting suggestsRemove it
SQLCH118errorA MergeTree SETTINGS entry the pinned server does not have, usually a typo or a setting a later release added. The CREATE fails with “Unknown setting”Fix the name, or drop the setting
SQLCH119warningAn engine the pinned catalog describes as deprecated or experimental, such as the Ordinary database engineMove to the engine that replaced it (Atomic for Ordinary)
SQLCH120warningA MergeTree engine written with the legacy positional arguments, MergeTree(date, (key), 8192)Write a bare MergeTree with ORDER BY, PARTITION BY and SETTINGS

These read the pinned catalog too: the type families and their aliases (system.data_type_families), the functions and aggregate combinators (system.functions, system.aggregate_function_combinators), and the lexicon’s overlay of type parameters, which no system table holds.

IdSeverityFlagsWhat to change
SQLCH121errorA column type names a family the pinned server does not have: UInt46, Nullabel(String), or uint64, since a family name is case-sensitive unless the catalog marks it case-insensitive (bigint, datetime and text are). Aliases count (BIGINT UNSIGNED). Checked at every depth: inside Array, Map, Tuple, Nested, Nullable, LowCardinality, Variant, and the argument types of AggregateFunction and SimpleAggregateFunction. Table, view and dictionary columns are all readUse a family the server lists, in its case
SQLCH122errorA type’s parameters do not fit its family: a parameter on a family that takes none (UUID(4)), a required one missing (FixedString, Enum8), too many (Decimal(10, 2, 1)), or a number outside its range (Decimal(100, 2), DateTime64(12)). The server takes and ignores a MySQL display width on an integer (INT(11)), a precision on a float and a length on a string (VARCHAR(255)), so those pass. JSON and Dynamic settings are left to the serverGive the family the parameters it takes
SQLCH123errorNullable around Array, Map, Nested, LowCardinality, Variant or another Nullable, which the server refuses. Nullable(Tuple(...)) is not reported: the server takes it when its profile sets enable_nullable_tuple_typeMove Nullable inside: Array(Nullable(T)), LowCardinality(Nullable(T))
SQLCH127errorA function the pinned server does not have, called in a column’s DEFAULT, MATERIALIZED, ALIAS, EPHEMERAL or TTL expression, in ORDER BY, PRIMARY KEY, PARTITION BY, SAMPLE BY or the table TTL, or in a skip index expression: DEFAULT noww(), PARTITION BY toYYYYMMM(at). A call is a name directly followed by (, outside string literals and quoted names. A name counts when the catalog has it (in its case, aliases included), when it is an aggregate function with combinator suffixes (countIf, sumState, uniqMerge), or when the project declares it with func. Keywords that take parentheses (IN, EXISTS, ARRAY, INTERVAL) and the forms the server’s parser reads itself (EXTRACT(... FROM ...), TRIM(BOTH ...), SUBSTRING, POSITION, DATE_ADD, DATE_DIFF) are not callsFix the name, or declare the function with func
IdSeverityFlagsWhat to change
SQLCH102errorA PRIMARY KEY that is not a prefix of ORDER BY. The server refuses the tableMake the primary key a prefix of the sorting key
SQLCH103errorThe version, sign or is_deleted column of a ReplacingMergeTree, CollapsingMergeTree or VersionedCollapsingMergeTree table has a type the engine does not accept. A version is a UInt, Date, DateTime or DateTime64; is_deleted is UInt8; a sign is Int8; none may be NullableChange the column’s type
SQLCH104errorAn engine argument names a column the table does not declare: a Replacing or Collapsing table’s version, sign or is_deleted column, or a Summing or Coalescing table’s summed columnsDeclare the column, or fix the name
SQLCH105errorORDER BY, PRIMARY KEY, PARTITION BY or SAMPLE BY lists a bare column name the table does not declare. An expression is left to the serverDeclare the column, or fix the name
SQLCH106errorA skip index expression mentions a column the table does not declare. Projections are not checkedDeclare the column, or fix the expression
SQLCH107errorA TTL that is a bare column, or a column plus an INTERVAL, on a column that is not a Date or DateTime. A function call is left to the serverBase the TTL on a Date or DateTime column
SQLCH111warningA String column that a CHECK constraint limits to at most 100 values (col IN (...) or col = 'a' OR col = 'b'). LowCardinality dictionary-encodes such a columnUse LowCardinality(String)
SQLCH112warningA PARTITION BY finer than a day: a bare DateTime column, or a sub-day truncation such as toStartOfHour. Every partition is a separate set of parts, and the server stops inserts when a table has too manyPartition by day or coarser, toYYYYMM(ts) usually
SQLCH113errorA MergeTree table with neither ORDER BY nor PRIMARY KEY. The server refuses the CREATEAdd a sort key, or ORDER BY tuple() for none
SQLCH124errorA table, view or dictionary declares a column twice: (id UInt64, id UInt32). The server refuses the CREATERemove or rename one
SQLCH126errorA GRANT column list names a column the interpolated table does not declare: GRANT SELECT(kindd) ON ${events}. A target written as a plain name, and a view that takes its columns from its SELECT, are left to the serverFix the column name
SQLCH115warningA column named like a secret or personal data (password, token, api_key, ssn, credit_card, iban and similar) with no COMMENT, no TTL and no encryption codec. The table’s own TTL counts for the whole rowSay what the column holds in a COMMENT, bound how long it is kept with a TTL, or encrypt it with AES_128_GCM_SIV or AES_256_GCM_SIV
IdSeverityFlagsWhat to change
SQLCH108errorCREATE OR REPLACE TABLE in a database whose engine is not Atomic (or Replicated, which is Atomic underneath). Judged against a database the same build declaresUse an Atomic database, or drop OR REPLACE
SQLCH109warningA materialized view that selects *. Its output columns become whatever the source has today, so adding a column to the source changes what the view writesList the columns
SQLCH110warningA materialized view writes an output column its TO target does not declare. ClickHouse matches the columns by name, so that column is not stored. Judged against a target the same build declares, and only when the select list names its columnsAlias the select item to a target column, or add the column to the target
SQLCH116warningA view declared SQL SECURITY DEFINER without a DEFINER. Its access then depends on whoever ran the CREATE, so the same declaration grants different access in each environmentName the DEFINER
IdSeverityFlagsWhat to change
SQL101errorTwo exports declare the same object: the same kind under the same qualified name, shop.events or app.orders twice. The build writes both CREATE statements, so the second fails on the server or, under OR REPLACE, replaces the first. The message names both exports. Grants, row policies, triggers and policies (named per table) and Postgres functions and procedures (which overload) are not comparedKeep one declaration and import it where the other was used

The Postgres checks read the build output, and the ones that depend on the version read the major it records (postgresMajor: sql.postgresMajor, else 18) against the per-major catalogs (see The Postgres Pin and Supported Majors). Several follow rules of squawk, strong_migrations and the PostgreSQL wiki’s “Don’t Do This” page, credited in the audit catalog.

IdSeverityFlagsWhat to change
SQLPG101warningA table with no primary key, and no UNIQUE constraint over NOT NULL columns. Logical replication then needs a REPLICA IDENTITY to send its updates and deletes, and nothing names one row. Partitions, typed tables and LIKE copies are skippedAdd a primary key
SQLPG102warningA foreign key whose referencing columns no index leads with. A delete or key update on the referenced table scans this oneCreate an index whose leading columns are the foreign key’s
SQLPG103warningA smallserial, serial or bigserial columnDeclare it GENERATED ALWAYS AS IDENTITY
SQLPG104warningA timestamp column without time zoneUse timestamptz
SQLPG105warningA json columnUse jsonb, unless the exact text must be kept
SQLPG106warningA char(n) column, which pads with spacesUse text, or varchar(n) for a length rule
SQLPG107warningA money column, whose meaning depends on lc_monetaryUse numeric(p, s) and a currency column
SQLPG108warningA varchar(n) with a habitual limit (50, 100, 128, 200, 250, 255, 256, 500, 512, 1000, 1024) rather than a ruleUse text, with a CHECK where the length is a rule
SQLPG109warningAn index that duplicates a primary key, a unique constraint or another indexDrop the duplicate
SQLPG110warningA btree index that is a leading prefix of another index on the same tableDrop the prefix index; the wider one serves the same queries
SQLPG111warningA materialized view with no unique index, so REFRESH ... CONCURRENTLY is refused and every refresh blocks readersAdd a unique index over plain columns, or use RefreshedView
SQLPG112warningA column named like a secret or personal data (password, token, api_key, ssn, email, phone, address and similar) with no comment on it or on its tableSay in a COMMENT ON what it holds and how it is protected
SQLPG113warningAn object in public while the project declares schemas of its ownQualify it with a declared schema
SQLPG114errorA storage parameter in WITH (...) that the object’s kind does not take, that the catalog does not know, or that the target major removedFix the name, or use a parameter the target major has
SQLPG115warningA feature newer than the target major: a storage parameter by the catalog’s first major, NULLS NOT DISTINCT (15), NOT ENFORCED and virtual generated columns (18)Avoid the feature, or raise sql.postgresMajor
SQLPG116errorAn extension the target major no longer ships, such as adminpack from 17Remove it, or target an older major
SQLPG117warningA view without security_invoker, which runs with its owner’s privileges and bypasses the reader’s row-level securitySet security_invoker = true in its WITH options
SQLPG118warningA table using INHERITS where declarative partitioning replaced itUse PARTITION BY and PARTITION OF
SQLPG119errorA column, array element, domain base or sequence AS type the target major does not have: numerik(12,2), intt[], app.mood when the project declares schema app and no type mood. Every spelling Postgres accepts counts: the catalog’s pg_type and format_type() names (int4, integer, timestamptz), the grammar’s own (int, decimal, double precision, time with time zone, bit varying), and types the project declares (type, domain, a table’s row type) by ${} or by name. A type in a schema the project does not declare is left alone. A warning instead when the project declares an extension, since extension types are not in the core catalog; citext, geometry, hstore, vector and the other types of common extensions pass once their extension is declared, and are an error naming the extension when it is notFix the name, declare the type, or declare the extension that provides it
SQLPG120errorA type modifier that does not fit: one on a type that takes none (bigint(8)), numeric precision outside 1 to 1000 or scale outside -1000 to 1000 (0 to the precision before 15), varchar(n) or char(n) outside 1 to 10485760, timestamp, time or interval precision outside 0 to 6, bit(n) below 1, float(p) outside 1 to 53. A declared or extension type’s modifiers are its ownDrop the modifier, or bring it into range
SQLPG121errorAn identity column whose type is not smallint, integer or bigint (id text GENERATED ALWAYS AS IDENTITY)Make it smallint, integer or bigint
SQLPG122errorA table that declares a column twice ((id bigint, id int)), or an enum that lists a label twice (ENUM ('a', 'a'))Remove or rename the repeat
SQLPG123errorA bare column name the table does not declare in PRIMARY KEY, UNIQUE or their INCLUDE, a foreign key’s own columns, an index’s columns or INCLUDE on a ${} table, or a GRANT/REVOKE column list on a ${} table. A partition or an inheriting table reads its declared parent’s columns; a table with LIKE, OF type or a plain-text parent is skippedFix the name, or write ${table.columns.name} so the build checks it
SQLPG124errorA foreign key to a ${} table that names a column the table does not declare (REFERENCES ${customers} (idd)), or whose column types do not compare: their pg_type categories differ (text to bigint), or, in the catch-all user-defined category, the types do (uuid to bytea). integer to bigint passes. A domain compares as its base typeReference a declared column with a type of the same category
SQLPG125errorAn index access method (USING btreee) or operator class (numeric_opz) the target major does not have, or an operator class that is not for the index’s method (jsonb_path_ops on a btree index). An extension’s (gin_trgm_ops from pg_trgm, hnsw from vector) passes when the project declares that extension and is an error naming it when it does not; a warning when the project declares some extension and the name is in neither the catalog nor the lexicon’s list of common extension objectsUse a method and operator class the target major has, or declare the extension
SQLPG126errorA function the target major does not have, called in a column DEFAULT, a generated column, a table’s or a domain’s CHECK, or an index expression: DEFAULT noww(), or uuidv7() before 18. A name counts when it is in the major’s pg_proc, declared by the project (func, procedure), a type name used as a cast (int4(x)), or qualified with a schema the project does not declare. Only a name followed by ( outside a string literal is a call; COALESCE, NULLIF, GREATEST, LEAST, CAST, EXTRACT, ROW, ARRAY, EXISTS, IN, ANY, ALL, SOME and the key word forms of OVERLAY, POSITION, SUBSTRING and TRIM are grammar, not functions. A warning when the project declares an extensionFix the name, declare the function, or declare the extension that provides it

lint.rules in chant.config.ts sets any lint rule or post-synth check id to "off", "warning" or "error". A lint rule can also be turned off on one line with a chant-disable comment. A post-synth check cannot, because its finding names an object in the build output rather than a line of source, so lint.rules is the only way to suppress one. See Rule Configuration.