Persistent multi-dialect SQL Language Server. PostgreSQL is the deepest target; MySQL / MariaDB, SQLite, and SQL Server are first-class for syntax, completion, hover, formatting, and connection-backed introspection. Built on tower-lsp + libpg_query.
MIT - Changelog - Contributing - Crate docs
-
701 lint rules (PG-first, dialect-aware) covering schema correctness, transaction safety, query smells, migration footguns, vendor mismatches (MySQL
ENGINE=, OracleDUAL/CONNECT BY, SQL ServerBEGIN TRANSACTION, ...). Delivered by push (publishDiagnostics) or by LSP 3.17 pull (textDocument/diagnostic), whichever the client advertises -- never both, so nothing renders twice. Pull requests on an unchanged buffer answerUnchangedwithout re-running the engine. -
Context-aware completion across ~50 phases:
CREATE INDEX ... USING+ opclass slot,CREATE TRIGGER ... EXECUTE FUNCTION,CREATE POLICY ... FOR / TO,ALTER COLUMN TYPE,CALL <proc>, PL/pgSQL local-variable scope, JOIN target resolution, etc. -
Document links: psql
\i/\ir/\include/\include_relativetargets,COPY ... FROM/TO '<file>'data files, and URLs in comments are all clickable. File links are emitted only when the path actually resolves on disk — aCOPYpath interpreted by the server usually doesn't exist locally, and a link that reliably errors is worse than none. - Incremental document sync: the editor ships only the edited range, spliced into the rope in place. On a 240 KB migration file that's 200 bytes over 200 keystrokes instead of 48 MB, and ~4x less server-side work per edit (0.016 ms vs 0.070 ms).
-
Deferred completion docs via
completionItem/resolve: the per-keystroke response ships labels and no markdown, and documentation is rendered only for the item you actually highlight. ASELECTprojection completion drops from ~440 KB to ~209 KB on the wire, and stops rendering markdown for 2321 built-in entries (~617 KB, ~1 ms) that get thrown away. -
Rich hover cards for tables (compact
CREATE TABLE+ indexes + triggers + policies + comment +ALTER TABLE OWNER TO), columns, functions, keywords, types, NULL three-valued logic notes. -
Offline mode built in: walks the workspace for
*.sqlfiles (migrations/,db/,sql/,schema/) and derives a synthetic catalog so completion + hover + diagnostics work without a live DB. Live introspect (PG / MySQL / SQLite) overrides. -
Code lenses:
Runabove every statement +EXPLAINfor DML;+ LIMIT 100,EXPLAIN ANALYZEfor slow SELECTs. VS Code wires them through to the active connection's CLI in a terminal; other clients see no broken-command popups. -
Inlay hints: column-name chip per INSERT VALUES tuple (DataGrip-style),
SELECT *expansion,JOIN ... ON ...predicate suggestion,-- ~N rowsat end of SELECT, literal-cast::inthint in WHERE. -
Formatter: external
sql-formatterv15+ reflow + DataGrip-styleCREATE TABLEalignment + PL/pgSQL block indenter. OptionalsingleLinepost-pass collapses DML statements onto one line while leaving DDL intact. Format-selection (textDocument/rangeFormatting) snaps outward to whole statements, so formatting a caret inside oneCREATE TABLEleaves its neighbours byte-identical. -
Semantic tokens with modifiers:
CREATEtargets carrydeclaration/definition, column and parameter lists inside aCREATEbody are highlighted as declared properties/parameters (a plain catalog lookup can't colour a table the file is creating), and built-in types/functions getdefaultLibraryso they read differently from your own.semanticTokens/rangecolours just the viewport. -
Refactors / code actions:
= NULL->IS NULL,EXISTS (...)->CROSS JOIN LATERAL,BEGIN TRANSACTION->BEGIN, extract subquery toWITH _tmp AS (...) CTE, 30+ more. -
Editor integrations: VS Code extension with connections + schema tree view, neovim setup that works with stock
vim.lsp+nvim-cmp.
cargo install duck-sqllspLibrary use:
cargo add dsl-analysis-- neovim
vim.lsp.config('duck_sqllsp', {
cmd = { 'duck-sqllsp', 'server' },
filetypes = { 'sql', 'mysql', 'plsql' },
root_markers = { '.duck-sqllsp.toml', '.duck-sqllsp.json', '.git' },
})
vim.lsp.enable('duck_sqllsp')duck-sqllsp --help
duck-sqllsp version
duck-sqllsp doctor # check formatter, config, workspace, connection
duck-sqllsp rules
duck-sqllsp lint file.sql
duck-sqllsp format file.sql --stdout
duck-sqllsp introspect file.sql # offline catalog from CREATE TABLE/FUNCTION/TYPE
duck-sqllsp introspect --url postgres://user:pass@host/dbProject config example (.duck-sqllsp.toml):
[duck_sqllsp]
activeConnection = "local"
dialect = "postgres" # postgres / mysql / sqlite / mssql (aliases accepted)
requireConnection = false
[duck_sqllsp.rules]
sql015 = "off" # silence a rule by code
[duck_sqllsp.style]
keywordCase = "upper"
functionCase = "lower"
typeCase = "upper"
identifierCase = "preserve"
[duck_sqllsp.style.formatter]
expressionWidth = 100
singleLine = true # collapse DML to one line; leaves DDL untouched
[[duck_sqllsp.connections]]
name = "local"
url = "postgres://user:pass@localhost:5432/mydb"Every key accepts camelCase or snake_case, and the [duck_sqllsp]
wrapper is optional.
- Configuration reference → — every setting, its default, and what it changes.
-
Troubleshooting → — start with
duck-sqllsp doctor. - Lint rule reference → — all 701 diagnostics by code, with what each one catches.
- Editor setup → — VS Code, neovim (0.11+ and lspconfig), Helix, Emacs, Sublime.
| Crate | Role |
|---|---|
dsl-parse |
SQL parser - libpg_query primary, sqlparser fallback for MySQL/SQLite/MSSQL |
dsl-catalog |
Schema model - tables (incl. owner), columns, constraints (inline + table-level), indexes, triggers, policies, sequences, types, functions |
dsl-knowledge |
Static keyword / type / function reference with PG-doc links |
dsl-resolve |
Name resolution, FROM / JOIN / LATERAL scope, CTE columns, alias chains |
dsl-format |
Formatter - sql-formatter reflow + DataGrip alignment + PL/pgSQL indent + optional one-line DML pass |
dsl-analysis |
Lint rule engine - 701 diagnostics with narrow ranges |
dsl-completion |
Context-aware completion engine, ~50 phases, alias + scope aware |
dsl-hover |
Hover cards with cursor-side narrowing, schema-qualified resolution |
dsl-conn |
Live PG / MySQL / SQLite catalog introspection (sqlx) |
dsl-server |
tower-lsp server - 25 LSP request handlers + startup progress |
dsl-cli |
duck-sqllsp binary - subcommands + stdio LSP + signal handling |
Any LSP client that can launch a binary works — duck-sqllsp server speaks stdio and needs nothing else. Setup for VS Code, neovim (0.11+ and lspconfig), Helix, Emacs, and Sublime →
-
VS Code: install
wildduck.duck-sqllsp-vscode. Sidebar tree views for connections + schema. Commands: Add Connection, Set Active, Test Connection, Refresh Schema, Restart Server, Show Logs. Run / EXPLAIN / EXPLAIN ANALYZE / + LIMIT 100 code lenses wire through to aduck-sqllspterminal runningpsql/mysql/sqlite3against the active connection. -
neovim: stock
vim.lsp+nvim-cmp. duck-sqllsp emits$/progressso statusline plugins surface "loading workspace..." while the .sql scan + DB introspect settle.
cargo build --release
cargo test --workspace --release
cargo clippy --workspace --all-features --release -- -D warnings2500+ tests (rules, idioms, completion phases, hover resolver, formatter, parsers).
| Metric | Target |
|---|---|
| Completion p50 | < 5 ms |
| Diagnostics p50 | < 20 ms |
| Hover p50 | < 3 ms |
| Format p50 | < 30 ms |
| Memory idle | < 30 MB |
| Memory @ 4 MiB file | < 150 MB |
| Cold start | < 50 ms |
| Document update | incremental, no re-parse on cached handlers |
- libpg_query primary parser, sqlparser fallback so MySQL backticks, MSSQL bracketed idents, SQLite quirks all parse.
- tower-lsp protocol layer. Every handler is a thin shim over a pure-function crate; the LSP transport is the only place tokio touches.
-
Per-document parse cache on
OnceLock- first heavy handler afterdidChangepays the parse cost, the rest reuse it. - Space-preserving strip keeps 1:1 byte offsets when stripping strings / comments / dollar-quoted bodies, so diagnostic ranges map back to source byte-exact.
-
Catalog snapshots are
parking_lot::RwLockreads cloned before any.await- no guard ever crosses an await point. -
PR_SET_PDEATHSIG+ SIGTERM / SIGINT / SIGHUP handling - the binary always dies with its editor.
@gentleduck/ui -
@gentleduck/iam -
@gentleduck/upload -
@gentleduck/md
PR checklist + style notes in CONTRIBUTING.md.
Security: SECURITY.md. Behaviour: CODE_OF_CONDUCT.md.
MIT. See LICENSE.
