Findings

Each is marked as confirmed by running the code against the test project or a small script, or as read from the code without running. Line numbers refer to 0.3.8 (commit 37c78b6), where the review was done. Thirteen of the fifteen findings are fixed, fully or in part: twelve in 0.4.0 and F13 in 0.4.1.

F1 · Fixedscope.py:147

A {{ config() }} header disabled column and CTE completion

A file containing a config call never parsed, in any dialect, so it had no tree to complete from. Config and other standalone macro calls, block set and call bodies, and else branches are now blanked out before parsing, with all positions kept.

Merged, released in 0.4.0, with tests. Still a parse error: a for loop ending each item with a comma, and jinja glued to an identifier.

F2 · Fixedmodel.py:199, 229, 246, 260

Live columns failed for DuckDB, MySQL, SQL Server and Spark

Commit 9f5490b gave _get_database_schema a third required argument and updated only the Postgres caller. The other four raised a swallowed TypeError.

Fixed in b3989db. A DuckDB load test now guards it; the other three were not run.

F3 · Partly fixedstate.py:79, 136

The run-results watcher crashed on results without compiled code

Missing fields, a half-written file and unparseable SQL are now skipped, and the project's dialect is used. Merged, released in 0.4.0, with tests.

Still open: the refresh replaces typed warehouse columns with a weaker list.

F4 · Fixedserver.py:203, 233

One failed step left the server with nothing at all

A missing backend package, no profiles.yml, or an unset env_var used to remove model and source completion too. The load now raises only when there is no project; everything else is recorded in a load report and shown as a warning.

Merged, released in 0.4.0, with tests and an end-to-end run of the server.

F5 · Fixedalias.py:70, 156

Aliases with capitals never matched

A. for ... }} A offered nothing, because declarations were found in lowercased text. Merged, released in 0.4.0, with tests.

F6 · Partly fixedalias.py:16

Only the simplest ref spelling bound an alias

