A PostgreSQL language server. Provides completion, hover, and
diagnostics for SQL — both inside .sql files and inside SQL string
literals embedded in .go source.
Schema is read from a directory of CREATE TABLE files using
libpg_query (via
wasilibs/go-pgquery, so no
CGO is required).
Download an archive for your platform from the
releases page,
extract it, and drop pgls somewhere on your $PATH. Builds
are produced for darwin / linux / windows × amd64 / arm64.
Requires Go 1.25 or later:
go install github.com/winebarrel/pgls@v0.5.0Point pgls at a directory containing your DDL:
pgls -schema ./db/schemaOr, when launched from an editor, configure it via LSP
initializationOptions (relative paths resolve against the workspace
root):
{ "initializationOptions": { "schemaDir": "db/schema" } }If the editor doesn't expose a clean way to pass initializationOptions
(classic Vim, plain CLI), drop a .pgls.json at the workspace root and
pgls will pick it up automatically:
{
"schemaDir": "db/schema",
"sqlFunctions": [
{ "name": "Query", "argIndex": 0 },
{ "name": "QueryContext", "argIndex": 1 },
{ "name": "Get", "argIndex": 1 },
{ "name": "Select", "argIndex": 1 }
]
}Each entry names a Go function or method (matched by selector, no
type resolution) and argIndex (0-origin) tells pgls which positional
argument carries the SQL string. Both name and argIndex are
required per entry — a missing argIndex is rejected at validation
time rather than silently defaulting to 0 (which would be wrong for
*Context methods, where the SQL lives at arg 1). Method names are
matched without a receiver, so "Query" covers db.Query, tx.Query,
*sql.DB.Query all together.
sqlFunctions itself is optional: omit the whole field to inherit
the default database/sql set (Query, QueryRow, Exec,
Prepare at arg 0, plus their *Context variants at arg 1), or set
it to [] to disable function-call detection so only marker comments
fire. Per-entry omission of argIndex is not the way to express
either — use one of those two switches instead.
.pgls.json is the project's authoritative schema location and
wins over initializationOptions when both are present —
editors can't accidentally point a colleague's pgls at the wrong
directory by leaking a stale per-machine setting. Use
initializationOptions.schemaDir only as an ad-hoc override for
projects that don't ship a .pgls.json.
The schemaDir field of .pgls.json must resolve to a path inside
the workspace — absolute paths and .. escapes are rejected, so
cloning an unfamiliar repo can't make pgls walk arbitrary .sql
files elsewhere on disk. The -schema CLI flag stays unrestricted
because the user supplies it explicitly.
vim.lsp.start({
name = 'pgls',
cmd = { 'pgls' },
root_dir = vim.fn.getcwd(),
init_options = { schemaDir = 'db/schema' },
filetypes = { 'go', 'sql' },
})Vim (with vim-lsp)
if executable('pgls')
augroup pgls_register
autocmd!
autocmd User lsp_setup call lsp#register_server({
\ 'name': 'pgls',
\ 'cmd': {server_info -> ['pgls']},
\ 'allowlist': ['go', 'sql'],
\ 'initialization_options': { 'schemaDir': 'db/schema' },
\ 'root_uri': {server_info -> lsp#utils#path_to_uri(
\ lsp#utils#find_nearest_parent_file_directory(
\ lsp#utils#get_buffer_path(), ['.git/', 'go.mod']))},
\ })
augroup END
endifFor coc.nvim add an entry to :CocConfig:
{
"languageserver": {
"pgls": {
"command": "pgls",
"filetypes": ["go", "sql"],
"rootPatterns": ["go.mod", ".git/"],
"initializationOptions": { "schemaDir": "db/schema" }
}
}
}[language-server.pgls]
command = "pgls"
config = { schemaDir = "db/schema" }
[[language]]
name = "go"
language-servers = ["gopls", "pgls"]
[[language]]
name = "sql"
language-servers = ["pgls"]Install pgls
from the VS Code Marketplace
(source). The extension
spawns the pgls binary on .go and .sql files; configure the
schema directory per-workspace in .vscode/settings.json:
{ "pgls.schemaDir": "db/schema" }-
Completion — table and column names, scoped to the cursor's SQL clause:
- after
FROM/JOIN/INTO/UPDATE: tables only - after
SELECT/WHERE/SET/ON: columns from FROM-tables alias.resolves the alias and offers that table's columns only
- after
-
Goto-definition — jump from a table reference to its
CREATE TABLEline, or from a qualified column (u.email) to the column's row in the DDL. Aliases are resolved to the underlying table. -
Hover — markdown summary of the identifier under the cursor: table layout for tables,
table.column \type`for columns, alias resolution foru.email`-style references. -
Diagnostics — flags
FROM/JOINreferences to unknown tables, qualifiers that resolve to neither a table nor an alias, and qualified columns missing from the resolved table. -
Go-aware — inside
.gofiles, pgls treats a string literal as SQL when one of:- It carries a JetBrains-style
language=sql(orlanguage=postgresql) marker comment on the line directly above. Block-comment form (/* language=sql */) and any case works. - It's passed to a recognized SQL method. Defaults cover
database/sql(Query,QueryRow,QueryContext,QueryRowContext,Exec,ExecContext,Prepare,PrepareContext); override withsqlFunctionsin.pgls.jsonorinitializationOptionsto add your own (e.g. sqlx'sGet/Select/NamedExec) or set it to[]to disable function-call detection entirely.
// marker form // language=sql q := `SELECT id, email FROM users WHERE id = $1` // function-call form (no comment needed) rows, err := db.Query(`SELECT id, email FROM users`)
Without either, pgls leaves the string alone — no completion, no diagnostics — so non-SQL strings never get false hits.
- It carries a JetBrains-style
-
Hot reload — the schema directory is watched; editing or adding
.sqlfiles triggers a reload (debounced 200 ms) and republishes diagnostics for all open documents.
- CTE / subquery columns are not validated — names introduced by
WITHor(SELECT ...) aliasare recognized as visible tables (soFROM cteandcte.foodon't false-flag), butcte.foois silently accepted because pgls does not analyze the CTE body to extract its output column list. Inner unknown-table typos (WITH a AS (SELECT * FROM nope)) are still flagged. - Scope leakage between subqueries — aliases defined inside a subquery are visible from the outer query. A v1 trade-off: yields occasional false negatives, never false positives.
ALTER TABLEis not parsed — onlyCREATE TABLEcontributes to the schema.- Unqualified column references are not validated — too many
false positives from SELECT-list aliases, CTE columns, and
function arguments. Qualified references (
u.email) are. - LSP 3.17
PositionEncodingKindnegotiation is not implemented — pgls assumes UTF-16, the LSP default.
go build ./...
go test ./...
go test -bench . ./internal/sqlctx/MIT