Skip to content

Bug: ROWNUM counter not reset in correlated subqueries #14

Description

@rophy

Bug Report: ROWNUM Counter Not Reset in Correlated Subqueries

Summary

When ROWNUM is used in a correlated subquery, the es_rownum counter is not reset between invocations, causing incorrect values to be returned. Each correlated execution should start with ROWNUM=1, but instead the counter continues incrementing across all invocations.

Environment

  • IvorySQL Version: 5beta1 (PostgreSQL 18beta1)
  • Branch: origin/feat/rownum (commit c1ea1b5)
  • Compatibility Mode: Oracle

Bug Description

In Oracle, when a correlated subquery contains ROWNUM, the counter resets to 1 for each execution of the subquery. IvorySQL does not reset the counter, causing it to continue incrementing across multiple correlated invocations.

Steps to Reproduce

1. Create Test Data

CREATETABLEtest_data (
id INT,
name VARCHAR2(50),
value NUMBER
);
INSERT INTO test_data VALUES (1, 'Alice', 100);
INSERT INTO test_data VALUES (2, 'Bob', 200);
INSERT INTO test_data VALUES (3, 'Charlie', 150);
INSERT INTO test_data VALUES (4, 'David', 300);
INSERT INTO test_data VALUES (5, 'Eve', 250);

2. Run Correlated Subquery

SELECT id,
name,
(SELECT ROWNUM FROM (
SELECT*FROM test_data t2 WHEREt2.id=t1.idORDER BY value DESC
) sub) as correlated_rn
FROM test_data t1
ORDER BY id;

Expected Behavior (Oracle 23c)

 ID NAME CORRELATED_RN
--- --------- -------------
1 Alice 1
2 Bob 1
3 Charlie 1
4 David 1
5 Eve 1

Explanation: Each correlated execution sees ROWNUM starting from 1.

Actual Behavior (IvorySQL)

 id | name | correlated_rn ----|---------|---------------
1 | Alice | 3
2 | Bob | 6
3 | Charlie | 9
4 | David | 12
5 | Eve | 15

Explanation: The counter continues incrementing:

  • First execution: ROWNUM goes from 1→3 (3 rows scanned in subquery)
  • Second execution: ROWNUM continues from 3→6 (3 more rows)
  • Third execution: ROWNUM continues from 6→9 (3 more rows)
  • And so on...

Root Cause Analysis

Current Implementation

The ROWNUM counter (es_rownum) is stored in EState and incremented during scan operations:

// src/include/nodes/execnodes.htypedefstructEState {
...
int64es_rownum; /* current ROWNUM value */
...
} EState;
// src/include/executor/execScan.h (line ~225)if (node->ps.state->es_rownum >= 0)
node->ps.state->es_rownum++;

The Problem

When a correlated subquery executes:

  1. The same EState is shared between outer query and subquery
  2. The subquery's scans increment es_rownum
  3. No reset occurs when the subquery completes
  4. Next correlated invocation sees the incremented value

Why This Happens

Correlated subqueries are executed via ExecSubPlan() in src/backend/executor/nodeSubplan.c, but there is no save/restore mechanism for es_rownum around the subquery execution.

Proposed Solution

Option 1: Save/Restore Around Correlated Execution (Recommended)

Modify nodeSubplan.c to save and restore es_rownum:

// src/backend/executor/nodeSubplan.cDatumExecSubPlan(SubPlanState*node, ExprContext*econtext, bool*isNull)
{
EState*estate=node->planstate->state;
int64saved_rownum;
Datumresult;
/* Save current ROWNUM counter */saved_rownum=estate->es_rownum;
/* Reset counter for correlated subquery execution */estate->es_rownum=0;
/* Execute the subquery */result=/* ... existing subquery execution logic ... */;
/* Restore ROWNUM counter */estate->es_rownum=saved_rownum;
returnresult;
}

Option 2: Per-Query Context Counter

