recon-driven sql composer; pick → compose → execute; llm out of structural sql

This commit is contained in:
2026-06-03 11:01:02 -03:00
parent 61494362a3
commit 29c620b2c2
27 changed files with 1516 additions and 249 deletions

View File

@@ -0,0 +1,229 @@
"""Composer SQL emission — golden-string tests against a representative recon.
The fixture mirrors enough of the financial dataset to exercise: metric
with default filter, group_by with single hop and multi-hop joins, typed
date_range filter on a real DATE column, and ORDER BY by both metric and
dimension. Adjust goldens cautiously — if a string changes, confirm the
new SQL still semantically matches before updating the test.
"""
from api.composer import compose
from api.composer.types import Filter, OrderBy, Pick, PickValidationError
from api.recon.types import Column, Metric, Recon, Relationship, Table
import pytest
def _fixture_recon() -> Recon:
cols_account = [
Column("account_id", "BIGINT", False),
Column("district_id", "BIGINT", True),
]
cols_district = [
Column("district_id", "BIGINT", False),
Column("A2", "TEXT", True),
Column("A3", "TEXT", True),
]
cols_loan = [
Column("loan_id", "BIGINT", False),
Column("account_id", "BIGINT", True),
Column("amount", "BIGINT", True),
Column("status", "TEXT", True),
Column("date", "DATE", True),
]
rels = [
Relationship("account", "district_id", "district", "district_id"),
Relationship("loan", "account_id", "account", "account_id"),
]
c2t: dict[str, list[str]] = {}
for t in [Table("account", "accounts", cols_account),
Table("district", "districts", cols_district),
Table("loan", "loans", cols_loan)]:
for c in t.columns:
c2t.setdefault(c.name, []).append(t.name)
for k in c2t:
c2t[k] = sorted(c2t[k])
metrics = {
"loan_default_rate": Metric(
name="loan_default_rate",
description="Default rate over finished loans.",
sql="AVG(CASE WHEN status = 'B' THEN 1.0 WHEN status = 'A' THEN 0.0 END)",
from_table="loan",
filter="status IN ('A','B')",
unit="ratio",
),
"loan_volume": Metric(
name="loan_volume",
description="Total CZK lent.",
sql="SUM(amount)",
from_table="loan",
filter=None,
unit="CZK",
),
}
return Recon(
schema="financial",
tables={
"account": Table("account", "accounts", cols_account),
"district": Table("district", "districts", cols_district),
"loan": Table("loan", "loans", cols_loan),
},
metrics=metrics,
column_to_tables=c2t,
relationships=rels,
)
# ── Aggregate, no group_by ──────────────────────────────────
def test_metric_only_no_group_by():
r = _fixture_recon()
pick = Pick(kind="aggregate", metric="loan_volume")
out = compose(pick, r)
assert out.sql == 'SELECT SUM("l"."amount") AS "loan_volume" FROM "loan" AS "l"'
assert out.used_tables == ["loan"]
def test_metric_with_default_filter_no_group_by():
r = _fixture_recon()
pick = Pick(kind="aggregate", metric="loan_default_rate")
out = compose(pick, r)
expected = (
"SELECT AVG(CASE WHEN \"l\".\"status\" = 'B' THEN 1.0 "
"WHEN \"l\".\"status\" = 'A' THEN 0.0 END) AS \"loan_default_rate\" "
"FROM \"loan\" AS \"l\" "
"WHERE \"l\".\"status\" IN ('A', 'B')"
)
assert out.sql == expected
# ── Group by — single hop ───────────────────────────────────
def test_group_by_multi_hop_join():
r = _fixture_recon()
pick = Pick(
kind="aggregate",
metric="loan_default_rate",
group_by=["A2"],
limit=10,
)
out = compose(pick, r)
# loan → account → district, A2 owned by district.
assert 'FROM "loan" AS "l"' in out.sql
assert 'JOIN "account" AS "a" ON "l"."account_id" = "a"."account_id"' in out.sql
assert (
'JOIN "district" AS "d" ON "a"."district_id" = "d"."district_id"' in out.sql
)
assert 'GROUP BY "A2"' in out.sql
# default ORDER BY metric DESC
assert 'ORDER BY "loan_default_rate" DESC' in out.sql
assert "LIMIT 10" in out.sql
assert set(out.used_tables) == {"loan", "account", "district"}
# ── Filters ─────────────────────────────────────────────────
def test_where_equals_filter():
r = _fixture_recon()
pick = Pick(
kind="aggregate",
metric="loan_volume",
where=[Filter(column="status", equals="D")],
)
out = compose(pick, r)
assert "WHERE \"l\".\"status\" = 'D'" in out.sql
def test_where_in_values_filter():
r = _fixture_recon()
pick = Pick(
kind="aggregate",
metric="loan_volume",
where=[Filter(column="status", in_values=["C", "D"])],
)
out = compose(pick, r)
assert "\"l\".\"status\" IN ('C', 'D')" in out.sql
def test_where_date_range_on_real_date_column():
r = _fixture_recon()
pick = Pick(
kind="aggregate",
metric="loan_default_rate",
where=[Filter(column="date", date_range="1996")],
)
out = compose(pick, r)
assert "\"l\".\"date\" BETWEEN '1996-01-01' AND '1996-12-31'" in out.sql
# metric's default filter still ANDed in
assert "\"l\".\"status\" IN ('A', 'B')" in out.sql
def test_metric_filter_ands_with_pick_filter():
r = _fixture_recon()
pick = Pick(
kind="aggregate",
metric="loan_default_rate",
where=[Filter(column="date", date_range="1996")],
)
out = compose(pick, r)
# Both filters in the same WHERE clause, joined by AND.
assert " AND " in out.sql
# ── ORDER BY by dimension ───────────────────────────────────
def test_order_by_dimension():
r = _fixture_recon()
pick = Pick(
kind="aggregate",
metric="loan_volume",
group_by=["A3"],
order_by=OrderBy(by="dimension", direction="asc", dimension="A3"),
)
out = compose(pick, r)
assert 'ORDER BY "A3" ASC' in out.sql
def test_order_by_dimension_not_in_group_by_raises():
r = _fixture_recon()
pick = Pick(
kind="aggregate",
metric="loan_volume",
group_by=["A2"],
order_by=OrderBy(by="dimension", direction="asc", dimension="A3"),
)
with pytest.raises(PickValidationError, match="not in group_by"):
compose(pick, r)
# ── Validation rejections ───────────────────────────────────
def test_unknown_metric_raises():
r = _fixture_recon()
pick = Pick(kind="aggregate", metric="moon_phase")
with pytest.raises(PickValidationError, match="not in recon"):
compose(pick, r)
def test_unknown_group_by_column_raises():
r = _fixture_recon()
pick = Pick(kind="aggregate", metric="loan_volume", group_by=["nonsense"])
with pytest.raises(ValueError, match="no table has a column named 'nonsense'"):
compose(pick, r)
def test_ambiguous_group_by_column_raises():
r = _fixture_recon()
# account_id lives on both account and loan; must be qualified.
pick = Pick(kind="aggregate", metric="loan_volume", group_by=["account_id"])
with pytest.raises(ValueError, match="ambiguous"):
compose(pick, r)
def test_qualified_disambiguates():
r = _fixture_recon()
pick = Pick(kind="aggregate", metric="loan_volume", group_by=["loan.account_id"])
out = compose(pick, r)
# Output alias becomes "loan_account_id" since the ref was qualified.
assert '"loan_account_id"' in out.sql