sql cursor errors showing python code with a psycopg2 cursor error representing how to fix sql cursor errors in psycopg2 node odbc tsql arcpy and sqlalchemy

SQL Cursor Errors Fixed: psycopg2, Node ODBC, TSQL, arcpy, and SQLAlchemy Guide

SQL cursors appear across multiple programming environments — Python’s psycopg2, Node.js ODBC, Microsoft’s T-SQL, Esri’s arcpy, and SQLAlchemy all have their own cursor implementations and their own characteristic errors. Here’s the fix for each of the most common cursor-related errors, organized by the tool that generated them.

psycopg2: cursor.execute() Not Accepting Returns in String

This error appears in two different scenarios in psycopg2, and they have different fixes:

Scenario 1: RETURNING Clause Not Fetching Results

When you use a PostgreSQL RETURNING clause on an INSERT, UPDATE, or DELETE and call cursor.execute(), the result is available — but you must explicitly fetch it immediately after. Not calling fetchone() or fetchall() before the cursor moves on is the most common source of ‘not accepting returns’ confusion.

# Wrong – executes but doesn’t retrieve the returned value

cursor.execute(

    ‘INSERT INTO users (name) VALUES (%s) RETURNING id’,

    (‘Alice’,)

)

# At this point the returned id is not captured

# Correct – fetch immediately after execute

cursor.execute(

    ‘INSERT INTO users (name) VALUES (%s) RETURNING id’,

    (‘Alice’,)

)

returned_id = cursor.fetchone()[0]  # Fetch the RETURNING value

The fetchone() call must happen before any other execute() call on the same cursor, or the result set is lost.

Scenario 2: String Interpolation Instead of Parameterized Queries

A common mistake is building SQL strings with Python f-strings or .format() rather than psycopg2’s parameterized query syntax. This causes syntax errors, unexpected behavior, and SQL injection vulnerabilities:

# Wrong – f-string interpolation

name = user_input

cursor.execute(f’SELECT * FROM users WHERE name = {name}’)

# Correct – parameterized query (psycopg2 handles escaping)

cursor.execute(‘SELECT * FROM users WHERE name = %s’, (name,))

Note the comma inside the tuple for a single parameter: (name,) not (name). A bare (name) without the comma is not a tuple in Python — psycopg2 will raise a TypeError. The %s placeholder is used for all data types in psycopg2, regardless of the actual Python type.

Scenario 3: Using cursor() on a Connection Object

If you’re calling cursor.execute() and getting an AttributeError rather than a syntax error, verify you’re calling it on a cursor object, not the connection itself:

import psycopg2

conn = psycopg2.connect(dsn)

# Wrong – calling execute on connection

conn.execute(‘SELECT …’)  # AttributeError

# Correct – create cursor first

cursor = conn.cursor()

cursor.execute(‘SELECT …’)

Node.js ODBC: Invalid Cursor State (Error 24000)

ODBC state code 24000 means ‘Invalid Cursor State’ — the cursor is not in a state where the requested operation is valid. In node-odbc this typically appears in one of three situations:

Cause 1: Fetching After Results Are Exhausted

Calling cursor.fetch() or statement.fetch() after the last row has been returned gives an invalid cursor state because there is nothing left to fetch. The fetch method returns null when the result set is exhausted; continuing to call fetch after null is returned triggers the error.

// Correct pattern – check for null on each fetch

let row;

while ((row = await statement.fetch()) !== null) {

    console.log(row);

}

Cause 2: Using cursor.fetch() When connection.query() Was Used

In node-odbc, connection.query() executes a SQL statement and returns results as an array — it doesn’t create a cursor you iterate with fetch(). Calling fetch() after query() will throw an invalid cursor state because query() already consumed and closed the result set.

// Wrong – query() returns array, not a cursor to iterate

const result = await connection.query(‘SELECT * FROM users’);

// result is already an array here – don’t call fetch()

// Correct – use the array directly

const result = await connection.query(‘SELECT * FROM users’);

result.forEach(row => console.log(row));

// If you need cursor-based iteration, use statement API

const statement = await connection.createStatement();

await statement.prepare(‘SELECT * FROM users’);

await statement.execute();

let row;

while ((row = await statement.fetch()) !== null) {

    console.log(row);

}

Cause 3: Multiple Result Sets (Stored Procedures)

When a stored procedure returns multiple result sets, ODBC requires advancing through each result set before fetching from the next one. Attempting to fetch from the second result set without first completing (or explicitly closing) the first generates an invalid cursor state error. Use statement.nextResult() to advance between result sets.

T-SQL Cursor: Complete Syntax and Best Practices

A T-SQL cursor is a database object that allows row-by-row processing of a query result set in SQL Server and Azure SQL. The full lifecycle:

— 1. Declare the cursor

DECLARE user_cursor CURSOR

    FORWARD_ONLY STATIC READ_ONLY

    FOR

    SELECT user_id, username FROM users WHERE active = 1;

— 2. Declare variables to hold fetched values

DECLARE @user_id INT, @username NVARCHAR(100);

— 3. Open the cursor