Create a separate ROWNUM context for each query level, similar to how Oracle manages its pseudo-column state. This would require more extensive changes to the executor.

Recommended Approach

Option 1 is simpler and aligns with Oracle's behavior. The save/restore pattern is already used in PostgreSQL for other execution state (e.g., snapshot handling).

Test Case

Add to src/oracle_test/regress/sql/rownum.sql:

-- Test ROWNUM in correlated subquery (should reset for each execution)SELECT id,
name,
(SELECT ROWNUM FROM (
SELECT*FROM test_data t2 WHEREt2.id=t1.idORDER BY value DESC
) sub) as correlated_rn
FROM test_data t1
ORDER BY id;
-- Expected: All correlated_rn values should be 1

Expected output:

 id | name | correlated_rn ----|---------|---------------
1 | Alice | 1
2 | Bob | 1
3 | Charlie | 1
4 | David | 1
5 | Eve | 1

Impact

Severity: HIGH

This bug affects any query using ROWNUM in correlated subqueries, which is a common Oracle pattern for:

  • Row numbering within groups
  • Finding the Nth item per category
  • Pagination within correlated contexts

Workaround

None available. The correlated subquery pattern cannot be reliably used with ROWNUM in current IvorySQL.

Additional Context

Related Code Locations

  1. Counter Management:

    • src/include/nodes/execnodes.h - EState definition
    • src/backend/executor/execUtils.c - EState initialization
    • src/include/executor/execScan.h - Counter increment
  2. Subquery Execution:

    • src/backend/executor/nodeSubplan.c - Subplan execution
    • ExecInitSubPlan() - Subplan initialization
    • ExecSubPlan() - Subplan execution (WHERE FIX NEEDED)
  3. Related Functions:

    • ExecReScanSubPlan() - Subplan rescanning
    • ExecEndSubPlan() - Subplan cleanup

Oracle Documentation Reference

From Oracle Database SQL Language Reference:

"For each row returned by a query, the ROWNUM pseudocolumn returns a number indicating the order in which Oracle selects the row from a table or set of joined rows. The first row selected has a ROWNUM of 1, the second has 2, and so on."

Key point: "for each query" - implying reset behavior for subqueries.

Verification in Oracle

Tested in Oracle Database 23c Free (container-registry.oracle.com/database/free:23.26.0.0-lite):

SQL>SELECT id, (SELECT ROWNUM FROM (SELECT*FROM test WHERE id =t.id)) as rn
FROM test t;
ID RN
---------- ----------112131

Confirms Oracle resets ROWNUM for each correlated execution.

Related Issues

  • This bug is independent of the ROWNUM+ORDER BY bug (fixed in PR #XXX)
  • The ROWNUM+ORDER BY fix operates at planner level
  • This bug requires executor-level changes

Proposed Patch

A minimal patch would modify ExecSubPlan() in src/backend/executor/nodeSubplan.c:

@@ -XXX,YY +XXX,ZZ @@ ExecSubPlan(SubPlanState *node, ...)
{
PlanState *planstate = node->planstate;
SubLinkType subLinkType = node->subLinkType;
+ EState *estate = planstate->state;+ int64 saved_rownum = -1;
...
+ /* Save and reset ROWNUM for correlated subquery */+ if (estate && estate->es_rownum >= 0)+ {+ saved_rownum = estate->es_rownum;+ estate->es_rownum = 0;+ }+
/* Execute subquery */
...
+ /* Restore ROWNUM counter */+ if (saved_rownum >= 0)+ estate->es_rownum = saved_rownum;+
return result;
}

Priority

HIGH - This is a critical Oracle compatibility issue affecting a commonly used pattern.

Labels

  • bug
  • oracle-compatibility
  • rownum
  • executor
  • correlated-subquery

Reported by: Testing during ROWNUM ORDER BY bug fix implementation
Date: 2025-11-29
IvorySQL Version: 5beta1 (commit c1ea1b5)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions