"""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