RTGL (Relational Task Generation Language) is a Python framework for writing compact, expressive predictive queries over relational data, with a focus on Relational Deep Learning, and inspired by the proprietary PQL from KumoAI.
Defining a prediction task over relational databases usually means hand-writing SQL or pandas pipelines for entity selection, temporal joins, and label aggregation: code that is verbose, easy to get subtly wrong, and rarely reusable across tasks. RTGL replaces that boilerplate: declare what to predict and for whom, and RTGL compiles the query to SQL, optionally executes it, and returns the result.
- 🔀 Two converters:
SConverterfor static prediction queries,TConverterfor temporal queries evaluated at a set of prediction timestamps. - 🔍 Automatic validation: built-in syntactic and semantic checks reject malformed queries and schema mismatches before any SQL is executed.
- 🔗 Multi-hop relationships: resolves the shortest foreign-key path between two tables automatically, not just direct relations.
- 🧵 Common Path Expressions (CPEs): name an explicit join path and reference it like a regular table, including cases where several equally short paths make automatic resolution ambiguous.
- 💉 SQL Injections: drop a raw SQL query in as a virtual table, declare its keys, and use it anywhere a table is expected.
- ⚙️ Dual output mode:
execute=Falsereturns the generated SQL;execute=Trueruns it on DuckDB and returns aTableobject.
pip install rtglRTGL operates over a Database of Table objects.
Wrap each pandas DataFrame with its primary key, its foreign keys, and an optional time column.
A database can be supplied either as a RelBench Database object or through RTGL's own simplified equivalent, shown below.
import pandas as pd
from rtgl.base import Database, Table
users = pd.DataFrame({
"user_id": [1, 2, 3],
"registration_date": pd.to_datetime(["2024-01-01", "2025-07-23", "2026-08-08"]),
# ... other columns
})
orders = pd.DataFrame({
"user_id": [1, 1, 1, 3],
"order_date": pd.to_datetime(["2026-07-05", "2026-07-20", "2026-08-10", "2026-08-15"]),
# ... other columns
})
# ... other dataframes
db = Database(table_dict={
"users": Table(
df=users,
pkey_col="user_id",
time_col="registration_date",
),
"orders": Table(
df=orders,
fkey_col_to_pkey_table={"user_id": "users"},
time_col="order_date",
),
# ... other tables
})A static query produces exactly one label per entity, independent of time.
from rtgl.converter import SConverter
converter = SConverter(db)
# how many orders has each user placed, over all time?
rtgl_query = """
PREDICT COUNT(orders.*)
FOR EACH users.*;
"""
# returns the generated SQL query
sql_query = converter.convert(rtgl_query, execute=False)
# returns a Table object with (fk, label) columns
table = converter.convert(rtgl_query, execute=True)
print(table.df)
# fk label
# 0 1 3
# 1 2 0
# 2 3 1
# db can be replaced later without rebuilding the converter
new_db = ...
converter.set_db(new_db)A temporal query is evaluated at a set of prediction timestamps, and every aggregation is computed over a time window relative to each one.
import pandas as pd
from rtgl.converter import TConverter
timestamps = pd.Series(pd.to_datetime(["2026-07-01", "2026-08-09"]))
converter = TConverter(db, timestamps)
# how much will each user place orders in the 30 days following each timestamp?
rtgl_query = """
PREDICT COUNT(orders.*, 0, 30, DAYS)
FOR EACH users.*;
"""
# returns the generated SQL query
sql_query = converter.convert(rtgl_query, execute=False)
# returns a Table object with (fk, timestamp, label) columns
table = converter.convert(rtgl_query, execute=True)
print(table.df)
# fk timestamp label
# 0 1 2026-07-01 2
# 1 2 2026-07-01 0
# 2 1 2026-08-09 1
# 3 2 2026-08-09 0
# 4 3 2026-08-09 1
# db and timestamps can be replaced later without rebuilding the converter
new_db = ...
converter.set_db(new_db)
new_timestamps = ...
converter.set_timestamps(new_timestamps)Note: User 3 has no row at 2026-07-01: their registration_date (2026-08-08) falls after that timestamp, so RTGL excludes them automatically.
By 2026-08-09 they exist, and a row appears.
See RTGL Fundamentals for why this behaviour matters.
Three guides cover the language in depth, each paired with a runnable notebook:
| Guide | Notebook | Covers |
|---|---|---|
| RTGL Fundamentals | 01-standard-tasks.ipynb |
Query anatomy, conditions, aggregations, automatic multi-hop, temporal windows |
| Common Path Expressions | 02-common-path-expressions.ipynb |
Explicit join paths and ambiguity resolution |
| SQL Injections | 03-sql-injections.ipynb |
Raw SQL as a virtual table |
The rtgl-tasks repository collects full prediction tasks built with RTGL on real datasets.
macOS and Linux:
wget -qO- https://astral.sh/uv/install.sh | shWindows:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"uv sync --all-extrasAfter modifying the lexer or parser grammar files (*.g4), regenerate the ANTLR outputs from the repository root:
./regenerate_parser.shpytestruff check .RTGL is released under the MIT License.