

Trilogy: Composable, reusable analytics — a SQL-based language that’s type-safe, testable and fun to use. Model once. Run anywhere.
Familiar
Looks like SQL. Feels like code.
Accurate
No gotchas with grouping or joins.
Testable
Type-safe analytics with modular, testable logic.
Efficient
Write less. Stage less. Run what matters.
Extendable
Open source. Rich python API.
Composable
Models as building blocks: share, reuse, and compose with ease.
Perfect for AI
Exactly the context LLMs need, but human readable/writable.
For those who turn data into insights
SQL remains unmatched for exploratory data analysis and reporting. Trilogy makes it scalable, reusable, and safe. Trilogy modernizes SQL with type-safety and composable logic - we don't need to replace SQL, we can make it make it better.
Easy to start, easy to scale. Iterate fast, then share reusable models across your team. Keep a single source of truth - update a metric, test it, and have it flow to 50 reports - automatically and safely.
Built for anyone who turns data into decisions.
Install
pip install pytrilogy
Features
Trilogy is SQL reinvented: a semantic layer with first-class support for logic, functions, and reuse.
- Write scripts once; update the model to keep them fresh.
- Ensure a single source of truth with common model files.
- Leverage rich compile-time checks and model validation to ensure correctness.
Here's a hello world:
Hello, World
# import from standard library / local files
# for types and fields resuse
import std.display;
# model logic is written first
# with a pure semantic model
const greeting <- 'Hello'; # comments describe fields
key noun string; # people, places, things
# that you bind to data
datasource things (
thing:noun
)
grain (noun)
query '''
select unnest(['world', 'friend', 'visitor']) as thing
''';
# with reusable functions
def initcap(word)-> upper(substring(word, 1, 1)) || substring(word,2, len(word));
# and run queries in familiar SQL syntax
WHERE noun like '%world%' or noun = 'friend'
SELECT
greeting || ' ' || @initcap(noun) || '!' as salutation,
rank salutation by len(salutation) asc as brevity_rank,
(len(salutation) / max(len(salutation)) by *)::float::percent as percent_of_longest
;