Inner spaces and {{- whitespace control now work for ref and source. Merged, released in 0.4.0, with tests.

Still open: the two-argument ref('package', 'x'), a ref used without an alias, and subquery aliases.

F7 · Fixedserver.py:435, 503

Source completion assumed double quotes

It now inserts the quote that was typed, and the second argument offers the tables of the named source. Merged, released in 0.4.0, with tests.

F8 · Fixedresolve.py:123

select * from a CTE completed as a literal *

CTEs are now resolved in order, so a star and its column types follow through earlier CTEs. A star that cannot be expanded is not offered. Merged, released in 0.4.0, with tests.

F9 · Fixedserver.py:391

Completion on the empty last line raised

A cursor after the final newline gave IndexError. Merged, released in 0.4.0, with tests.

F10 · Partly fixedmodel.py:365, 414

Athena and Glue read only the first page

Both now page through every database and table, with two-page tests. Merged, released in 0.4.0. Checked against stubbed clients, not a real account.

Still open: the region and access keys in the profile are ignored; only the named AWS profile is used.

F11 · Fixedstate.py:73

Documented sources vanished when the warehouse lacked them

Every documented source is now kept; the warehouse only adds columns to the ones it has. Merged, released in 0.4.0, with tests.

F12 · Fixedmodel.py:556

Same table name in two schemas: last one scanned won

Tables are now identified by schema, and by database where the reader reports one. A model matches the target schema first; a source matches the schema it declares. Ambiguous names match nothing. Merged, released in 0.4.0, with tests.

Models with a literal alias, custom schema or database are matched by it. Merged, released in 0.4.0. Still open: sources in a database the reader does not scan, and locations set in dbt_project.yml or by macros.

F13 · Fixedserver.py:273

Loading blocked the server until the warehouse was read

Confirmed on a generated project with 2,000 models and 6,000 warehouse tables: 35.6 seconds to the first completion. Now 2.4 seconds, with live columns in after 5.4. The warehouse is read in one statement per database, on a background thread.

Merged, released in 0.4.1, with tests. The scan still covers every schema on purpose, so undocumented tables can complete. Still open: Spark reads table by table, and YAML files are parsed twice.

F14 · Partly fixedprofiles.py:316

Profile lookup is narrower than dbt's

Fixed on main, not released: --profiles-dir and DBT_PROFILES_DIR are honoured in dbt's order. Still open: there is no way to choose a target other than the profile's default, and the VS Code client watches project files but the server reloads on nothing except run results.

F15 · Fixedmodel.py, enrich_models_from_config

One YAML model without columns disabled all documented columns

A model entry with only a description raised KeyError, which failed the whole config source for the project. Confirmed by running it. Merged, released in 0.4.0, with tests.

Smaller items, such as the extras in pyproject.toml, are in the task list. The MySQL target requires a user key; I believe dbt-mysql calls it username, which is worth checking against a real profile.

Making testing less tedious

Before 0.4.0 the tests covered text handling only. Finding F2 would have been caught by a single test that opens the DuckDB file already in the repo; that test now exists. The idea is to test each layer at the cheapest level that can catch its bugs, and to stop needing an editor to check anything.

  1. Tier 0: fakes for the mapping logic (done)

    Most reader code is turning an API response into columns. The Databricks, Athena, Glue and Redshift readers now run against stand-in clients in test/test_cloud_readers.py. No account needed, runs in CI. Next step: replace the hand-written responses with ones recorded from a real workspace.

  2. Tier 1: DuckDB for real (done)

    Runs the full load against testdata/project and asserts on the columns. In-process, no setup, covers the shared ibis path and the merge rules.

  3. Tier 2: containers (done)

    Postgres, MySQL, SQL Server and Spark Connect from test/compose.yml, on ports of their own. test/test_containers.py seeds each one and runs the real reader, a full project load and dbt-ls check. just test-all starts them, runs everything and stops them again; about 15 seconds to start, 4 seconds to run. On its first run it found two bugs: the Spark extra could not connect at all, and SQL Server types lost their precision. A CI job runs the same file; it has not run on GitHub yet.

  4. Speed: a generated big project (done)

    scripts/generate_bench_project.py writes a dbt project and an empty DuckDB warehouse of any size. The numbers under F13 come from it.

  5. Tier 3: a smoke command for cloud platforms (command done)

    dbt-ls check loads a project and prints what each source returned, how long it took, and any error. A cloud check is now one command and a pasted result. Not yet done: running it against a real Databricks or AWS account and recording the result with date and version.

workflow
# one command per tier
uv run pytest                      # tier 0+1: unit, fakes, DuckDB  (seconds, CI)
docker compose -f test/compose.yml up -d --wait && \
  uv run pytest -m containers      # tier 2: Postgres, MySQL, SQL Server, Spark
uv run dbt-ls check path/to/proj   # tier 3: cloud smoke test, by hand

Fabric, which you cannot run

Fabric and Synapse speak the same protocol and dialect as SQL Server. If the Fabric reader is the SQL Server reader plus a different sign-in, the query logic is covered by the SQL Server container and only the sign-in stays unverified. Label that honestly in the README as untested, and ask a Fabric user to run dbt-ls check. Until then Fabric users already get everything except live columns by using config,catalog.

Questions you may have coming back

Where do I start reading?

serve_completions in server.py. It is one function with a branch per kind of completion, and every other module is reached from it or from _load_project just above it.

How do I see what the server is doing?

Set DBT_LS_LOG_LEVEL=DEBUG. Everything goes to stderr, which the editor shows in its LSP log. Each completion logs which branch matched and what it served.

How do I test a completion without an editor?

Use the complete helper in test/conftest.py. Write the SQL with a | where the cursor is and pass models and sources built with the model and source helpers.

Why are there both regexes and a parser?

The parser gives query block boundaries and CTE select lists, but fails on text being typed. The regexes find alias declarations in any text. Scope comes from the last good tree, declarations from the live buffer.

Why does a CTE have columns but a model sometimes not?

CTE columns come from the file's own select list. Model columns come only from YAML, the catalog, the warehouse or a dbt run. A model that is undocumented, never built and not in the catalog has none.

What is uncommitted right now?

Nothing that changes behaviour. Main is pushed and CI is green, container tests included. Not released since 0.4.1: --profiles-dir / DBT_PROFILES_DIR, the devcontainer, hover, and keeping undocumented tables as WarehouseTable with one state.tables list. Still outside git: testdata/project/.user.yml and todo.md. The untracked seed.py draft is no longer in the working tree; it was never committed.

How do I rebuild these pages?

Run uv run docs/build.py. It writes docs/index.html from the fragments in docs/src, from README.md (the Use it tab) and from todo.md (the Tasks tab). Edit those, never index.html.