Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7705e72
feat: implement Oracle-compatible ROWNUM pseudocolumn
rophy Nov 29, 2025
56e7fcc
fix: ROWNUM reset for UNION/UNION ALL branches (Oracle compatibility)
rophy Dec 2, 2025
f66e6ce
test: add ROWNUM tests for INTERSECT, EXCEPT, LATERAL, DML, empty tables
rophy Dec 2, 2025
3561a21
refactor: improve code quality per PR review feedback
rophy Dec 8, 2025
a8171fa
refactor: fix replace_rownum_expr_mutator signature
rophy Dec 8, 2025
3a9f276
test: add ROWNUM pagination test for nested subqueries
rophy Jan 6, 2026
11a8ed6
fix: use local ROWNUM counter for nested subqueries
rophy Jan 6, 2026
2ecbfb7
refactor: use compatible_db instead of database_mode for ROWNUM
rophy Jan 6, 2026
5e4b64b
fix: validate ROWNUM in GROUP BY clause
rophy Jan 6, 2026
bb36772
fix: ROWNUM in aggregate functions returns correct values
rophy Jan 6, 2026
0455343
test: add nested aggregate function tests for ROWNUM
rophy Jan 7, 2026
91730a3
fix: ROWNUM aggregate with ORDER BY when column order differs
rophy Jan 7, 2026
d6d8e08
test: add scroll cursor ROWNUM bug regression test
rophy Jan 8, 2026
5c54046
fix: materialize scroll cursor results when ROWNUM present
rophy Jan 8, 2026
fc279e0
test: add deeply nested ROWNUM scroll cursor test
rophy Jan 8, 2026
ede7906
test: add ROWNUM keyword behavior regression tests
rophy Jan 8, 2026
42cb0d3
fix: reject ROWNUM/ROWID as column and table aliases
rophy Jan 8, 2026
61d2053
fix: allow ROWNUM/ROWID as PL/iSQL variable names
rophy Jan 8, 2026
7412485
test: add PostgreSQL mode test for Oracle keywords
rophy Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/backend/executor/execExpr.c
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,41 @@ ExecInitExprList(List *nodes, PlanState *parent)
* Caution: before PG v10, the targetList was a list of ExprStates; now it
* should be the planner-created targetlist, since we do the compilation here.
*/

/*
* Helper function to check if an expression contains ROWNUM
*/
static bool
expression_contains_rownum_walker(Node *node, void *context)
{
if (node == NULL)
return false;

if (IsA(node, RownumExpr))
return true;

return expression_tree_walker(node, expression_contains_rownum_walker, context);
}

/*
* Check if a target list contains ROWNUM expressions
*/
static bool
targetlist_contains_rownum(List *targetList)
{
ListCell *lc;

foreach(lc, targetList)
{
TargetEntry *tle = lfirst_node(TargetEntry, lc);

if (expression_contains_rownum_walker((Node *) tle->expr, NULL))
return true;
}

return false;
}

ProjectionInfo *
ExecBuildProjectionInfo(List *targetList,
ExprContext *econtext,
Expand Down Expand Up @@ -519,6 +554,13 @@ ExecBuildProjectionInfo(List *targetList,

ExecReadyExpr(state);

/*
* Check if the target list contains ROWNUM expressions.
* If so, we need to materialize the result tuple to preserve the
* ROWNUM values and prevent re-evaluation in outer queries.
*/
projInfo->pi_needsMaterialization = targetlist_contains_rownum(targetList);

return projInfo;
}

Expand Down Expand Up @@ -2646,6 +2688,14 @@ ExecInitExprRec(Expr *node, ExprState *state,
break;
}

case T_RownumExpr:
{
/* Oracle ROWNUM pseudocolumn */
scratch.opcode = EEOP_ROWNUM;
ExprEvalPushStep(state, &scratch);
break;
}

case T_ReturningExpr:
{
ReturningExpr *rexpr = (ReturningExpr *) node;
Expand Down
48 changes: 48 additions & 0 deletions src/backend/executor/execExprInterp.c
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
&&CASE_EEOP_SQLVALUEFUNCTION,
&&CASE_EEOP_CURRENTOFEXPR,
&&CASE_EEOP_NEXTVALUEEXPR,
&&CASE_EEOP_ROWNUM,
&&CASE_EEOP_RETURNINGEXPR,
&&CASE_EEOP_ARRAYEXPR,
&&CASE_EEOP_ARRAYCOERCE,
Expand Down Expand Up @@ -1593,6 +1594,18 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
EEO_NEXT();
}

EEO_CASE(EEOP_ROWNUM)
{
/*
* Oracle ROWNUM pseudocolumn: return the current row number.
* The row number is incremented by the executor for each row
* emitted.
*/
ExecEvalRownum(state, op);

EEO_NEXT();
}

EEO_CASE(EEOP_RETURNINGEXPR)
{
/*
Expand Down Expand Up @@ -3322,6 +3335,41 @@ ExecEvalNextValueExpr(ExprState *state, ExprEvalStep *op)
*op->resnull = false;
}

/*
* Evaluate Oracle ROWNUM pseudocolumn.
*
* Returns the current row number from the executor state. The row number
* is incremented for each row emitted during query execution.
*
* ROWNUM starts at 1 and increments before any ORDER BY is applied.
*/
void
ExecEvalRownum(ExprState *state, ExprEvalStep *op)
{
PlanState *planstate;
EState *estate = NULL;
int64 rownum_value = 1; /* default */

/* Safely get the PlanState and EState */
if (state && state->parent)
{
planstate = state->parent;
if (planstate)
estate = planstate->state;
}

/*
* Use the estate-level ROWNUM counter.
* When ROWNUM appears in a SELECT list, materialization (handled in
* ExecScanExtended) ensures the value is captured and not re-evaluated.
*/
if (estate && estate->es_rownum > 0)
rownum_value = estate->es_rownum;

*op->resvalue = Int64GetDatum(rownum_value);
*op->resnull = false;
}

/*
* Evaluate NullTest / IS NULL for rows.
*/
Expand Down
3 changes: 3 additions & 0 deletions src/backend/executor/execUtils.c
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ CreateExecutorState(void)
estate->es_parallel_workers_to_launch = 0;
estate->es_parallel_workers_launched = 0;

/* Oracle ROWNUM support: initialize row counter */
estate->es_rownum = 0;

estate->es_jit_flags = 0;
estate->es_jit = NULL;

Expand Down
10 changes: 10 additions & 0 deletions src/backend/executor/nodeAppend.c
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ ExecInitAppend(Append *node, EState *estate, int eflags)
/* For parallel query, this will be overridden later. */
appendstate->choose_next_subplan = choose_next_subplan_locally;

/* Copy is_union flag for ROWNUM reset handling (Oracle compatibility) */
appendstate->as_is_union = node->is_union;

return appendstate;
}

Expand Down Expand Up @@ -386,6 +389,13 @@ ExecAppend(PlanState *pstate)
/* choose new sync subplan; if no sync/async subplans, we're done */
if (!node->choose_next_subplan(node) && node->as_nasyncremain == 0)
return ExecClearTuple(node->ps.ps_ResultTupleSlot);

/*
* For UNION queries, reset ROWNUM when switching to a new branch.
* In Oracle, each UNION branch has its own independent ROWNUM counter.
*/
if (node->as_is_union)
node->ps.state->es_rownum = 0;
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/backend/executor/nodeMergeAppend.c
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ ExecInitMergeAppend(MergeAppend *node, EState *estate, int eflags)
*/
mergestate->ms_initialized = false;

/* Copy is_union flag for ROWNUM reset handling (Oracle compatibility) */
mergestate->ms_is_union = node->is_union;

return mergestate;
}

Expand Down Expand Up @@ -238,10 +241,22 @@ ExecMergeAppend(PlanState *pstate)
/*
* First time through: pull the first tuple from each valid subplan,
* and set up the heap.
*
* For UNION queries, reset ROWNUM before each subplan starts.
* This ensures each UNION branch has independent ROWNUM counting
* (Oracle compatibility).
*/
i = -1;
while ((i = bms_next_member(node->ms_valid_subplans, i)) >= 0)
{
/*
* For UNION, reset ROWNUM before each branch executes.
* Each child's Sort will buffer all tuples from its scan,
* so ROWNUM needs to start fresh for each branch.
*/
if (node->ms_is_union)
node->ps.state->es_rownum = 0;

node->ms_slots[i] = ExecProcNode(node->mergeplans[i]);
if (!TupIsNull(node->ms_slots[i]))
binaryheap_add_unordered(node->ms_heap, Int32GetDatum(i));
Expand Down
13 changes: 12 additions & 1 deletion src/backend/executor/nodeResult.c
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,18 @@ ExecResult(PlanState *pstate)
}

/* form the result tuple using ExecProject(), and return it */
return ExecProject(node->ps.ps_ProjInfo);
TupleTableSlot *result = ExecProject(node->ps.ps_ProjInfo);

/*
* If the projection contains ROWNUM expressions, materialize
* the virtual tuple to preserve the ROWNUM values as constants.
*/
if (node->ps.ps_ProjInfo && node->ps.ps_ProjInfo->pi_needsMaterialization)
{
ExecMaterializeSlot(result);
}

return result;
}

