"""Regression: recon SQL validator catches LLM-generated schema mismatches. Bug captured: drill_down generated `SELECT district_id FROM loan` — syntactically valid, parses, but `loan` doesn't have a `district_id` column. We want to catch this BEFORE Postgres does, with a clear message naming where the column actually lives. """ import pytest from api.recon.types import Column, Metric, Recon, Relationship, Table from api.recon.validate import ReconValidationError, validate_sql def _recon() -> Recon: return Recon( schema="financial", tables={ "loan": Table("loan", None, [ Column("loan_id", "BIGINT", False), Column("account_id", "BIGINT", True), Column("amount", "BIGINT", True), Column("status", "TEXT", True), ]), "account": Table("account", None, [ Column("account_id", "BIGINT", False), Column("district_id", "BIGINT", True), ]), "district": Table("district", None, [ Column("district_id", "BIGINT", False), Column("A2", "TEXT", True), Column("A13", "BIGINT", True), ]), }, metrics={}, column_to_tables={ "loan_id": ["loan"], "account_id": ["account", "loan"], "amount": ["loan"], "status": ["loan"], "district_id": ["account", "district"], "A2": ["district"], "A13": ["district"], }, relationships=[ Relationship("account", "district_id", "district", "district_id"), Relationship("loan", "account_id", "account", "account_id"), ], ) def test_valid_sql_passes(): sql = "SELECT account_id, amount FROM loan WHERE status = 'B'" validate_sql(sql, _recon()) def test_valid_join_passes(): sql = """ SELECT d.A2, AVG(l.amount) FROM loan l JOIN account a ON l.account_id = a.account_id JOIN district d ON a.district_id = d.district_id GROUP BY d.A2 """ validate_sql(sql, _recon()) def test_unknown_column_on_table_raises(): # The exact bug we hit in production: district_id picked from loan # without joining through account. sql = """ WITH x AS (SELECT district_id, AVG(amount) FROM loan GROUP BY district_id) SELECT * FROM x """ with pytest.raises(ReconValidationError) as exc: validate_sql(sql, _recon()) msg = str(exc.value) assert "district_id" in msg # The hint should name where district_id actually lives. assert "account" in msg and "district" in msg def test_misspelled_column_raises(): sql = "SELECT amnt FROM loan" with pytest.raises(ReconValidationError) as exc: validate_sql(sql, _recon()) assert "amnt" in str(exc.value) def test_qualified_column_on_wrong_table_raises(): # district.amount doesn't exist (amount is on loan). sql = "SELECT d.amount FROM district d" with pytest.raises(ReconValidationError): validate_sql(sql, _recon())