OPEN user_cursor;

— 4. Fetch the first row

FETCH NEXT FROM user_cursor INTO @user_id, @username;

— 5. Loop while fetch succeeded (@@FETCH_STATUS = 0)

WHILE @@FETCH_STATUS = 0

BEGIN

    — Process the row

    PRINT ‘Processing: ‘ + @username;

    — Fetch next row at end of loop

    FETCH NEXT FROM user_cursor INTO @user_id, @username;

END

— 6. Close and deallocate

CLOSE user_cursor;

DEALLOCATE user_cursor;

Key points about T-SQL cursor types:

Cursor TypeBehaviorBest For
FORWARD_ONLYCan only scroll forward; fastestSequential processing (default)
STATICCreates a snapshot at open time; data doesn’t reflect updatesRead-only iteration where base data may change
KEYSETKey values fixed at open; detects updates to non-key columnsOrdered browsing with update visibility
DYNAMICReflects all changes to base data; slowestWhen you need live data visibility during iteration

T-SQL cursors should be a last resort. Set-based SQL operations (using WHERE, JOIN, GROUP BY, and window functions) consistently outperform cursors for the same work. Cursors process one row at a time, requiring a round-trip per row. An UPDATE…FROM or a CTE can typically replace a cursor pattern with dramatically better performance.

Nested Cursors in T-SQL

Two cursors — one inside the other — are valid T-SQL but require careful naming. Each cursor must have a unique name, and the inner cursor must complete its full lifecycle (OPEN, FETCH loop, CLOSE, DEALLOCATE) for every iteration of the outer cursor:

DECLARE outer_cursor CURSOR FOR SELECT dept_id FROM departments;

OPEN outer_cursor;

FETCH NEXT FROM outer_cursor INTO @dept_id;

WHILE @@FETCH_STATUS = 0

BEGIN

    — Inner cursor for each department

    DECLARE inner_cursor CURSOR FOR

        SELECT emp_id FROM employees WHERE dept_id = @dept_id;

    OPEN inner_cursor;

    FETCH NEXT FROM inner_cursor INTO @emp_id;

    WHILE @@FETCH_STATUS = 0

    BEGIN

        — Process employee

        FETCH NEXT FROM inner_cursor INTO @emp_id;

    END

    CLOSE inner_cursor;

    DEALLOCATE inner_cursor;

    FETCH NEXT FROM outer_cursor INTO @dept_id;

END

CLOSE outer_cursor;

DEALLOCATE outer_cursor;

Nested cursors compound the performance penalty of row-by-row processing exponentially. If you find yourself writing nested cursors, step back and consider whether the logic can be expressed as a single set-based query, possibly with a temp table or CTE to hold intermediate results.

arcpy UpdateCursor: How to Update Features in ArcGIS

The arcpy.da.UpdateCursor is the correct way to iterate over and modify features in an ArcGIS feature class or table. The da (Data Access) version is significantly faster than the older arcpy.UpdateCursor and should always be used in current scripts.

import arcpy

feature_class = r’C:/data/cities.gdb/Cities’

fields = [‘Population’, ‘Status’]

# Use ‘with’ statement for automatic cursor cleanup

with arcpy.da.UpdateCursor(feature_class, fields) as cursor:

    for row in cursor:

        # row[0] = Population, row[1] = Status

        if row[0] > 1000000:

            row[1] = ‘Major City’

            cursor.updateRow(row)  # Must call updateRow to save changes

Critical points for arcpy UpdateCursor:

  • Always call cursor.updateRow(row) after modifying values — without this call, changes are not saved to the feature class
  • Use the ‘with’ statement — it ensures the cursor and any file locks are released when the block exits, even if an error occurs
  • If not using ‘with’, call del cursor explicitly at the end — otherwise the feature class remains locked and other operations will fail
  • The WHERE clause (fourth parameter) filters which rows are returned: arcpy.da.UpdateCursor(fc, fields, where_clause=’Population > 500000′)
  • Field names are case-insensitive in arcpy but must exactly match field names as they exist in the feature class (no spaces or special characters that aren’t in the actual field name)
  • OID@ and SHAPE@ are special token fields: ‘OID@’ returns the Object ID, ‘SHAPE@’ returns the geometry object

Common arcpy UpdateCursor Errors

ErrorFix
RuntimeError: cannot open cursorFeature class is locked by another process or ArcGIS Pro session. Close ArcGIS Pro or release other cursors on the same data
FieldError: field not foundField name doesn’t exist or is misspelled. Check arcpy.ListFields(fc) to see exact field names
Changes not saved after loopNot calling cursor.updateRow(row). Add cursor.updateRow(row) after modifying row values
Feature class still locked after scriptNot using ‘with’ statement or not calling del cursor. Use ‘with arcpy.da.UpdateCursor(…) as cursor’

SQLAlchemy AttributeError: ‘engine’ Object Has No Attribute ‘cursor’

This error is a SQLAlchemy version compatibility issue. In SQLAlchemy 1.x, developers could call methods directly on the engine object. In SQLAlchemy 2.x (the current version), the API changed significantly and engine-level shortcuts were removed.

