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:
| Ids | What they are | Where they run |
|---|---|---|
| SQL101 | a post-synth check over the build output, both dialects | chant build and chant lint |
| SQLCH001 to SQLCH003 | lint rules over the TypeScript source | chant lint, the editor, and chant serve lsp |
| SQLCH101 to SQLCH127 | post-synth checks over the build output | chant build and chant lint |
| SQLCH200 to SQLCH250 | the change classifier’s rules | chant sql diff, chant sql plan, clickhouseApply; see Planning and the Change Classifier |
| SQLPG001 to SQLPG004 | Postgres lint rules over the TypeScript source | chant lint, the editor, and chant serve lsp |
| SQLPG101 to SQLPG126 | Postgres post-synth checks over the build output | chant build and chant lint |
| SQLPG200 to SQLPG270 | the Postgres change classifier’s rules | chant 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.
Lint rules
Section titled “Lint rules”| Id | Severity | Flags | What to change |
|---|---|---|---|
| SQLCH001 | error | The 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 instead | Fix the statement, or use the tag for the statement it holds |
| SQLCH002 | error | A 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 reported | Make the column non-nullable, or set allow_nullable_key = 1 and accept that NULL sorts as a value of its own |
| SQLCH003 | error | ${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 else | Reach the column through .columns |
| SQLPG001 | error | The 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 refused | Fix the statement, use the tag for the statement it holds, and name every index |
| SQLPG002 | error | ${users.kind} where ${users.columns.kind} was meant, as SQLCH003 for Postgres templates | Reach the column through .columns |
| SQLPG003 | warning | An 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 sequence | Interpolate the declared object: nextval(${ticketSeq}) renders nextval('app.ticket_seq'::regclass), as the catalog prints it, and keeps the dependency |
| SQLPG004 | error | An 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 set | Use 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 |
Post-synth checks
Section titled “Post-synth checks”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.
Engines, codecs and settings
Section titled “Engines, codecs and settings”These read the pinned server’s catalog, so a pin move can change their verdicts (see Where the ClickHouse Types Come From).
| Id | Severity | Flags | What to change |
|---|---|---|---|
| SQLCH101 | error | A 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 removed | Use an engine the server lists |
| SQLCH114 | error | A column CODEC names a codec the pinned server does not have (system.codecs) | Use a codec the server lists |
| SQLCH125 | error | A 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 checked | Use a parameter in range, or drop it for the default |
| SQLCH117 | warning | A MergeTree SETTINGS entry the pinned server marks obsolete. The server accepts it and ignores it, so the table does not behave as the setting suggests | Remove it |
| SQLCH118 | error | A 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 |
| SQLCH119 | warning | An engine the pinned catalog describes as deprecated or experimental, such as the Ordinary database engine | Move to the engine that replaced it (Atomic for Ordinary) |
| SQLCH120 | warning | A MergeTree engine written with the legacy positional arguments, MergeTree(date, (key), 8192) | Write a bare MergeTree with ORDER BY, PARTITION BY and SETTINGS |
Column types and functions
Section titled “Column types and functions”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.
| Id | Severity | Flags | What to change |
|---|---|---|---|
| SQLCH121 | error | A 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 read | Use a family the server lists, in its case |
| SQLCH122 | error | A 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 server | Give the family the parameters it takes |
| SQLCH123 | error | Nullable 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_type | Move Nullable inside: Array(Nullable(T)), LowCardinality(Nullable(T)) |
| SQLCH127 | error | A 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 calls | Fix the name, or declare the function with func |
Keys and columns
Section titled “Keys and columns”| Id | Severity | Flags | What to change |
|---|---|---|---|
| SQLCH102 | error | A PRIMARY KEY that is not a prefix of ORDER BY. The server refuses the table | Make the primary key a prefix of the sorting key |
| SQLCH103 | error | The 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 Nullable | Change the column’s type |
| SQLCH104 | error | An 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 columns | Declare the column, or fix the name |
| SQLCH105 | error | ORDER BY, PRIMARY KEY, PARTITION BY or SAMPLE BY lists a bare column name the table does not declare. An expression is left to the server | Declare the column, or fix the name |
| SQLCH106 | error | A skip index expression mentions a column the table does not declare. Projections are not checked | Declare the column, or fix the expression |
| SQLCH107 | error | A 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 server | Base the TTL on a Date or DateTime column |
| SQLCH111 | warning | A 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 column | Use LowCardinality(String) |
| SQLCH112 | warning | A 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 many | Partition by day or coarser, toYYYYMM(ts) usually |
| SQLCH113 | error | A MergeTree table with neither ORDER BY nor PRIMARY KEY. The server refuses the CREATE | Add a sort key, or ORDER BY tuple() for none |
| SQLCH124 | error | A table, view or dictionary declares a column twice: (id UInt64, id UInt32). The server refuses the CREATE | Remove or rename one |
| SQLCH126 | error | A 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 server | Fix the column name |
| SQLCH115 | warning | A 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 row | Say 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 |
Databases and views
Section titled “Databases and views”| Id | Severity | Flags | What to change |
|---|---|---|---|
| SQLCH108 | error | CREATE OR REPLACE TABLE in a database whose engine is not Atomic (or Replicated, which is Atomic underneath). Judged against a database the same build declares | Use an Atomic database, or drop OR REPLACE |
| SQLCH109 | warning | A materialized view that selects *. Its output columns become whatever the source has today, so adding a column to the source changes what the view writes | List the columns |
| SQLCH110 | warning | A 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 columns | Alias the select item to a target column, or add the column to the target |
| SQLCH116 | warning | A 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 environment | Name the DEFINER |
Both dialects
Section titled “Both dialects”| Id | Severity | Flags | What to change |
|---|---|---|---|
| SQL101 | error | Two 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 compared | Keep one declaration and import it where the other was used |
Postgres
Section titled “Postgres”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.
| Id | Severity | Flags | What to change |
|---|---|---|---|
| SQLPG101 | warning | A 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 skipped | Add a primary key |
| SQLPG102 | warning | A foreign key whose referencing columns no index leads with. A delete or key update on the referenced table scans this one | Create an index whose leading columns are the foreign key’s |
| SQLPG103 | warning | A smallserial, serial or bigserial column | Declare it GENERATED ALWAYS AS IDENTITY |
| SQLPG104 | warning | A timestamp column without time zone | Use timestamptz |
| SQLPG105 | warning | A json column | Use jsonb, unless the exact text must be kept |
| SQLPG106 | warning | A char(n) column, which pads with spaces | Use text, or varchar(n) for a length rule |
| SQLPG107 | warning | A money column, whose meaning depends on lc_monetary | Use numeric(p, s) and a currency column |
| SQLPG108 | warning | A varchar(n) with a habitual limit (50, 100, 128, 200, 250, 255, 256, 500, 512, 1000, 1024) rather than a rule | Use text, with a CHECK where the length is a rule |
| SQLPG109 | warning | An index that duplicates a primary key, a unique constraint or another index | Drop the duplicate |
| SQLPG110 | warning | A btree index that is a leading prefix of another index on the same table | Drop the prefix index; the wider one serves the same queries |
| SQLPG111 | warning | A materialized view with no unique index, so REFRESH ... CONCURRENTLY is refused and every refresh blocks readers | Add a unique index over plain columns, or use RefreshedView |
| SQLPG112 | warning | A 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 table | Say in a COMMENT ON what it holds and how it is protected |
| SQLPG113 | warning | An object in public while the project declares schemas of its own | Qualify it with a declared schema |
| SQLPG114 | error | A storage parameter in WITH (...) that the object’s kind does not take, that the catalog does not know, or that the target major removed | Fix the name, or use a parameter the target major has |
| SQLPG115 | warning | A 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 |
| SQLPG116 | error | An extension the target major no longer ships, such as adminpack from 17 | Remove it, or target an older major |
| SQLPG117 | warning | A view without security_invoker, which runs with its owner’s privileges and bypasses the reader’s row-level security | Set security_invoker = true in its WITH options |
| SQLPG118 | warning | A table using INHERITS where declarative partitioning replaced it | Use PARTITION BY and PARTITION OF |
| SQLPG119 | error | A 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 not | Fix the name, declare the type, or declare the extension that provides it |
| SQLPG120 | error | A 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 own | Drop the modifier, or bring it into range |
| SQLPG121 | error | An identity column whose type is not smallint, integer or bigint (id text GENERATED ALWAYS AS IDENTITY) | Make it smallint, integer or bigint |
| SQLPG122 | error | A 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 |
| SQLPG123 | error | A 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 skipped | Fix the name, or write ${table.columns.name} so the build checks it |
| SQLPG124 | error | A 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 type | Reference a declared column with a type of the same category |
| SQLPG125 | error | An 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 objects | Use a method and operator class the target major has, or declare the extension |
| SQLPG126 | error | A 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 extension | Fix the name, declare the function, or declare the extension that provides it |
Suppressing a finding
Section titled “Suppressing a finding”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.