Type Checking¶
The Postgres Language Server checks your SQL against your database schema without running it. As you type, it reports what Postgres would reject: tables and columns that don't exist, ambiguous column references, operators and functions that don't match their argument types, invalid casts, and values that can't be assigned to a column.
How it Works¶
When the language server connects to your database, it loads a snapshot of its schema: tables, columns, types, functions, operators, and casts. It then applies the statements of the current file on top of that snapshot, so a table created earlier in a migration is known to the statements after it. Nothing is executed against the database.
Each statement is checked against this catalog by the rules of the typecheck group. The type checks follow Postgres' own algorithms for coercion, overload resolution, and common types, so they agree with what Postgres does at runtime.
The rules only report what Postgres would certainly reject. Whenever the language server can't be sure, for example after a DO block or a CREATE EXTENSION whose effects it can't see, it stays silent. They are tested against Postgres' own regression suite for versions 15 to 18.
What Gets Checked¶
| Rule | Reports |
|---|---|
unknownRelation |
SELECT * FROM user when the table is named users |
unknownColumn |
SELECT user_naem FROM users |
unknownSchema, unknownFunction, unknownType |
references to schemas, functions, and types that don't exist |
ambiguousColumn |
a column that exists in several joined tables |
missingFromClauseEntry |
t.col without t in the FROM clause |
insertColumnMismatch |
more values than target columns, or the other way round |
operatorTypeMismatch |
1 + now() |
functionArgumentMismatch |
length(1), or a call that matches several overloads |
invalidCast |
now()::int |
assignmentTypeMismatch |
INSERT INTO t (qty) VALUES (now()) |
functionReturnTypeMismatch |
a LANGUAGE sql function whose final statement doesn't return what the function declares |
Configuration¶
The typecheck rules are configured like any other linter rule or group:
{
"linter": {
"rules": { "invalidCast": "warn" },
"groups": { "typecheck": "error" }
}
}
typecheck.enabled: false turns type checking off entirely.
You can configure the schemas included in the search path for type checking:
{
"typecheck": {
"searchPath": ["public", "app_*", "auth"]
}
}
The searchPath supports:
- Exact schema names (e.g., "public")
- Glob patterns (e.g., "app_*" to match app_users, app_products, etc.)
- The order matters - schemas are searched in the order specified
Even if not specified, the LSP will always search"public" in last position. A SET search_path in the file takes effect for the statements after it.
Requirements¶
Type checking needs a database connection to load the schema. Without one, the typecheck rules are skipped. The rules never send your statements to the database.
Legacy: EXPLAIN-based checking¶
Before the typecheck rules, the language server checked statements by running EXPLAIN on them. This check still runs for SELECT, INSERT, UPDATE, and DELETE statements that only reference objects that exist unchanged in the database, and reports the errors Postgres returns. It needs permission to prepare statements in your database.
Warning
The EXPLAIN-based check is deprecated and will be removed in the next major release. The typecheck rules replace it.