What Changed Between SQLAlchemy 1.x and 2.x

# SQLAlchemy 1.x (old, now broken in 2.x)

engine.execute(‘SELECT * FROM users’)  # Worked in 1.x

engine.cursor()                         # AttributeError in 2.x

# SQLAlchemy 2.x (correct)

with engine.connect() as conn:

    result = conn.execute(text(‘SELECT * FROM users’))

    rows = result.fetchall()

Note the text() wrapper for string SQL in SQLAlchemy 2.x — raw strings are not accepted directly by execute(). Import text from sqlalchemy: from sqlalchemy import text.

Accessing a Raw DBAPI Cursor in SQLAlchemy 2.x

If you specifically need a raw DBAPI cursor (for example, to call stored procedures that require cursor-level operations), use engine.raw_connection():

with engine.raw_connection() as conn:

    cursor = conn.cursor()

    cursor.execute(‘SELECT * FROM users’)

    rows = cursor.fetchall()

    conn.commit()  # If modifying data

raw_connection() bypasses SQLAlchemy’s ORM and connection pooling abstractions and gives you direct DBAPI access. Use it sparingly — most operations are better handled through the standard SQLAlchemy 2.x Connection API.

Fixing Legacy SQLAlchemy 1.x Code

If you’re maintaining code written for SQLAlchemy 1.x, the upgrade path involves three main changes:

  • Replace engine.execute() with engine.connect() + conn.execute()
  • Wrap string SQL in text(): conn.execute(text(‘SELECT …’))
  • Replace engine.cursor() with engine.raw_connection() then .cursor() on the resulting connection

Summary: SQL Cursor Error Quick Reference

Tool / ErrorRoot CauseQuick Fix
psycopg2 RETURNING not capturedNot calling fetchone() after execute()Add cursor.fetchone() immediately after execute()
psycopg2 string interpolationUsing f-string in SQL instead of %s paramsUse cursor.execute(sql, (param,)) syntax
Node ODBC error 24000Fetching past end of results or wrong API patternCheck for null on each fetch(); don’t call fetch() after query()
T-SQL cursor not workingMissing FETCH at loop start or wrong @@FETCH_STATUS checkFetch before the loop AND at the end of each iteration
arcpy changes not savedNot calling cursor.updateRow(row)Always call cursor.updateRow(row) after modifying row
arcpy feature class lockedCursor not released; missing del cursor or ‘with’ statementUse ‘with arcpy.da.UpdateCursor(…) as cursor:’ pattern
SQLAlchemy engine.cursor() AttributeErrorSQLAlchemy 2.x removed engine-level APIUse engine.connect() for normal queries; engine.raw_connection() for DBAPI cursor

Frequently Asked Questions

Why does psycopg2 cursor.execute() not return values?

cursor.execute() never directly returns query results in psycopg2. After executing a SELECT or a DML statement with RETURNING, call cursor.fetchone() (for one row), cursor.fetchall() (for all rows), or iterate the cursor directly. The results exist in the cursor object, not as a return value of execute().

What is T-SQL cursor @@FETCH_STATUS?

@@FETCH_STATUS is a system variable in T-SQL that indicates the status of the last FETCH operation: 0 means the fetch succeeded and a row was returned; -1 means the fetch failed or the row was beyond the result set; -2 means the row fetched was missing (for KEYSET cursors). Loop while @@FETCH_STATUS = 0.

How do I update features using arcpy UpdateCursor?

Open a cursor with arcpy.da.UpdateCursor(feature_class, field_list), iterate through rows, modify row values, and call cursor.updateRow(row) after each modification. Use the ‘with’ statement to ensure the cursor is released when done. Without cursor.updateRow(row), changes are not saved.

How do I fix ‘engine’ object has no attribute ‘cursor’ in SQLAlchemy?

This is a SQLAlchemy 2.x compatibility error. Use engine.connect() and conn.execute(text(sql)) for standard queries. For raw DBAPI cursor access, use engine.raw_connection() to get a connection, then call .cursor() on that object.

Can you have a cursor inside a cursor in SQL?

Yes — T-SQL supports nested cursors, provided each inner cursor uses a unique name and follows its complete lifecycle (DECLARE, OPEN, FETCH, CLOSE, DEALLOCATE) inside the outer cursor’s loop. Nested cursors have severe performance implications and should be replaced with set-based operations or temp tables where possible.

Final Thoughts

SQL cursor errors across different libraries share a common theme: cursor state management. Whether it’s psycopg2 requiring an explicit fetchone() call after a RETURNING clause, node-odbc’s 24000 error from fetching past the end of a result set, T-SQL’s strict OPEN/FETCH/CLOSE/DEALLOCATE lifecycle, or arcpy requiring updateRow() to commit changes, each tool has its own state machine for cursor operations. Understanding where in that state machine the error occurs — rather than searching for the error message in isolation — is the fastest path to a fix. The code examples above cover the most common patterns and their correct equivalents.

Leave a Comment

Your email address will not be published. Required fields are marked *