dbt-ls · project map

What dbt-ls does, and where it stands

A language server that gives dbt SQL files completion for models, sources, CTEs and typed columns. These pages explain how to use it, how the code works and what works on which platform. The Findings and Tasks tabs cover what is still broken and what to do next.

Release 0.4.1 Reviewed at 0.3.8; updated 19 October 2026 Source about 3,900 lines, 17 modules Tests 335 pass, plus 17 against real containers Live backends 9 Date 11 October 2026

What it does

The server reads a dbt project from disk, optionally asks the warehouse what columns exist, and answers completion, hover and go-to-definition requests from the editor. It never runs dbt and never renders jinja.

models/int_cards.sql
with cards as (
    select c.id, c.mana_cost as cost
    from {{ ref('stg_cards') }} c          -- ref(' completes model names
),
sets as (
    select s.code, s.released_at
    from {{ source('raw', 'sets') }} s     -- source(' completes sources
)
select
    cards.cost,                            -- alias. completes typed columns
    sets.released_at
from cards                                 -- from/join completes CTE names
join sets on sets.code = cards.id
Works

Model names in ref('

Lists every .sql file under the project's model paths, with its path as detail.

Works

Sources in source('

Lists source tables from the YAML files and inserts both arguments. In the second argument, lists the tables of that source. Warehouse tables with no YAML follow, marked not in sources.yml.

Works

CTE names after from / join

Offers the CTEs declared in the current file, then warehouse tables as schema.table. After from schema. it lists that schema only.

Partly

Columns after alias.

Resolves the alias to a model, source, CTE or plain schema.table in the current query block and lists its columns with data types. Still fails in a few cases, listed under Findings.

New in 0.4.1

Tables nobody documented

Every warehouse table that matches no model and no documented source is kept and offered in completion, with its columns. Nothing has to be written in YAML first.

New in 0.4.1

Loads in the background

Names complete as soon as the files are read. The warehouse is read in one statement per database on a separate thread, and live columns follow.

On main

Hover

Over ref('…') or source(…): the model's file or the relation it reads, then its columns with types. Over alias.column: the column's type and where it comes from, or that the relation has no such column.

Works

Go to definition

Jumps from a ref('model') to the model's file. Not available for sources or columns.

Partly

Refresh after dbt run

Watches target/run_results.json and re-derives column lists from the compiled SQL. Can replace typed columns with untyped ones.

Works

Check without an editor

dbt-ls check path/to/project loads the project the way the server does and prints, per schema source, how many models got columns and types, how long it took, any error, and how many undocumented tables it found.

Works

Two editor commands

dbt-ls.reload rebuilds everything. dbt-ls.current_model returns the project root and the model's selector path so an editor can run it.

Works

Two editors

Neovim through vim.lsp.config. A VS Code extension that starts one server per dbt_project.yml in the workspace.

Not implemented: diagnostics, seeds, snapshots, macros, rename, formatting.

What works on which platform

Most of the server does not depend on the warehouse. Only one thing does: reading live column names and types from it. That split is the key to making testing less tedious.

Every dbt adapter

No connection needed

Model, source and CTE completion, go to definition, columns from YAML files, columns and types from target/catalog.json, and both commands. This includes Snowflake, BigQuery and Fabric.

No profile is needed for these.

Nine adapters

Live columns from the warehouse

One function per adapter in model.py. Each lists the tables of every non-system schema; a shared step then matches them to models and sources by schema, and keeps the rest as undocumented tables.

DuckDB has real load tests. Postgres, MySQL, SQL Server and Spark Connect are tested against containers. The four cloud readers are tested against stand-in clients only.

PlatformProfile typeParsed asLive columns throughSign-in it acceptsState on main
DuckDBduckdbduckdbOne statement, local fileNone neededWorks covered by load tests
PostgreSQLpostgrespostgresOne statement on pg_catalogUser and passwordWorks container tests
MySQLmysqlmysqlOne statement on information_schemaUser and password, key must be serverWorks container tests
SQL ServersqlservertsqlOne statement over ODBCSQL login only, key must be serverWorks container tests
SparksparksparkSpark Connect session, one request per tableNoneWorks container tests; needs pyspark[connect]
DatabricksdatabricksdatabricksDatabricks SDK, Unity Catalog listsAccess token, or service principal OAuthNot run stub-tested
Athenaathenatrinoboto3 Athena metadataNamed AWS profile or defaultNot run stub-tested, paging fixed
Gluegluetrinoboto3 Glue catalogNamed AWS profile or defaultNot run stub-tested, paging fixed
Redshiftredshiftredshiftboto3 Redshift Data API, svv_columnsIAM profile only, cluster or serverlessNot run stub-tested; password profiles rejected
SnowflakesnowflakesnowflakeNothingNot applicableNo live columns
BigQuerybigquerybigqueryNothingNot applicableNo live columns
Fabric, Synapsefabric, synapsetsqlNothingNot applicableNo live columns
TrinotrinotrinoNothingNot applicableNo live columns
Anything elseanypostgresNothingNot applicableNo live columns

This describes main after 0.4.1. "Works" means the reader ran against a real instance in a test: DuckDB in the default run, the other four in the container tests, which CI runs on every push. The cloud platforms have not been run against a real account in any test. Not released yet: two fixes found by the container tests (the Spark extra, and SQL Server types with their precision), --profiles-dir / DBT_PROFILES_DIR support, and hover. Which cloud platforms you verified by hand over the summer is not recorded anywhere in the repo, so I could not include it.