It was found that this code mistake caused an infinite loop in production:
DECLARE @BatchID INT
DECLARE MyCursor CURSOR FOR
SELECT BatchID = ...
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO @BatchID
WHILE @@FETCH_STATUS = 0
BEGIN
...
IF [condition]
BEGIN
...
FETCH NEXT FROM MyCursor INTO @BatchID
END
END
CLOSE MyCursor
DEALLOCATE MyCursor
Assuming the need for a cursor [is warranted][1], is there any way to safeguard against this mistake (besides more testing/code review)?
In other languages, we have `FOREACH` loops that manage progressing the iteration for us, or `FOR` loops that have an [afterthought][2], so is there any equivalent in SQL Server that prevents sloppy mistakes like misplacing (or forgetting!) it?
I have never seen custom loops in SQL need to do anything [fancy][3] with the cursor, and `WHILE` loops have the same risk (along with cases like `DELETE #WorkData WHERE ID = @BatchID` when [@BatchID is `NULL`][4]), so how might this risk be mitigated programmatically/cleanly for the typical use case?
---
An approach like this is very unappealing:
DECLARE @BatchID INT
DECLARE MyCursor CURSOR FOR
SELECT BatchID = -1--dummy entry to always be skipped
UNION ALL SELECT BatchID = ...
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO @BatchID
WHILE @@FETCH_STATUS = 0
BEGIN
FETCH NEXT FROM MyCursor INTO @BatchID
IF @@FETCH_STATUS = 0
BEGIN
...
END
END
CLOSE MyCursor
DEALLOCATE MyCursor
It seems to me that checking `@@FETCH_STATUS` twice in a row without any cursor position/control statements in-between could be an effective way for SQL Server to *guess* if there was such a mistake, as my experience with cursors has never seen two checks in a row intentionally (without nested cursors at least, but those still have control statements like `OPEN` and `CLOSE` between checks of `@@FETCH_STATUS` of the outer cursor).
<sub>P.S. The `[condition]` this time was equivalent to "not the DST transition day" (which was this Sunday). More time zone bugs!</sub>
I am most interested about **language features** to achieve the same automatically-provided guarantee of how other languages handle the *afterthought* clause of a for-loop (e.g. the `i++` in `for (int i = 0; i < myArray.Length; i++)`). The fewer things the individual is responsible for, the fewer places there are to make a mistake, and it would be a monumental task to audit the literally thousands of stored procedures in our system, especially since many of them are inappropriately iterating data instead of using set-based logic already.
[1]: https://stackoverflow.com/questions/58141/why-is-it-considered-bad-practice-to-use-cursors-in-sql-server
[2]: https://en.wikipedia.org/wiki/For_loop#Traditional_for-loops
[3]: https://docs.microsoft.com/en-us/sql/t-sql/language-elements/declare-cursor-transact-sql?view=sql-server-ver15#arguments
[4]: https://docs.microsoft.com/en-us/sql/t-sql/language-elements/equals-transact-sql?view=sql-server-ver15#remarks
To avoid problems like this I'm using different pattern for fetch:
```
OPEN Cursor;
WHILE 1=1
BEGIN
FETCH NEXT FROM Cursor INTO [...];
IF @@FETCH_STATUS <> 0 BREAK;
[...]
END;
CLOSE Cursor;
DEALLOCATE Cursor;
```
In this pattern you have `FETCH` exactly once and you need to check fetch status once too.