Skip to main content

dbt-cst

A lossless dbt model parser written in Rust with Python bindings. It parses a model (Jinja templated SQL) into a concrete syntax tree (CST) that keeps every comment, blank line, indentation and Jinja tag, so a file can be read, modified and written back out with its formatting intact:

from dbt_cst import parse

doc = parse(source)
assert str(doc) == source  # byte for byte, for every model in albert-dbt-analytics-transforms

The tree groups the SQL into queries, clauses, select columns and CTEs, and the Jinja into blocks, without interpreting either: a column is the text between two commas, a WHERE clause is the text between its keyword and the next clause keyword. That is enough to add and remove columns in a model someone has customized by hand, which rewriting the file from a template cannot do. The Python package is an abi3 wheel built against Python 3.9, so one wheel works on Python 3.9 and 3.11.

Python usage

from dbt_cst import parse, DbtSyntaxError

doc = parse(path.read_text())

    doc.config["materialized"]                  # "view"
    doc.config["materialized"] = "table"        # quoted like the call's other strings
    doc.config["tags"] = ["pii"]                # added in the call's layout: one per line, trailing comma
    doc.config.set_raw("schema", 'var("schema")')
    del doc.config["alias"]

    # Example: Modifying multiple config values
    doc.config["materialized"] = "table"
    doc.config["schema"] = "analytics"
    doc.config["tags"] = ["pii", "finance"]
    doc.config["enabled"] = False

    # Example: Using raw expressions for dynamic values
    doc.config.set_raw("alias", 'var("model_alias")')
    doc.config.set_raw("schema", 'var("schema")')

    # Example: Removing config values
    if "description" in doc.config:
        del doc.config["description"]

    doc.sources                                 # [("trading", "apex_apexentry")]
    doc.refs                                    # [("users",), ("package", "model")]

query = doc.query                           # the model's own SELECT, after any CTEs
select = query.select
select.names                                # ["account_id", "activity_id", "percent"]
"account_id" in select                      # True, compared ignoring case

select.add("user_id")                       # appended in the select's comma style
select.add("amount / 100 AS dollars", index=select.index("account_id") + 1)
select.remove("activity_id")                # with its comma and the comments above it
select.find("percent").text = '"percent" AS pct'

renamed = query.cte("renamed").query.select # CTE bodies are queries too
for query in doc.queries():                 # every query, subqueries included
    ...

path.write_text(str(doc))

API

parse(source) -> Document raises DbtSyntaxError (a ValueError) with line, column and offset attributes. Only a Jinja tag that is never closed is an error. SQL that does not fit the expected shape still parses: it ends up as unstructured tokens and still round trips.

Object Members
Document config, query, queries(), sources, refs, str()
Config c[key], get(key, default), c[key] = value, del c[key], raw(key), set_raw(key, src), keys(), in, len, iter
Query select (the first), selects (one per UNION branch), ctes, cte(name), clauses
Cte name, query
Clause name (FROM, GROUP BY, UNION ALL, ...), str()
Select columns, names, find(name), index(name), add(text, index=None), remove(name_or_column), in, len, iter
Column text (settable), expr, alias, name, is_templated, select, detach()

Config values are Jinja literals: strings, numbers, booleans, None, lists and dicts. Reading an argument whose value is an expression such as var("x") raises ValueError; use raw() for its source text.

Column.name is the alias, or the last part of a plain column reference (t.id gives id), unquoted. It is None for an unaliased expression or a bare Jinja tag such as {{ dbt_utils.star(...) }}.

Handles are live: a Column from select.columns stays attached to the document, and two handles to the same node compare equal.

How it parses

  1. Lexing (src/lexer.rs) splits the file into tokens covering every byte. Jinja tags end where Jinja says they do, skipping string literals and nested brackets, so {{ {'a': 1} }} and {{ "}}" }} are one tag each. A tag inside a SQL string or comment is absorbed into that token. {% raw %} sections are one token.
  2. Grouping (src/parser.rs) pairs (/), [/] and Jinja block tags with one stack. Any {% name %} whose {% endname %} appears in the file is opened tentatively, so tags from Jinja, dbt (materialization, test, snapshot) and packages all work without a list of names. A tag that never closes, like a one line {% set %}, or one whose end would cross a parenthesis, is put back as a plain token. elif and else split blocks into branches.
  3. Structure: a top level SELECT or WITH starts a query, which is split into clauses at top level clause keywords. SELECT items are split at top level commas, and WITH clauses into CTEs. "Top level" means outside any bracket or Jinja block, so commas in function calls and keywords in subqueries never split anything. A Jinja block that supplies its own commas, like {% for c in cols %}{{ c }},{% endfor %}, separates the columns around it.