return NULL;
Expand Down
27 changes: 25 additions & 2 deletions src/backend/executor/nodeSubplan.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ ExecSubPlan(SubPlanState *node,
SubPlan *subplan = node->subplan;
EState *estate = node->planstate->state;
ScanDirection dir = estate->es_direction;
int64 save_rownum = estate->es_rownum;
Datum retval;

CHECK_FOR_INTERRUPTS();
Expand All @@ -82,14 +83,22 @@ ExecSubPlan(SubPlanState *node,
/* Force forward-scan mode for evaluation */
estate->es_direction = ForwardScanDirection;

/*
* Reset ROWNUM counter for Oracle compatibility.
* Each correlated subquery invocation should start with ROWNUM=0,
* matching Oracle's behavior.
*/
estate->es_rownum = 0;

/* Select appropriate evaluation strategy */
if (subplan->useHashTable)
retval = ExecHashSubPlan(node, econtext, isNull);
else
retval = ExecScanSubPlan(node, econtext, isNull);

/* restore scan direction */
/* restore scan direction and ROWNUM counter */
estate->es_direction = dir;
estate->es_rownum = save_rownum;

return retval;
}
Expand Down Expand Up @@ -262,6 +271,12 @@ ExecScanSubPlan(SubPlanState *node,
/* with that done, we can reset the subplan */
ExecReScan(planstate);

/*
* Reset ROWNUM counter for Oracle compatibility.
* This ensures correlated subqueries start fresh for each outer row.
*/
planstate->state->es_rownum = 0;

/*
* For all sublink types except EXPR_SUBLINK and ARRAY_SUBLINK, the result
* is boolean as are the results of the combining operators. We combine
Expand Down Expand Up @@ -1104,6 +1119,7 @@ ExecSetParamPlan(SubPlanState *node, ExprContext *econtext)
SubLinkType subLinkType = subplan->subLinkType;
EState *estate = planstate->state;
ScanDirection dir = estate->es_direction;
int64 save_rownum = estate->es_rownum;
MemoryContext oldcontext;
TupleTableSlot *slot;
ListCell *l;
Expand All @@ -1124,6 +1140,12 @@ ExecSetParamPlan(SubPlanState *node, ExprContext *econtext)
*/
estate->es_direction = ForwardScanDirection;

/*
* Reset ROWNUM counter for Oracle compatibility.
* InitPlans should start with ROWNUM=0, matching Oracle's behavior.
*/
estate->es_rownum = 0;

/* Initialize ArrayBuildStateAny in caller's context, if needed */
if (subLinkType == ARRAY_SUBLINK)
astate = initArrayResultAny(subplan->firstColType,
Expand Down Expand Up @@ -1257,8 +1279,9 @@ ExecSetParamPlan(SubPlanState *node, ExprContext *econtext)

MemoryContextSwitchTo(oldcontext);

/* restore scan direction */
/* restore scan direction and ROWNUM counter */
estate->es_direction = dir;
estate->es_rownum = save_rownum;
}

/*
Expand Down
21 changes: 21 additions & 0 deletions src/backend/executor/nodeSubqueryscan.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,26 @@ static TupleTableSlot *
SubqueryNext(SubqueryScanState *node)
{
TupleTableSlot *slot;
EState *estate = node->ss.ps.state;
int64 save_rownum = estate->es_rownum;

/*
* For Oracle ROWNUM compatibility: each subquery maintains its own
* local ROWNUM counter. Save the outer query's counter, swap in
* this subquery's counter, execute the subplan, then restore.
* This allows nested subqueries to have independent ROWNUM sequences.
*/
estate->es_rownum = node->sub_rownum;

/*
* Get the next tuple from the sub-query.
*/
slot = ExecProcNode(node->subplan);

/* Update local counter and restore outer query's counter */
node->sub_rownum = estate->es_rownum;
estate->es_rownum = save_rownum;

/*
* We just return the subplan's result slot, rather than expending extra
* cycles for ExecCopySlot(). (Our own ScanTupleSlot is used only for
Expand Down Expand Up @@ -112,6 +126,7 @@ ExecInitSubqueryScan(SubqueryScan *node, EState *estate, int eflags)
subquerystate->ss.ps.plan = (Plan *) node;
subquerystate->ss.ps.state = estate;
subquerystate->ss.ps.ExecProcNode = ExecSubqueryScan;
subquerystate->sub_rownum = 0;

/*
* Miscellaneous initialization
Expand Down Expand Up @@ -182,6 +197,12 @@ ExecEndSubqueryScan(SubqueryScanState *node)
void
ExecReScanSubqueryScan(SubqueryScanState *node)
{
/*
* Reset local ROWNUM counter for Oracle compatibility.
* Each rescan starts with ROWNUM = 1.
*/
node->sub_rownum = 0;

ExecScanReScan(&node->ss);

/*
Expand Down
Loading