recon-driven sql composer; pick → compose → execute; llm out of structural sql
This commit is contained in:
229
tests/test_composer_compose.py
Normal file
229
tests/test_composer_compose.py
Normal 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
|
||||
Reference in New Issue
Block a user