Trilogy Parser (trilogy-parser)
A Rust-based CLI tool and Python library for parsing PreQL (Trilogy) files and resolving import dependencies with ETL-aware dependency ordering.
Features
- Parse PreQL files to extract imports, datasource declarations, and persist statements
- Resolve import dependencies transitively
- Build dependency graphs with ETL-aware ordering:
- Files that persist (write) to a datasource run before files that declare it, even if they import it.
- Standard import dependencies (imported files run before importing files)
Exit codes:
0: Success1: Error (parse error, file not found, circular dependency, etc.)
Installation
Crate versions track pytrilogy releases: a
published trilogy-parser version always carries the grammar of the pytrilogy
release with the same number.
Feature flags
| Feature | Default | Effect |
|---|---|---|
| (none) | ✅ | Pure Rust. No Python toolchain required. |
python |
Builds the PyO3 bindings (python_bindings, parse_trilogy_syntax*). Used only by the wheel build. |
Rust consumers should stay on default features; enabling python pulls in pyo3
and requires a Python interpreter at build time.
Rust Library Usage
use Parser;
use Path;
use ;
DatasourceDeclaration / DatasourceInfo carry the backing of each datasource:
address plus an address_kind of literal, templated (an f-string address
resolved at run time), query (a view, no physical table), or file. Flags
is_root, is_partial, and is_partitioned mirror the corresponding modifiers.
CLI Usage
Parse a single file
Parse a directory
Resolve dependencies
Analyze datasources
Python Integration
The Rust resolver is integrated into the Python package via PyO3 and maturin.
Building the Python Extension
# or
Using in Python
# Create script nodes from files
=
=
# Use the ETL dependency strategy (backed by Rust)
=
=
# Get execution order (graphs use the Rust-backed graph facade;
# nodes are script-path strings)
=
The ETLDependencyStrategy uses the Rust-based resolver under the hood for fast, accurate dependency analysis based on:
- Import statements
- Datasource declarations
- Persist statements (append/overwrite/persist)
Development
Running Rust Tests
Test layout:
- Unit tests for the parser — imports, datasource backings/modifiers, persist statements
- Unit tests for the resolver — dependency resolution and ordering
tests/cli_integration.rs— every CLI command, including the JSON output contracttests/public_api.rs— the crate's exported surface, reached only throughtrilogy_parser::*, so a regression in what is actually published fails here
Building the CLI
The binary will be at target/release/trilogy-parser-cli (or .exe on Windows).
Building for Python
Run maturin from the base of the pytrilogy repo. [not this directory.]
# Development mode (installs in current Python environment)
# Production build
# The wheel will be in target/wheels/
Dependency Ordering Rules
The resolver implements three key dependency rules:
- Import Dependencies : Imported files should run before importing files
- Persist-Before-Declare: Files that persist to a datasource must run before files that declare it, even if they import that file.
Edge Cases
- Case 1: file A imports from file B → B must run before A for all datasources in B
- Case 2: file A imports from file B, then updates datasource from file B → update takes precedence, so A runs before B.
Scope
src/trilogy.pest is the full Trilogy grammar — the same one that backs
pytrilogy's pest parser — so TrilogyParser::parse(Rule::start, ..) accepts any
valid Trilogy source.
The higher-level helpers (parse_file, parse_imports, ImportResolver) walk
only the dependency-relevant constructs of that tree: imports, datasource
declarations, and persist statements. Everything else in a file is parsed and
skipped. For anything beyond dependency analysis, drive the grammar directly.
Tree shape
pest has no memoization, so the grammar never spells two alternatives that read
the same text: a construct that lark names by alternation is one left-factored
rule here, and the Python adapter (trilogy_parser.rs) restores lark's names
from the shape. A consumer of the raw tree sees the factored rules:
| pest rule | lark rules it stands for |
|---|---|
expr |
comparison (one child, or head + operator + rhs) and between_comparison (three children) |
access_chain |
the atom alone, attr_access / index_access / map_key_access / fcast / chained_access, and the X ? cond form of filter_item |
paren_group |
parenthetical (one expr, no comma) and expr_tuple |
aggregate_functions with a window_sql_over child |
window_item(window_item_sql_aggregate(...)) |
select_chain |
select_statement, or multi_select_statement when a merge tail follows |
rule_calls(text, max_calls) reports how many rule calls a parse makes; it is
the deterministic measure tests/parse_cost.rs gates nesting cost on, so a new
alternation that re-reads an expression fails cargo test rather than shipping
as a parse that is exponential in nesting depth.