namespace TCMSDAL.QueryBuilders; /// /// SQL for TCMS Requirement Traceability. /// All queries run against the TCMS database via IQueryExecutor. /// /// Dual-source strategy: /// GetByScreen merges MTCMSSCREENLINK (explicit admin links) with the Tags field /// using the @FE:{route} prefix convention, so the widget works from day one /// without any admin setup. /// /// Required indexes (created by 20260606_TCMS_Traceability_Schema.sql): /// IX_MTCMSSCREENLINK_ROUTE(SCREENROUTE) /// IX_MTCMSSCREENLINK_TC(TESTCASEID) /// IX_MTCMSAPILINK_OP(APIOPERATIONID) WHERE NOT NULL /// IX_MTCMSAPILINK_TC(TESTCASEID) /// public static class TCMSTraceabilityQB { // ── Screen link CRUD ────────────────────────────────────────────────────── public const string INSERT_SCREEN_LINK = @" INSERT INTO MTCMSSCREENLINK (TESTCASEID, SCREENROUTE, SCREENTITLE, MODULENAME, CREATEDBYID, CREATEDON) OUTPUT INSERTED.SCREENLINKID VALUES (@TestCaseId, @ScreenRoute, @ScreenTitle, @ModuleName, @CreatedById, GETDATE())"; public const string DELETE_SCREEN_LINK = @" DELETE FROM MTCMSSCREENLINK WHERE SCREENLINKID = @ScreenLinkId"; public const string GET_SCREEN_LINKS_FOR_TESTCASE = @" SELECT SCREENLINKID AS ScreenLinkId, TESTCASEID AS TestCaseId, SCREENROUTE AS ScreenRoute, SCREENTITLE AS ScreenTitle, MODULENAME AS ModuleName, CREATEDBYID AS CreatedById FROM MTCMSSCREENLINK WHERE TESTCASEID = @TestCaseId ORDER BY SCREENTITLE"; // ── API link CRUD ───────────────────────────────────────────────────────── public const string INSERT_API_LINK = @" INSERT INTO MTCMSAPILINK (TESTCASEID, APIOPERATIONID, APIPATH, HTTPMETHOD, CREATEDBYID, CREATEDON) OUTPUT INSERTED.APILINKID VALUES (@TestCaseId, @ApiOperationId, @ApiPath, @HttpMethod, @CreatedById, GETDATE())"; public const string DELETE_API_LINK = @" DELETE FROM MTCMSAPILINK WHERE APILINKID = @ApiLinkId"; public const string GET_API_LINKS_FOR_TESTCASE = @" SELECT APILINKID AS ApiLinkId, TESTCASEID AS TestCaseId, APIOPERATIONID AS ApiOperationId, APIPATH AS ApiPath, HTTPMETHOD AS HttpMethod, CREATEDBYID AS CreatedById FROM MTCMSAPILINK WHERE TESTCASEID = @TestCaseId ORDER BY HTTPMETHOD, APIPATH"; // ── GetByScreen — dual source (link table UNION tag prefix) ────────────── /// /// Returns slim test case rows for a given FE screen route. /// Priority: link-table rows (with ScreenLinkId) come first; tag-convention rows /// (@FE:{route}) are included only when no explicit link exists, so each TC appears once. /// Steps loaded separately via GET_STEPS_FOR_CASES. /// public const string GET_TESTCASES_BY_SCREEN = @" -- Explicitly linked test cases — include ScreenLinkId so the FE can unlink SELECT sl.SCREENLINKID AS ScreenLinkId, NULL AS ApiLinkId, tc.TESTCASEID AS TestCaseId, tc.TCID AS TcId, tc.TITLE AS Title, tc.PRIORITY AS Priority, tc.TESTTYPE AS TestType, tc.EXECUTIONTYPE AS ExecutionType, tc.LASTRUNSTATUS AS LastRunStatus, tc.LASTRUNDATE AS LastRunDate FROM MTCMSTESTCASE tc JOIN MTCMSSCREENLINK sl ON sl.TESTCASEID = tc.TESTCASEID WHERE sl.SCREENROUTE = @ScreenRoute AND tc.STATUS = 'Active' UNION ALL -- Tag-convention rows included only when no explicit link table row exists SELECT NULL AS ScreenLinkId, NULL AS ApiLinkId, tc.TESTCASEID AS TestCaseId, tc.TCID AS TcId, tc.TITLE AS Title, tc.PRIORITY AS Priority, tc.TESTTYPE AS TestType, tc.EXECUTIONTYPE AS ExecutionType, tc.LASTRUNSTATUS AS LastRunStatus, tc.LASTRUNDATE AS LastRunDate FROM MTCMSTESTCASE tc WHERE tc.TAGS LIKE '%@FE:' + @ScreenRoute + '%' AND tc.STATUS = 'Active' AND NOT EXISTS ( SELECT 1 FROM MTCMSSCREENLINK sl2 WHERE sl2.TESTCASEID = tc.TESTCASEID AND sl2.SCREENROUTE = @ScreenRoute ) ORDER BY Priority, TcId"; // ── GetByApiOperation ───────────────────────────────────────────────────── public const string GET_TESTCASES_BY_API_OPERATION = @" SELECT al.APILINKID AS ApiLinkId, NULL AS ScreenLinkId, tc.TESTCASEID AS TestCaseId, tc.TCID AS TcId, tc.TITLE AS Title, tc.PRIORITY AS Priority, tc.TESTTYPE AS TestType, tc.EXECUTIONTYPE AS ExecutionType, tc.LASTRUNSTATUS AS LastRunStatus, tc.LASTRUNDATE AS LastRunDate FROM MTCMSTESTCASE tc JOIN MTCMSAPILINK al ON al.TESTCASEID = tc.TESTCASEID WHERE al.APIOPERATIONID = @ApiOperationId AND tc.STATUS = 'Active' ORDER BY Priority, TcId"; // ── Steps (bulk load by case-id list, avoid N+1) ───────────────────────── /// /// Load steps for a set of test cases in one round-trip. /// Caller filters to relevant TestCaseIds using STRING_SPLIT on @Ids. /// Dapper maps by TestCaseId for client-side grouping. /// public const string GET_STEPS_FOR_CASES = @" SELECT s.TESTCASEID AS TestCaseId, s.STEPTYPE AS StepType, s.STEPTEXT AS StepText FROM MTCMSTESTSTEP s WHERE s.TESTCASEID IN ( SELECT TRY_CAST(value AS INT) FROM STRING_SPLIT(@CaseIdsCsv, ',') ) ORDER BY s.TESTCASEID, s.STEPORDER"; // ── Screen tree (left panel) ────────────────────────────────────────────── public const string GET_SCREEN_TREE = @" SELECT sl.SCREENROUTE AS ScreenRoute, sl.SCREENTITLE AS ScreenTitle, sl.MODULENAME AS ModuleName, COUNT(DISTINCT sl.TESTCASEID) AS TotalCases FROM MTCMSSCREENLINK sl JOIN MTCMSTESTCASE tc ON tc.TESTCASEID = sl.TESTCASEID AND tc.STATUS = 'Active' GROUP BY sl.SCREENROUTE, sl.SCREENTITLE, sl.MODULENAME ORDER BY sl.MODULENAME, sl.SCREENTITLE"; // ── Coverage gaps ───────────────────────────────────────────────────────── /// /// Registered screens with zero active test cases. /// All registered routes come from the link table; orphaned routes (no active cases) /// appear here so QA can see where coverage has dropped to zero. /// public const string GET_SCREEN_GAPS = @" SELECT sl.SCREENROUTE AS Route, sl.SCREENTITLE AS Title, sl.MODULENAME AS ModuleName, COUNT(DISTINCT CASE WHEN tc.STATUS = 'Active' THEN tc.TESTCASEID END) AS LinkedCases, 'Screen' AS GapType FROM MTCMSSCREENLINK sl LEFT JOIN MTCMSTESTCASE tc ON tc.TESTCASEID = sl.TESTCASEID GROUP BY sl.SCREENROUTE, sl.SCREENTITLE, sl.MODULENAME HAVING COUNT(DISTINCT CASE WHEN tc.STATUS = 'Active' THEN tc.TESTCASEID END) = 0 ORDER BY sl.MODULENAME, sl.SCREENTITLE"; public const string GET_API_GAPS = @" SELECT al.APIPATH AS Route, al.APIPATH AS Title, al.HTTPMETHOD AS ModuleName, COUNT(DISTINCT CASE WHEN tc.STATUS = 'Active' THEN tc.TESTCASEID END) AS LinkedCases, 'ApiOperation' AS GapType FROM MTCMSAPILINK al LEFT JOIN MTCMSTESTCASE tc ON tc.TESTCASEID = al.TESTCASEID GROUP BY al.APIPATH, al.HTTPMETHOD HAVING COUNT(DISTINCT CASE WHEN tc.STATUS = 'Active' THEN tc.TESTCASEID END) = 0 ORDER BY al.APIPATH"; // ── Coverage summary for a screen (used to build TCMSScreenCoverageDTO) ── public const string GET_SCREEN_SUMMARY = @" SELECT sl.SCREENROUTE AS ScreenRoute, sl.SCREENTITLE AS ScreenTitle, sl.MODULENAME AS ModuleName, COUNT(DISTINCT tc.TESTCASEID) AS TotalCases, COUNT(DISTINCT CASE WHEN tc.PRIORITY = 'P1' THEN tc.TESTCASEID END) AS P1Count, COUNT(DISTINCT CASE WHEN tc.PRIORITY = 'P2' THEN tc.TESTCASEID END) AS P2Count, COUNT(DISTINCT CASE WHEN tc.PRIORITY = 'P3' THEN tc.TESTCASEID END) AS P3Count, COUNT(DISTINCT CASE WHEN tc.LASTRUNSTATUS = 'Pass' THEN tc.TESTCASEID END) AS PassCount, COUNT(DISTINCT CASE WHEN tc.LASTRUNSTATUS = 'Fail' THEN tc.TESTCASEID END) AS FailCount, MAX(tc.LASTRUNDATE) AS LastRunDate FROM MTCMSSCREENLINK sl JOIN MTCMSTESTCASE tc ON tc.TESTCASEID = sl.TESTCASEID AND tc.STATUS = 'Active' WHERE sl.SCREENROUTE = @ScreenRoute GROUP BY sl.SCREENROUTE, sl.SCREENTITLE, sl.MODULENAME"; }