Limits
TypeSQL runs inside the TypeScript compiler, so it works within the compiler's budgets. This page lists what it can't do, or does only in part, and what to do instead.
When you hit a limit, sql.as<Row>() lets you state the row type yourself.
Toolchain
- TypeSQL needs TypeScript 7.1 or later (the native compiler;
7.1.0-betaworks). - It needs
strictNullChecks, which"strict": trueturns on. Without it, everysql()call and migration.add()is an error that says so, and with"skipLibCheck": falseTypeScript also reports errors inside TypeSQL's own declaration files. - It models PostgreSQL and SQLite. MySQL is planned. SQLite's own limits are listed in SQLite.
- The editor completions (VS Code, Neovim) use APIs that are new in TypeScript 7.1 and may still change before 7.1 is final.
Statement size
The lexer handles up to 4096 tokens per statement (a generated table in the engine sets this), but some shapes hit TypeScript's own depth limits well before that. Measured with TypeScript 7.1 (tsrs gives the same results):
Construct Largest that compiles a || a || …,a + a + …about 85 terms select list of id + n AS cnabout 300 columns (~1800 tokens) nested derived tables ( FROM (SELECT … FROM (…)))11 levels UNION ALLbranches42 IN (1, 2, …)items, multi-rowVALUESrowsabout 1000 AND/ORchains (the parser balances them), 20 joins, 20 CTEs and aCASEwith 150 branches are fine.Past these sizes the compiler reports its own error instead of a SQL error: TS2589 ("Type instantiation is excessively deep and possibly infinite"), TS5114 or TS5115. Split the statement, or give its row type with
sql.as<Row>(), which skips the analysis.In
migrations(), keep one migration per.add(): one text is limited to about 4000 tokens, and past that the error is a raw TS2589.
SQL text
- Only literal SQL is analysed: a string literal, or a template literal whose substitutions are compile-time constants. Anything else is an error; see Parameters.
- Tagged templates (
sql`…`) are not supported, because TypeScript does not expose their text to the type system. - Mixing
:nameand$nparameters in one statement is an error. - A colon directly followed by a name is a named parameter, also in an array slice:
a[1:n]isa[1$1]. Writea[1: n]for a slice up to the columnn.
Fragments and lists
sql.combineover an array checks the statement once, with every distinct element once. Its cost grows with the number of distinct elements, not with the number of combinations (80 distinct sort elements cost about 0.2M instantiations).- In a select list (
SELECT ${cols}), such a list counts every element as selected. - A fragment whose text is a union (
sql.id(name)withname: "a" | "b") is checked once per member. So two such fragments in one statement costn × mchecks. A genericsql.idoverRelationName<DB>checks the statement against every table and view. For lists, usesql.combineover an array, which costs one check. sql.setandsql.valuesare checked likesql.combineover an array, with every key of the input type once.- Some calls throw at runtime because the database would reject the SQL:
sql.combinewith no items,sql.setwith no defined value, andsql.valueswith an empty array or several rows without values. Asql.param()value also throws when it is used in a second statement; wrap a reused value in a fragment. - The types of fragments and queries are also string types, so TypeScript can keep
${fragment}in a template's type. As a result, TypeScript accepts one where astringis expected, such as a$nargument for atextparameter. Passing a fragment or query as a parameter value, tosql.param()or tosql.id()throws at runtime.
Defaults and generated columns
The schema does not tell identity GENERATED ALWAYS columns apart from other columns with a default. So writing one is accepted, though Postgres rejects it without OVERRIDING SYSTEM VALUE. The DDL reference lists the other DDL limits.
JSON
- Typed JSON (see JSON) needs literal keys and paths.
json_build_object(col, …)or-> $1on an object gives plain JSON, or the union of all members. - Only the string literals
'[]','{}','null','true'and'false'keep their value when they meet typed JSON (coalesce(json_agg(x), '[]')). Any other literal makes the resultunknown. - Functions that rewrite JSON (
jsonb_set,||,jsonb_strip_nulls,json_populate_record,jsonb_path_query,json_each) return plain JSON. So doesrow_to_json(ROW(…)). - A
Json<T>column'sTis trusted, not checked against the database. Positional and named parameters for it are typedTbut passed to the driver as they are; onlysql.json()makes arrays work with node-postgres. - A
sql.json(value)passed as a positional or named argument is not checked: a JSON parameter accepts anysql.json()value there. A raw object passed that way is checked againstT. - Inline values (
sql.param(),sql.json()) and values in fragments,sql.setandsql.valuesare checked against the parameter's type, with some leniency. A plainstring,numberorbooleanis checked by kind only, so anystringis accepted for an enum column. An object with an index signature is not checked for missing keys. An array's elements are checked against the union of the parameter's element types, so tuples are checked loosely. Some values are checked by kind only (a union of more than 64 literals, an object with more than 48 keys, a union of more than 8 object types, a function) or not at all (anything nested more than six levels deep). - A plain
jsonbparameter (typedunknown) refuses objects with alengthkey. Wrap those insql.json(). - Inside JSON,
int8andnumericare numbers.JSON.parserounds integers beyond 2^53, and afloat8/numericthat isNaNorInfinityis serialized as a string. - Elements of array columns, and of arrays that columns and functions return, are typed non-null, since PostgreSQL doesn't record whether an array holds NULLs. Arrays built from nullable values have nullable elements:
array_agg(x)over a nullablex,ARRAY[…]andARRAY(SELECT …)with a nullable item,array_appendand the other array functions given a nullable element, andregexp_match. Where the engine loses track of an array's elements (a view column created by DDL, a chained slice such asx[1][1:2]), they are typed non-null. - Arrays are typed one-dimensional. PostgreSQL doesn't record dimensions in a type (
int[][]is the same type asint[]), so a multi-dimensional value such asARRAY[[1, 2], [3, 4]]is typednumber[]but comes back nested ([[1, 2], [3, 4]]). Assert the row type withsql.as<Row>()where you store such values. - In
JSON_OBJECT, write pairs ask VALUE vork: v. Ink:v, the:vis read as a named parameter whenvis a name.
Types and hovers
- Editor hovers list a row's keys in alphabetical order, because TypeScript 7 orders the keys of computed object types. The rows the driver returns keep the
SELECTorder. - A whole-row reference (
to_json(t),(t).*) is nullable when all of the row's columns are, since that is how the outer side of a join looks. So a table whose columns are all nullable is typed as a nullable row, even when it is not joined. - Some results can't be known statically: rows of
FETCH(the cursor is declared in another statement) and results of user-defined operators (unknown). - With
CasePlugin, two columns that map to the same key (user_idand"userId") are typed as the union of both; at runtime the later one wins. Alias one of them to keep both. - Some driver types differ from their row type at runtime, such as
numeric[]under node-postgres. See Type mapping.
ORM schemas
The Kysely and Prisma limits are on their sections of From Kysely, Drizzle or Prisma. For Drizzle:
- Generated-always columns count as having a default. Postgres rejects an
INSERTthat sets one, but TypeSQL doesn't. $onUpdatecolumns look like they have a default, soINSERTdoesn't require them..$type<T>()is ignored; rows use the type map (jsonb().$type<T>()isunknown).text({ enum: [...] })is plaintext: in Drizzle, those values are only a TypeScript constraint.
On SQLite, text({ enum }) and text({ mode: "json" }).$type<T>() keep their types, because SQLite has no enum or JSON type to tell apart. With Prisma on SQLite, a computed column (max(created_at)) has no declared type, so Prisma returns SQLite's raw value (a string) where TypeSQL says Date; cast or convert it.
Unsupported syntax
Unsupported syntax is always a compile error, never a silent any. Notable gaps:
- tracking which views depend on which tables;
- column alias lists on plain tables.