The lexer is written with nom, like looker-cst's parser. The one hand written scan is the end of a {{ }} or {% %} tag, which tracks bracket depth and skips string literals the way Jinja's own lexer does.

Rust usage

let doc = dbt_cst::parse(&source)?;
let query = doc.query().unwrap();
let select = dbt_cst::query::clauses_named(&query.read(), "SELECT").remove(0);
dbt_cst::edit::insert_item(&mut select.write(), usize::MAX, "user_id")?;
assert!(doc.to_string().contains("user_id"));

cargo run --example dump -- path/to/model.sql prints the tree.

Development

cargo test                           # Rust tests, cloning the corpus into target/corpus on first run
uv run pytest                        # builds the extension with maturin, then runs the Python tests
DBT_EXTRA_REPOS=../albert-dbt-analytics-transforms cargo test --release --test corpus

The corpus is the 14 public dbt projects listed in tests/corpus_repos.txt (dbt-utils, dbt-expectations, automate-dv, the Fivetran packages and others, about 1,700 .sql files). Both test suites shallow clone them into target/corpus on first use, or into DBT_CORPUS when set. A failed clone skips those files unless REQUIRE_DBT_CORPUS is set, as in CI. DBT_EXTRA_REPOS adds more checkouts, separated by :.

  • tests/corpus.rs round trips every file, adds and removes a column in every SELECT, removes each column in turn, and adds and removes a config argument.
  • tests/python/test_corpus.py does the same read, modify and write cycle through the Python API, one test per file.
  • tests/fixtures/ holds awkward inputs (CRLF, unicode, unterminated quotes, unbalanced brackets, unpaired Jinja blocks) that both suites round trip, and edge_cases.sql, a customized model whose structure both suites check.
  • tests/python/test_albert_models.py checks every model in a sibling albert-dbt-analytics-transforms checkout (or ALBERT_DBT_REPO) against the field list DBTGenerator reads today.

Metadata

Release files for dbt-cst 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dbt-cst 0.1.0
File Size Uploaded
dbt_cst-0.1.0.tar.gz 59.0 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for dbt-cst 0.1.0
File
dbt_cst-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.9 abi3 Linux glibc 2.17+ x86-64 Details
dbt_cst-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.9 abi3 Linux glibc 2.17+ ARM64 Details
dbt_cst-0.1.0-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details
dbt_cst-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details

Total release size: 1.7 MB

Release files / dbt_cst-0.1.0.tar.gz

Download URL dbt_cst-0.1.0.tar.gz
Size 59.0 kB
Tags Source
SHA-256 checksum
How to use checksums
56ab8f3a08593f0e80bc1a7d80b1b17764c6b8c375f7c42b5c08459440f287d2
BLAKE2b-256 checksum
How to use checksums
5f9e53e069672f0b29f3e55d6241756c2f1f4a91be2ee2b32f6ee2c15df5a63f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / dbt_cst-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL dbt_cst-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 409.8 kB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
30299be7170d04fa1c2baae0d9c320aa83376e830f6b78cbc590833a15dde273
BLAKE2b-256 checksum
How to use checksums
4b648d989d3c3dc1ed849c26416c7f466d8895a175ffc4f1f76f1cefcbd129ed
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / dbt_cst-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL dbt_cst-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 394.4 kB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
9a44c376a3733377fee9a8defbbd581abf3e7f199be9d27556eb4fa826612b83
BLAKE2b-256 checksum
How to use checksums
ecf20594011f080b7b8f9ca757e09268a022bc724e341c0df96e1b3f493b26ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / dbt_cst-0.1.0-cp39-abi3-macosx_11_0_arm64.whl

Download URL dbt_cst-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
Size 418.5 kB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
73b7f7494285310f4e1376e4fd20f6f35108c37b70cccf393e23c5a4407cbb47
BLAKE2b-256 checksum
How to use checksums
cf75758c6d5863b3ec862b9e1cf35ee4c585c2ee1ff4e1f1f2be70ccfcdfe6f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / dbt_cst-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl

Download URL dbt_cst-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl
Size 423.3 kB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
62770b4994a0940c08f8fd91225b8b1646e3f778a1a31b377479f4e0b3750407
BLAKE2b-256 checksum
How to use checksums
df8d43a274c996adf1f967f937df21934f24495f77e1ee75f74ab1bf0db209a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.2.0

5 release files

This release

0.1.0 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page