11from datetime import datetime
22
33from testgen .common .models import with_database_session
4+ from testgen .common .models .profiling_run import ProfilingRun
45from testgen .common .models .test_definition import TestDefinition
56from testgen .common .source_data_service import (
67 SourceDataResult ,
8+ build_hygiene_query ,
79 build_test_result_query ,
10+ fetch_hygiene_source_data ,
811 fetch_test_result_source_data ,
912)
1013from testgen .mcp .exceptions import MCPResourceNotAccessible , MCPUserError
1114from testgen .mcp .permissions import get_project_permissions , mcp_permission
12- from testgen .mcp .tools .common import DocGroup , parse_uuid , validate_limit
15+ from testgen .mcp .tools .common import DocGroup , parse_uuid , resolve_hygiene_issue , validate_limit
1316from testgen .mcp .tools .markdown import MdDoc
1417
1518_DOC_GROUP = DocGroup .INVESTIGATE
1619
1720
18- def _resolve_context (test_definition_id : str , reference_date : str | None ) -> dict :
21+ def _validate_source_args (test_definition_id : str | None , issue_id : str | None , reference_date : str | None ) -> None :
22+ """Enforce 'exactly one entity' and the reference_date-only-with-test-definition rule."""
23+ if bool (test_definition_id ) == bool (issue_id ):
24+ raise MCPUserError ("Provide exactly one of test_definition_id or issue_id." )
25+ if issue_id and reference_date :
26+ raise MCPUserError (
27+ "reference_date applies only to test_definition_id; omit it when looking up a hygiene issue."
28+ )
29+
30+
31+ def _resolve_test_definition_context (test_definition_id : str , reference_date : str | None ) -> dict :
1932 """Look up the test definition context and validate permissions."""
2033 td_uuid = parse_uuid (test_definition_id , "test_definition_id" )
2134 perms = get_project_permissions ()
@@ -40,40 +53,86 @@ def _resolve_context(test_definition_id: str, reference_date: str | None) -> dic
4053 return context
4154
4255
56+ def _resolve_hygiene_context (issue_id : str ) -> dict :
57+ """Resolve a hygiene issue (permission-scoped) into the lookup context the service expects.
58+
59+ The source profiling run is intrinsic to the issue, so ``profiling_starttime`` comes from the
60+ issue's ``ProfilingRun`` — there is no caller-supplied reference date.
61+ """
62+ issue = resolve_hygiene_issue (issue_id )
63+ run = ProfilingRun .get (issue .profile_run_id )
64+ return {
65+ "table_groups_id" : issue .table_groups_id ,
66+ "anomaly_id" : issue .type_id ,
67+ "detail" : issue .detail ,
68+ "schema_name" : issue .schema_name ,
69+ "table_name" : issue .table_name ,
70+ "column_name" : issue .column_name ,
71+ "profiling_starttime" : run .profiling_starttime if run else None ,
72+ "project_code" : issue .project_code ,
73+ }
74+
75+
76+ def _render_header_fields (doc : MdDoc , context : dict ) -> None :
77+ """Render the entity-neutral location fields shared by both tools."""
78+ if context .get ("test_type" ):
79+ doc .field ("Test type" , context .get ("test_type" ), code = True )
80+ doc .field ("Table" , f"{ context .get ('schema_name' )} .{ context .get ('table_name' )} " , code = True )
81+ column = context .get ("column_names" ) or context .get ("column_name" )
82+ if column :
83+ doc .field ("Column" , column , code = True )
84+
85+
4386@with_database_session
4487@mcp_permission ("view" )
4588def get_source_data_query (
46- test_definition_id : str ,
89+ test_definition_id : str | None = None ,
90+ issue_id : str | None = None ,
4791 reference_date : str | None = None ,
4892 limit : int = 100 ,
4993) -> str :
50- """Get the SQL query that would be used to look up source data for a test definition , without executing it.
94+ """Get the SQL query that would be used to look up source data, without executing it.
5195
52- Builds a lookup query using current test definition parameters (thresholds, conditions) .
96+ Builds a lookup query using the current criteria of a test definition or a hygiene issue .
5397 The query targets the connected database.
5498 Some test types (e.g. Freshness Trend, Schema Drift) do not have source data lookups.
5599
100+ Provide exactly one of ``test_definition_id`` or ``issue_id``.
101+
56102 Args:
57103 test_definition_id: UUID of a test definition, e.g. from ``list_test_results``.
58- reference_date: ISO 8601 date used as the test reference point (default: now).
104+ issue_id: UUID of a hygiene issue, e.g. from ``list_hygiene_issues``. Mutually exclusive
105+ with ``test_definition_id``.
106+ reference_date: ISO 8601 date used as the test reference point (default: now). Applies only
107+ to ``test_definition_id``.
59108 limit: Maximum rows the query would return (default 100, max 500).
60109 """
110+ _validate_source_args (test_definition_id , issue_id , reference_date )
61111 validate_limit (limit , 500 )
62- context = _resolve_context (test_definition_id , reference_date )
63112
64- query = build_test_result_query (context , limit )
113+ if test_definition_id :
114+ context = _resolve_test_definition_context (test_definition_id , reference_date )
115+ entity_label , entity_id = "Test Definition" , test_definition_id
116+ query = build_test_result_query (context , limit )
117+ else :
118+ context = _resolve_hygiene_context (issue_id )
119+ entity_label , entity_id = "Hygiene Issue" , issue_id
120+ query = build_hygiene_query (context , limit )
121+
65122 if not query :
123+ if test_definition_id :
124+ return (
125+ f"Source data lookup is not available for test type `{ context .get ('test_type' , 'unknown' )} `.\n \n "
126+ "This test type does not have a defined lookup query."
127+ )
66128 return (
67- f "Source data lookup is not available for test type ` { context . get ( 'test_type' , 'unknown' ) } ` .\n \n "
68- "This test type does not have a defined lookup query."
129+ "Source data lookup is not available for this hygiene issue .\n \n "
130+ "This hygiene issue type does not have a defined lookup query."
69131 )
70132
71133 doc = MdDoc ()
72- doc .heading (1 , f"Source Data Query for Test Definition `{ test_definition_id } `" )
73- doc .field ("Test type" , context .get ("test_type" ), code = True )
74- doc .field ("Table" , f"{ context .get ('schema_name' )} .{ context .get ('table_name' )} " , code = True )
75- if context .get ("column_names" ):
76- doc .field ("Column" , context ["column_names" ], code = True )
134+ doc .heading (1 , f"Source Data Query for { entity_label } `{ entity_id } `" )
135+ _render_header_fields (doc , context )
77136 doc .field ("Limit" , limit )
78137 doc .code_block (query , language = "sql" )
79138
@@ -83,34 +142,46 @@ def get_source_data_query(
83142@with_database_session
84143@mcp_permission ("view" )
85144def get_source_data (
86- test_definition_id : str ,
145+ test_definition_id : str | None = None ,
146+ issue_id : str | None = None ,
87147 reference_date : str | None = None ,
88148 limit : int = 100 ,
89149) -> str :
90- """Look up rows from the connected database that match or violate a test definition 's criteria.
150+ """Look up rows from the connected database that match or violate a test or hygiene issue 's criteria.
91151
92152 Executes the source data query against the connected database and returns matching rows.
93- Shows CURRENT data — rows may have changed since the test last ran .
153+ Shows CURRENT data — rows may have changed since the test or profiling run .
94154 Some test types (e.g. Freshness Trend, Schema Drift) do not have source data lookups.
95155
156+ Provide exactly one of ``test_definition_id`` or ``issue_id``.
157+
96158 Args:
97159 test_definition_id: UUID of a test definition, e.g. from ``list_test_results``.
98- reference_date: ISO 8601 date used as the test reference point (default: now).
160+ issue_id: UUID of a hygiene issue, e.g. from ``list_hygiene_issues``. Mutually exclusive
161+ with ``test_definition_id``.
162+ reference_date: ISO 8601 date used as the test reference point (default: now). Applies only
163+ to ``test_definition_id``.
99164 limit: Maximum rows to return (default 100, max 500).
100165 """
166+ _validate_source_args (test_definition_id , issue_id , reference_date )
101167 validate_limit (limit , 500 )
102- context = _resolve_context (test_definition_id , reference_date )
168+
169+ if test_definition_id :
170+ context = _resolve_test_definition_context (test_definition_id , reference_date )
171+ entity_label , entity_id = "Test Definition" , test_definition_id
172+ fetch = fetch_test_result_source_data
173+ else :
174+ context = _resolve_hygiene_context (issue_id )
175+ entity_label , entity_id = "Hygiene Issue" , issue_id
176+ fetch = fetch_hygiene_source_data
103177
104178 mask_pii = not get_project_permissions ().has_permission ("view_pii" , context .get ("project_code" ))
105179
106- result : SourceDataResult = fetch_test_result_source_data (context , limit , mask_pii )
180+ result : SourceDataResult = fetch (context , limit , mask_pii )
107181
108182 doc = MdDoc ()
109- doc .heading (1 , f"Source Data for Test Definition `{ test_definition_id } `" )
110- doc .field ("Test type" , context .get ("test_type" ), code = True )
111- doc .field ("Table" , f"{ context .get ('schema_name' )} .{ context .get ('table_name' )} " , code = True )
112- if context .get ("column_names" ):
113- doc .field ("Column" , context ["column_names" ], code = True )
183+ doc .heading (1 , f"Source Data for { entity_label } `{ entity_id } `" )
184+ _render_header_fields (doc , context )
114185
115186 if result .status == "OK" :
116187 row_count = len (result .df ) if result .df is not None else 0
0 commit comments