Skip to content

diag: @GradeOverrides and @GradeWeights parsed with CURSOR — replace with set-based STRING_SPLIT to eliminate cursor overhead and simplify error accumulation #302

@nanoDBA

Description

@nanoDBA

Problem

The @GradeOverrides and @GradeWeights parameter parsing blocks (lines ~475–555) use explicit CURSOR LOCAL FAST_FORWARD loops over STRING_SPLIT results. This pattern is anti-idiomatic when set-based alternatives are available, adds visual complexity to the parameter validation section, and is harder to maintain correctly (cursor lifecycle management, CONTINUE / FETCH NEXT in each error branch).

The cursor approach also has a subtle bug: if RAISERROR severity is raised to 16 inside the cursor loop for validation errors (it isn't currently, errors accumulate in @errors), the cursor would not be cleaned up. Even at severity 10 (the current approach), any future refactoring that adds an early RETURN inside the loop risks leaving the cursor open.

Additionally, the cursor declaration uses undeclared column aliases (@go_pair) populated from STRING_SPLIT's value column, which is fine but relies on STRING_SPLIT always returning a single column named value — this is an implicit contract not documented in the code.

Evidence

Grade override cursor block (lines ~478–520):

DECLARE go_cursor CURSOR LOCAL FAST_FORWARD FOR
    SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@GradeOverrides, N',') WHERE LTRIM(RTRIM(value)) <> N'';
OPEN go_cursor;
FETCH NEXT FROM go_cursor INTO @go_pair;

WHILE @@FETCH_STATUS = 0
BEGIN
    IF CHARINDEX(N'=', @go_pair) = 0
    BEGIN
        SET @errors = @errors + N'@GradeOverrides: invalid pair ...';
        FETCH NEXT FROM go_cursor INTO @go_pair;
        CONTINUE;
    END;
    ...
    FETCH NEXT FROM go_cursor INTO @go_pair;
END;

CLOSE go_cursor;
DEALLOCATE go_cursor;

The same pattern is repeated verbatim for @GradeWeights (lines ~525–565).

Proposed Fix

Replace both cursor blocks with set-based CROSS APPLY / STRING_SPLIT parsing using a single inline table expression. Error accumulation can still be done via STRING_AGG or concatenation from a derived table:

-- @GradeOverrides: set-based parse
;WITH override_pairs AS (
    SELECT
        pair    = LTRIM(RTRIM(value)),
        cat     = UPPER(LTRIM(RTRIM(LEFT(LTRIM(RTRIM(value)), CHARINDEX('=', LTRIM(RTRIM(value))) - 1)))),
        val     = UPPER(LTRIM(RTRIM(SUBSTRING(LTRIM(RTRIM(value)), CHARINDEX('=', LTRIM(RTRIM(value))) + 1, 50))))
    FROM STRING_SPLIT(@GradeOverrides, N',')
    WHERE LTRIM(RTRIM(value)) <> N''
    AND   CHARINDEX('=', LTRIM(RTRIM(value))) > 0
)
SELECT
    @override_completion  = MAX(CASE WHEN cat = N'COMPLETION'  THEN CASE WHEN val = 'IGNORE' THEN 'X' ELSE LEFT(val,1) END END),
    @override_reliability = MAX(CASE WHEN cat = N'RELIABILITY' THEN CASE WHEN val = 'IGNORE' THEN 'X' ELSE LEFT(val,1) END END),
    ...
FROM override_pairs
WHERE cat IN (N'COMPLETION', N'RELIABILITY', N'SPEED', N'WORKLOAD')
AND   val IN (N'A', N'B', N'C', N'D', N'F', N'IGNORE');

Error messages for invalid pairs can be accumulated with a separate STRING_AGG over rows that fail the validation predicates.

Impact

Low. The cursor approach works correctly in practice (FAST_FORWARD, small input sets). The impact is primarily code quality, maintainability, and adherence to the procedure's own T-SQL style (which is otherwise consistently set-based). The subtle cursor leak risk on future refactoring is the most concrete functional concern.

Metadata

Metadata

Assignees

No one assigned

    Labels

    by-designWorking as intended, kept for referencep4-deferredValid but deferred to future releasepersona-reviewIssue generated via SQL SME persona code reviewtriage-reviewedReviewed and triaged 2026-03-11

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions