-
Notifications
You must be signed in to change notification settings - Fork 163
feat: ROWNUM Implementation #1000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughAdds Oracle ROWNUM end-to-end: new Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Parser
participant Planner
participant Executor
participant EState as EState(es_rownum)
Client->>Parser: submit SQL (uses ROWNUM)
Parser->>Parser: emit RownumExpr in parse tree
Parser->>Planner: pass query tree
Planner->>Planner: transform ROWNUM (LIMIT or projection rewrites)
Planner->>Executor: produce plan (may mark is_union / add ROWNUM Vars)
Executor->>EState: initialize es_rownum = 0
loop per input tuple
Executor->>EState: es_rownum++ (pre-qual)
Executor->>Executor: evaluate qual
alt qual passes
Executor->>Executor: ExecProject -> EEOP_ROWNUM executed
Executor->>Executor: if pi_needsMaterialization: ExecMaterializeSlot
Executor->>Client: return tuple
else qual fails
Executor->>EState: es_rownum-- (revert)
end
end
sequenceDiagram
autonumber
participant OuterExec
participant Subplan
participant EState as EState(es_rownum)
OuterExec->>EState: save outer_rownum
OuterExec->>EState: reset es_rownum = 0
OuterExec->>Subplan: execute subplan
loop subplan tuples
Subplan->>EState: es_rownum++ / evaluate / emit
end
OuterExec->>EState: restore outer_rownum
sequenceDiagram
autonumber
participant AppendExec
participant BranchA
participant BranchB
participant EState as EState(es_rownum)
AppendExec->>AppendExec: detect is_union
AppendExec->>EState: reset es_rownum = 0
AppendExec->>BranchA: scan branch A
loop A tuples
BranchA->>EState: es_rownum++ / emit
end
AppendExec->>EState: reset es_rownum = 0
AppendExec->>BranchB: scan branch B
loop B tuples
BranchB->>EState: es_rownum++ / emit
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (10)
src/backend/optimizer/util/clauses.c (1)
464-468: ROWNUM volatility classification looks right, but COPY helper ignores itMarking
RownumExpras non-immutable incontain_mutable_functions_walkerand volatile incontain_volatile_functions_walkeris consistent with its per-row behavior. However,contain_volatile_functions_not_nextval_walker()does not recognizeRownumExpr, so expressions that are “volatile” only because of ROWNUM will be treated as non‑volatile in COPY’s special check. If you want COPY’s volatility checks to see ROWNUM as volatile (which seems safest), consider adding a parallelIsA(node, RownumExpr)branch there as well.Also applies to: 595-599, 715-751
src/backend/optimizer/util/pathnode.c (1)
1576-1576: Good initialization; minor whitespace inconsistency.The default initialization is correct and mirrors the pattern in
create_append_path. However, line 1363 uses a tab before the comment while this line uses spaces. Consider standardizing to match the project's style.src/backend/utils/adt/ruleutils.c (1)
10841-10844: DeparsingT_RownumExprasROWNUMis correct and minimalThe dedicated
T_RownumExprbranch that emits the literalROWNUMcleanly supports Oracle-style pseudocolumn deparsing and avoids colliding with user columns (which still go through theVarpath). Behavior is straightforward and looks correct. If you ever see this node outside Oracle-compatible mode, an assertion or mode check here could help catch misuse, but it's not strictly necessary.src/backend/nodes/nodeFuncs.c (1)
1423-1856: Optional: addT_RownumExprhandling inexprLocationfor better diagnosticsRight now
exprLocationfalls through to the default case forT_RownumExpr, returning-1. IfRownumExprcarries alocationfield inprimnodes.h, adding a dedicated case (similar toT_NextValueExpr/others that just expose theirlocation) would give more precise error locations when ROWNUM appears in reported expressions.src/backend/executor/nodeAppend.c (1)
293-295: Per-branch ROWNUM reset for UNION viaas_is_unionlooks good; watch async-only UNION cases
- Copying
node->is_unionintoappendstate->as_is_unioninExecInitAppendcleanly separates UNION vs non‑UNION Appends without impacting inheritance/appendrel use cases.- In
ExecAppend, resettinges_rownumto0immediately afterchoose_next_subplanadvances to a new subplan gives each UNION branch its own ROWNUM sequence in the synchronous case, matching the stated Oracle‑style semantics.One thing to be aware of: the reset hook is tied to
choose_next_subplan, i.e., synchronous branch changes. If an Append used as a UNION aggregator ever ends up with only async‑capable children (or yields most rows via the async path),es_rownumwill not be reset between those async branches. If such plans are possible with ROWNUM, you may want to either (a) disable async Append for UNION queries involving ROWNUM, or (b) add equivalent per‑branch reset logic on the async path.Also applies to: 393-399
src/backend/optimizer/path/allpaths.c (1)
1337-1362: UNION detection and is_union propagation are partial; parameterized/ordered appends remain untaggedThe
is_union_allheuristic:is_union_all = true; foreach (childrel in live_childrels) if (childrel->rtekind != RTE_SUBQUERY) is_union_all = false;is a reasonable way to distinguish UNION/UNION ALL append relations from inheritance/partitioning (which use
RTE_RELATIONchildren).You then set
appendpath->is_union = is_union_allfor:
- The main unparameterized Append path,
- The startup-optimized Append path,
- The unordered partial Append path, and
- The parallel‑aware mixed partial/non‑partial Append path.
Two potential gaps:
- Parameterized AppendPaths built later in the function:
create_append_path(root, rel, subpaths, NIL, NIL, required_outer, 0, false, -1);are not tagged with
is_union, so a UNION appendrel that ends up parameterized would not trigger per‑branch ROWNUM reset in the executor.
- Ordered Append paths created in
generate_orderedappend_paths(for appendrels that can use Append instead of MergeAppend) are also not tagged, even when they originate from a UNION appendrel. If such a path is chosen for a UNION in Oracle mode, its executor state would seeas_is_union == false.If UNION/UNION ALL appendrels can never reach these code paths in practice, it’d be helpful to document that invariant. Otherwise, consider propagating
is_union_allto allcreate_append_pathcalls associated with the same appendrel, including parameterized and ordered variants, to avoid subtle mode‑dependent differences in ROWNUM behavior.Also applies to: 1547-1555, 1559-1568, 1607-1614, 1639-1665
src/backend/executor/execExprInterp.c (2)
3354-3359: Simplify redundant null check.The check
if (planstate)on line 3357 is redundant sinceplanstatewas just assigned fromstate->parent, which was already verified non-NULL by the enclosingif (state && state->parent)condition./* Safely get the PlanState and EState */ if (state && state->parent) { planstate = state->parent; - if (planstate) - estate = planstate->state; + estate = planstate->state; }
3346-3371: Consider adding an assertion for defensive coding.The function correctly handles the case where
estateis NULL by defaulting to 1. However, since ROWNUM evaluation requires proper executor state setup, consider adding a debug assertion to catch unexpected scenarios wherestate->parentis NULL during normal query execution.void ExecEvalRownum(ExprState *state, ExprEvalStep *op) { PlanState *planstate; EState *estate = NULL; int64 rownum_value = 1; /* default */ + Assert(state != NULL); + /* Safely get the PlanState and EState */ if (state && state->parent) {This makes the expectation explicit while preserving the graceful fallback for edge cases.
src/backend/optimizer/plan/createplan.c (2)
2084-2228: ROWNUM-before-Sort rewrite is sound; a few refinements suggestedThe transformation that pushes
RownumExprevaluation belowSort/IncrementalSortand replaces them withVars in the projection tlist correctly achieves “ROWNUM assigned before ORDER BY” semantics and handles multiple/nested ROWNUM occurrences. A couple of points to tighten:
- The synthetic ROWNUM columns added to the sort input tlist and the sort’s own tlist are created with
resjunk = false. Since these are purely internal carriers, consider marking themresjunk = truein both places to avoid them ever being treated as user-visible or accidentally included in any “physical tlist” decisions.rownum_typeis hard-coded asINT8OID. If the declared SQL type ofROWNUMin IvorySQL ever differs, this will silently produce Vars of a different type than the originalRownumExpr. It would be safer to use the same type as the expression (e.g., via a dedicatedROWNUMtype constant orexprTypeon a representativeRownumExpr) so planner and executor stay consistent.count_rownum_exprsis called once perTargetEntrytwice (once when building the scan-input tlist, once when buildingrownum_vars), andreplace_rownum_expr_mutatoralso walks the same trees. Not a correctness issue, but you could cache per-TLE counts or store the total length in the context (instead of recomputinglist_length(context->rownum_vars)inside the mutator) if you want to avoid redundant tree walks; this is mostly micro-optimization.Overall, the structure and ordering look correct; these suggestions are mainly for clarity and robustness.
7575-7683: ROWNUM helpers integrate cleanly; align walker signature and clarify intentThe new helpers correctly reuse
expression_tree_walker/mutatorto (a) detect ROWNUM, (b) count occurrences, and (c) substitute them withVars:
- For
count_rownum_exprs_walker, the second parameter is currentlyint *countbut it’s passed as the callback toexpression_tree_walker, whose convention is avoid *context. It would be cleaner (and more in line with existing walkers in this file) to declare it asstatic bool count_rownum_exprs_walker(Node *node, void *context)and castcontexttoint *inside. That avoids any prototype/ABI mismatches under stricter compilers.contain_rownum_expr_walkerandcollect_rownum_exprsare straightforward and match how other contain-XYZ and collect-XYZ helpers are written; no functional issues there.These are small API-alignment improvements; the current code should still work, but tightening the signature will make it more future-proof.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
src/oracle_test/regress/expected/rownum.outis excluded by!**/*.outsrc/pl/plisql/src/expected/plisql_rownum.outis excluded by!**/*.out
📒 Files selected for processing (29)
src/backend/executor/execExpr.c(3 hunks)src/backend/executor/execExprInterp.c(3 hunks)src/backend/executor/execUtils.c(1 hunks)src/backend/executor/nodeAppend.c(2 hunks)src/backend/executor/nodeMergeAppend.c(2 hunks)src/backend/executor/nodeResult.c(1 hunks)src/backend/executor/nodeSubplan.c(6 hunks)src/backend/executor/nodeSubqueryscan.c(3 hunks)src/backend/nodes/nodeFuncs.c(5 hunks)src/backend/optimizer/path/allpaths.c(4 hunks)src/backend/optimizer/plan/createplan.c(5 hunks)src/backend/optimizer/plan/planner.c(4 hunks)src/backend/optimizer/prep/prepunion.c(3 hunks)src/backend/optimizer/util/clauses.c(2 hunks)src/backend/optimizer/util/pathnode.c(2 hunks)src/backend/oracle_parser/ora_gram.y(3 hunks)src/backend/parser/parse_expr.c(1 hunks)src/backend/utils/adt/ruleutils.c(2 hunks)src/include/executor/execExpr.h(2 hunks)src/include/executor/execScan.h(3 hunks)src/include/nodes/execnodes.h(5 hunks)src/include/nodes/pathnodes.h(2 hunks)src/include/nodes/plannodes.h(2 hunks)src/include/nodes/primnodes.h(1 hunks)src/include/oracle_parser/ora_kwlist.h(1 hunks)src/oracle_test/regress/parallel_schedule(1 hunks)src/oracle_test/regress/sql/rownum.sql(1 hunks)src/pl/plisql/src/Makefile(1 hunks)src/pl/plisql/src/sql/plisql_rownum.sql(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-02T10:43:25.257Z
Learnt from: syasirs
Repo: IvorySQL/IvorySQL PR: 858
File: src/Makefile.global.in:513-514
Timestamp: 2025-09-02T10:43:25.257Z
Learning: In IvorySQL Makefile.global.in, $(wildcard t/oracle/*.pl) is used instead of t/oracle/*.pl in Oracle TAP test discovery because the t/oracle directory is optional and may not exist in most builds. The wildcard function safely returns empty string when the directory is missing, preventing shell expansion errors.
Applied to files:
src/pl/plisql/src/Makefile
🧬 Code graph analysis (7)
src/include/executor/execExpr.h (1)
src/backend/executor/execExprInterp.c (2)
EEOP_ROWNUM(1597-1607)ExecEvalRownum(3346-3371)
src/backend/executor/nodeResult.c (2)
src/include/executor/executor.h (1)
ExecProject(479-503)src/include/executor/tuptable.h (1)
ExecMaterializeSlot(475-479)
src/backend/optimizer/path/allpaths.c (1)
src/backend/optimizer/util/pathnode.c (1)
create_append_path(1299-1420)
src/backend/executor/nodeSubqueryscan.c (1)
src/include/executor/executor.h (1)
ExecProcNode(310-317)
src/backend/utils/adt/ruleutils.c (1)
src/common/stringinfo.c (1)
appendStringInfoString(229-233)
src/backend/optimizer/plan/createplan.c (2)
src/include/nodes/pg_list.h (2)
list_length(151-155)list_nth(298-303)src/backend/nodes/makefuncs.c (2)
makeTargetEntry(289-313)makeVar(66-100)
src/backend/executor/execExpr.c (1)
src/backend/executor/execExprInterp.c (1)
EEOP_ROWNUM(1597-1607)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: meson_build (ubuntu-latest)
- GitHub Check: oracle_regression (ubuntu-latest)
- GitHub Check: pg_regression (ubuntu-latest)
- GitHub Check: build (ubuntu-latest)
- GitHub Check: oracle_pg_regression (ubuntu-latest)
- GitHub Check: contrib_regression
🔇 Additional comments (41)
src/pl/plisql/src/Makefile (1)
57-61: Addingplisql_rownumto REGRESS suite looks consistentThe new
plisql_rownumtest is wired into the existing PL/iSQL regression list right afterplisql_exception, which keeps naming and grouping consistent. No issues from the Makefile side.src/oracle_test/regress/sql/rownum.sql (1)
1-659: ROWNUM regression coverage is broad and well‑structuredThis script exercises ROWNUM across the key semantic areas you’re modifying (filters, ORDER BY, aggregates, joins, set ops, correlated subqueries, DML, and known INTERSECT/EXCEPT/LATERAL limitations), with clean setup/teardown of helper tables. From the test‑harness perspective the additions look consistent and complete.
src/oracle_test/regress/parallel_schedule (1)
162-167: Schedulingrownumas its own test group is reasonablePlacing the new
rownumregression after existing Oracle‑compat tests and running it alone is a safe, simple choice; it avoids any subtle interference with earlier tests and matches how other Oracle‑specific suites are scheduled.src/backend/executor/execUtils.c (1)
165-167: es_rownum initialization matches ExecEvalRownum expectationsInitializing
estate->es_rownumto 0 at creation is consistent withExecEvalRownum’s “default to 1 unless the executor has started counting” logic and avoids any dependence on uninitialized state. No issues here.src/include/executor/execExpr.h (1)
195-205: EEOP_ROWNUM opcode and ExecEvalRownum declaration are consistentThe new
EEOP_ROWNUMopcode is added in the same “special‑purpose expression types” block asEEOP_NEXTVALUEEXPR, and theExecEvalRownum(ExprState *, ExprEvalStep *)prototype matches the interpreter case that calls it without anExprContext. From the header’s perspective, the wiring looks correct and doesn’t affectExprEvalSteplayout.Also applies to: 888-889
src/backend/optimizer/util/clauses.c (1)
865-903: ROWNUM should probably be treated as parallel‑unsafe (or at least restricted)
max_parallel_hazard_walker()currently special‑casesNextValueExprasPROPARALLEL_UNSAFE, but does not mentionRownumExpr. IfRownumExpris implemented to access session state (such as a global counter fromEStatelikeExecEvalRownumdoes), allowing the planner to treat ROWNUM expressions as parallel‑safe could yield incorrect per‑query row numbering in parallel plans. I'd recommend classifyingRownumExprhere as at leastPROPARALLEL_RESTRICTED, and probablyPROPARALLEL_UNSAFE, to prevent parallelization of queries that reference ROWNUM until parallel semantics are explicitly implemented.src/backend/optimizer/util/pathnode.c (1)
1363-1363: LGTM: Proper default initialization for UNION path handling.The default
falseinitialization is appropriate, and the comment clearly documents that callers will override this when building UNION paths for ROWNUM reset logic.src/include/oracle_parser/ora_kwlist.h (1)
461-461: LGTM: Correct keyword addition for ROWNUM support.The keyword entry is properly formatted and alphabetically positioned between "rowid" and "rows". The categorization as
UNRESERVED_KEYWORDwithBARE_LABELis appropriate and consistent with similar keywords in the list.src/backend/parser/parse_expr.c (2)
689-703: ROWNUM ColumnRef detection and hook interactionRecognizing unqualified
rownumonly inDB_ORACLEmode and only as a bare, case‑insensitive identifier is consistent with Oracle semantics and placed correctly afterp_pre_columnref_hook. Note that this path returns aRownumExprdirectly and therefore never invokesp_post_columnref_hook; if any extension expects to see or overrideROWNUMvia the post‑hook, we’d need to setnodeand fall through instead of an earlyreturn.
976-1006: Condition for raising “missing column” errors now depends on parser modeThe new guard
if ((compatible_db == ORA_PARSER && !missing_ok) || compatible_db == PG_PARSER)preserves the previous
missing_okbehavior for the Oracle parser while ensuring PG parser mode still always reports undefined columns. That looks sound giventransformColumnRefOrFunCallis Oracle‑only, but it’s worth double‑checking that there is no PG‑parser caller that intentionally passesmissing_ok = trueand relies on suppressing these errors.src/include/nodes/pathnodes.h (1)
2070-2071: Initialize and propagateis_unionconsistently for Append/MergeAppend pathsAdding
bool is_uniontoAppendPathandMergeAppendPathis a reasonable way to flag UNION/UNION ALL branches for executor behavior (e.g., ROWNUM resets). Please ensure every constructor/call site that builds these paths explicitly setsis_union(or clearly relies onmakeNode()zero‑initialization) and that any path copying/cloning logic propagates it correctly; otherwise UNION and non‑UNION appends could be misclassified.Also applies to: 2093-2094
src/include/nodes/plannodes.h (2)
362-367:Append.is_unionflag is a reasonable hook for UNION‑scoped ROWNUM handlingAppending a dedicated
is_unionflag at the end ofAppendis structurally safe and the comment clearly documents its ROWNUM/UNION purpose; assuming the planner sets this only for UNION/UNION ALL append paths, this looks correct and non‑intrusive.
407-412:MergeAppend.is_unionmirrorsAppendsemantics appropriatelyAdding
is_uniontoMergeAppendwith matching semantics (per‑branch ROWNUM reset for UNION/UNION ALL) is consistent with the Append change and gives the executor the information it needs without impacting other MergeAppend uses.src/backend/executor/nodeResult.c (1)
135-146: ROWNUM‑aware materialization inExecResultis consistent with executor patternsCapturing the projection into
resultand materializing only whenpi_needsMaterializationis set follows existing slot semantics and ensures ROWNUM (and similar expressions) become stable values without penalizing other projections; the change is localized and looks correct.src/include/nodes/primnodes.h (1)
2150-2163:RownumExprnode definition is minimal and appropriateDefining a dedicated
RownumExpras anExprwrapper with just a location field cleanly models the pseudocolumn; it keeps all behavioral logic in planner/executor while giving the parser and expression machinery a distinct node tag to key off of.src/backend/utils/adt/ruleutils.c (1)
9332-9351: ROWNUM treated correctly as a simple expression inisSimpleNodeAdding
T_RownumExprto the simple/function-like node set makes ROWNUM participate cleanly in pretty-print parenthesis decisions and avoids unnecessary wrapping. This is consistent with other scalar, argument-less nodes and looks correct.src/backend/oracle_parser/ora_gram.y (3)
789-793: ROWNUM added as ordinary keyword token is consistentAdding
ROWNUMalongsideROWIDin the main%token <keyword>list is consistent with other Oracle‑style pseudocolumns and makes it available to the keyword category nonterminals below. No issues here; it matches the PR’s intent to recognizeROWNUMat parse time.
20388-20392: ROWNUM classified as unreserved keyword allows identifier usagePlacing
ROWNUMinunreserved_keywordmeans it:
- Can be used wherever
ColIdorNonReservedWordis allowed (e.g., in expressions, qualified names).- Remains non‑reserved, so it can technically be used as a user‑defined identifier (table/column/role, etc.).
This is appropriate if IvorySQL intends
ROWNUMto behave like other Oracle pseudocolumns that are recognized semantically (e.g., via ColumnRef transformation) rather than reserved in the grammar. If exact Oracle behavior is desired (i.e., disallowing user columns namedROWNUM), that restriction will need to be enforced during DDL analysis, not here.Please confirm that allowing
ROWNUMas a user‑defined identifier is acceptable for your Oracle‑compatibility goals; if not, you’ll want an explicit check in DDL validation to rejectROWNUMas a column/table name while still treating it as an unreserved keyword in queries.
21106-21111: Allowing ROWNUM as BareColLabel matches target‑list usageAdding
ROWNUMtobare_label_keywordallows constructs like:
SELECT expr ROWNUM(alias namedROWNUM)SELECT 1 ROWNUM FROM twhile
SELECT ROWNUM FROM tstill parses as a column reference, not as an alias, because thea_expr BareColLabelproduction requires two tokens.This is consistent with how other pseudocolumn‑adjacent names (e.g.,
ROWID) are treated and should not interfere with theROWNUMpseudocolumn semantics implemented in parse analysis/executor. No grammar‑level issues spotted.src/backend/nodes/nodeFuncs.c (1)
294-297: RownumExpr integration into core node helpers looks consistentThe new
T_RownumExprhandling inexprType,exprCollation,exprSetCollation,expression_tree_walker_impl, andexpression_tree_mutator_implis aligned with how other primitive, non‑collatable scalar nodes are treated. No functional issues spotted here.Also applies to: 1079-1082, 1340-1343, 2179-2180, 3061-3062
src/backend/optimizer/prep/prepunion.c (1)
814-818: Correctly tagging UNION Append/MergeAppend paths withis_unionSetting
is_union = trueon the Append and MergeAppend paths constructed specifically for UNION/UNION ALL ensures only true set-operation Append/MergeAppend plans participate in the per-branch ROWNUM reset logic, while appendrel/inheritance Appends stay untouched. This matches the intended Oracle‑style UNION semantics and keeps the flag well-scoped.Also applies to: 861-867, 970-977
src/include/nodes/execnodes.h (2)
376-385: ProjectionInfo and EState extensions look structurally soundAppending
pi_needsMaterializationandes_rownumat the end of their structs is ABI-safe within the backend, and both will default to 0 via palloc0 when older callers don’t initialize them. The semantics (projection-level “needs materialization” flag and executor‑wide ROWNUM counter) are coherent with the usage inexecScan.hand executor code paths.Also applies to: 752-758
1495-1522: New UNION/ROWNUM tracking flags are consistent with executor state designThe additions of
as_is_uniontoAppendState,ms_is_uniontoMergeAppendState, andrownum_resettoSubqueryScanStatefit naturally into the existing state structures and match how they’re consumed innodeAppend.c,nodeMergeAppend.c, andnodeSubqueryscan.c. With palloc0-based allocation and explicit initialization in the init functions, there’s no obvious structural or lifecycle issue from this header.Also applies to: 1539-1553, 1947-1960
src/backend/executor/nodeMergeAppend.c (1)
200-208: ms_is_union wiring and initial per-branch ROWNUM reset are reasonableCopying
node->is_unionintomergestate->ms_is_unionand, on the first pass, resettinges_rownumto 0 before seeding each valid subplan ensures that UNION‑style MergeAppend branches start from a clean ROWNUM counter even if their startup (e.g., Sort) already consumed input and bumped the counter. Subsequent calls reuse the heap without additional resets, so per-branch sequences remain monotone without extra overhead. This is consistent as long as only true UNION/UNION ALL MergeAppends are tagged withis_union.Also applies to: 241-263
src/include/executor/execScan.h (1)
183-285: ROWNUM accounting and projection materialization in ExecScanExtended are locally consistentWithin
ExecScanExtended:
- Incrementing
es_rownumonce per fetched tuple beforeExecQualand rolling it back on qual failure ensures only qualifying rows consume ROWNUM values, while expressions evaluated in the qual and projection see the correct, pre‑filter value.- The fast path when
!qual && !projInfoleaveses_rownumuntouched, avoiding overhead for scans that don’t involve predicates or projections.- On the projection path, materializing the result slot only when
projInfo->pi_needsMaterializationis set allows you to freeze ROWNUM (and other volatile expressions) into a physical tuple in the relatively rare cases where later re-reading the slot in a different context (e.g., subquery ORDER BY) would otherwise re‑expose volatility.Given the planner’s responsibility to normalize simple
ROWNUM <= Nfilters into LIMITs, this scan-level counter management looks coherent and doesn’t introduce obvious regressions for non‑ROWNUM queries.src/backend/executor/nodeSubqueryscan.c (1)
46-52: ROWNUM reset in SubqueryNext can miscount for non-blocking subplansThe current logic marks
first_callviarownum_reset, setsrownum_reset = truebefore the firstExecProcNode(node->subplan), and resetsnode->ss.ps.state->es_rownum = 0once whenfirst_callis true. This correctly cancels any es_rownum bumps during startup of a blocking subplan (e.g., Sort that drains its input on first call) so the outer SubqueryScan ROWNUM starts from 1.However, if later calls invoke
ExecProcNode(node->subplan)for a non-blocking (streaming) subplan that callsExecScanExtendedinternally on each fetch,es_rownumwill increment on each inner fetch. The SubqueryScan's ownExecScanExtendedthen pre-increments again for the same output row, yielding double-counted outer ROWNUM values (e.g., 1,3,5,… instead of 1,2,3,…).This is a correctness issue only if the planner permits streaming subplans under SubqueryScan in ROWNUM contexts. If all such subplans are guaranteed to be fully blocking (drain on first call, then buffer), the current approach is safe. Verify whether that invariant holds; if not, consider:
- Snapshot and restore
es_rownumaround each subplan call:int64 saved_rownum = node->ss.ps.state->es_rownum; slot = ExecProcNode(node->subplan); /* ... */ node->ss.ps.state->es_rownum = saved_rownum;to isolate inner increments, or
- Document and enforce (with assertions) that only fully-blocking subplans appear under SubqueryScan in Oracle-mode ROWNUM paths.
Also applies to: 57-72, 133-134, 201-209
src/backend/executor/execExpr.c (3)
380-414: LGTM: Helper functions for ROWNUM detection are well-implemented.The walker function correctly uses
expression_tree_walkerfor recursion and short-circuits on findingRownumExpr. Thetargetlist_contains_rownumfunction properly iterates through target entries usinglfirst_nodefor type-safe access.
557-562: Materialization flag correctly set for ROWNUM expressions.Setting
pi_needsMaterializationensures ROWNUM values are preserved during projection, preventing re-evaluation that would yield incorrect values in outer queries. The placement afterExecReadyExpris correct since the flag is independent of expression compilation.
2691-2697: LGTM: T_RownumExpr case follows standard pattern.The implementation correctly emits the
EEOP_ROWNUMstep with no additional data required, consistent with similar simple expression types likeT_CurrentOfExpr.src/backend/executor/nodeSubplan.c (4)
69-101: LGTM: Proper save/restore pattern for ROWNUM state in ExecSubPlan.The
save_rownumcorrectly preserves the caller's ROWNUM state, and the reset to 0 before subplan execution ensures each correlated subquery invocation starts fresh, matching Oracle semantics. The restoration at the end maintains correct ROWNUM values for the outer query.
1122-1148: LGTM: Proper save/restore pattern for ROWNUM state in ExecSetParamPlan.The InitPlan execution correctly saves and restores
es_rownum, ensuring that lazy evaluation of initplan parameters doesn't interfere with the outer query's ROWNUM state. The reset to 0 before execution matches the expected Oracle behavior.
1282-1284: Correct restoration of both scan direction and ROWNUM counter.The comment update from "restore scan direction" to "restore scan direction and ROWNUM counter" accurately reflects the additional restoration logic added for
es_rownum.
274-278: Verify redundancy claim by examining control flow and ExecReScan implementation.The comment claims the reset at lines 274-278 is redundant because
es_rownumis already reset at line 91 beforeExecScanSubPlanis called. This claim requires verification of:
- Whether
ExecReScan(called before line 274-278) modifieses_rownum- Whether there is control flow that could skip the line 91 reset but reach lines 274-278
- The purpose of the second reset in relation to the subplan execution lifecycle
If the reset is confirmed redundant, remove it. If intentional, add a clarifying comment explaining why it's necessary after
ExecReScan.src/backend/executor/execExprInterp.c (2)
555-555: LGTM: Dispatch table entry correctly added.The
EEOP_ROWNUMopcode is properly placed in the dispatch table. TheStaticAssertDeclat line 613 ensures the table stays synchronized with theExprEvalOpenum.
1597-1607: LGTM: Case handler follows established patterns.The
EEOP_ROWNUMcase correctly delegates to the out-of-lineExecEvalRownum()function and advances withEEO_NEXT(), consistent with similar opcodes likeEEOP_NEXTVALUEEXPR.src/backend/optimizer/plan/planner.c (3)
62-65: New Oracle-mode headers are consistent with usageIncluding
utils/guc.handutils/ora_compatible.hhere is consistent with the newdatabase_mode/DB_ORACLEusage below; no issues with these additions from a planning-layer perspective.
252-252: Static declaration fortransform_rownum_to_limitis appropriateDeclaring
transform_rownum_to_limit(Query *parse)asstatickeeps the helper confined to this translation unit and matches its single use fromsubquery_planner; interface and placement look fine.
1038-1044: Early invocation oftransform_rownum_to_limitinsubquery_planneris well-placedRunning
transform_rownum_to_limit(parse)right after initializingPlannerInfoand before CTE processing, MERGE-to-join rewriting, join-tree manipulation, and expression preprocessing is appropriate:
- You see the original
jointree->qualsbeforepreprocess_expressionflattens AND/OR structure and before quals are pushed around, which simplifies detecting a top-levelROWNUMpredicate.- Later calls to
subquery_plannerfor nested queries will each run this helper once at their own entry, so you don’t rely solely on the manualrtablerecursion inside the helper.Given the conservative guards inside
transform_rownum_to_limit, this placement should not affect non-ROWNUM queries or non-Oracledatabase_mode.src/backend/optimizer/plan/createplan.c (3)
82-93: Rownum helper declarations and context struct look consistentThe helper prototypes and
replace_rownum_context(withrownum_varsandrownum_idx) are minimal and sufficient for the later mutator; no issues here.
1281-1287: Propagatingis_unionintoAppendplan node is correctCopying
best_path->is_unionintoAppendensures executor-side union-specific behavior (e.g., per-branch ROWNUM reset) can rely on plan metadata; the change is straightforward and safe.
1574-1578: Propagatingis_unionintoMergeAppendis consistent withAppendSetting
node->is_union = best_path->is_unionkeeps union metadata available for merge-based unions as well; this is consistent with theAppendchange and looks good.
Add ROWNUM pseudocolumn with Oracle-compatible semantics: - ROWNUM <= N optimized to LIMIT for simple queries - Blocks LIMIT optimization for complex queries (ORDER BY, DISTINCT, GROUP BY, aggregation) to preserve Oracle semantics - Handles special Oracle edge cases: ROWNUM > N, ROWNUM >= N, ROWNUM = N - Supports tautology cases: ROWNUM > 0, ROWNUM >= 1 return all rows - Marks ROWNUM as volatile to ensure per-row evaluation Includes comprehensive regression tests covering all edge cases.
In Oracle, each UNION/UNION ALL branch has independent ROWNUM counting. For example: SELECT ROWNUM, id FROM t1 UNION ALL SELECT ROWNUM, id FROM t2 produces rn=1,2,3 from t1 then rn=1,2,3 from t2, not rn=1,2,3,4,5,6. Implementation: - Add is_union flag to Append/MergeAppend plan nodes - Detect UNION operations in planner (generate_union_paths, allpaths.c) - Reset es_rownum in executor when switching between UNION branches - MergeAppend resets before each child in initialization (Sort buffers all) - Append resets when switching to next child during iteration
Added test cases documenting ROWNUM behavior in additional scenarios: - INTERSECT/EXCEPT: Documents that IvorySQL shares ROWNUM counter across both sides (differs from Oracle which resets for each side) - LATERAL joins: Documents that IvorySQL doesn't reset ROWNUM for each outer row (differs from Oracle CROSS APPLY behavior) - DELETE/UPDATE with ROWNUM: Works correctly (matches Oracle) - Empty tables: Works correctly (returns 0 rows) Tests include comments explaining Oracle vs IvorySQL behavior differences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/backend/nodes/nodeFuncs.c (1)
2179-2180: RownumExpr as a primitive leaf is correctly wired into walkers/mutators; consider exprLocation support
- Adding
T_RownumExprto the “primitive node types with no expression subnodes” inexpression_tree_walker_impland to the simplecopyObjectbucket inexpression_tree_mutator_implmatches the struct (no subnodes) and is appropriate.- Optional: if you want error messages that call
exprLocation()to point at ROWNUM usage, consider adding acase T_RownumExpr:there that returns thelocationfield fromRownumExpr. Right now it will quietly return-1.Also applies to: 3061-3062
src/backend/optimizer/path/allpaths.c (1)
1337-1361: ROWNUM UNION detection looks correct but is a bit ad‑hoc; consider clarifying the intent.The
is_union_allheuristic (“alllive_childrelshavertekind == RTE_SUBQUERY”) is a reasonable way to identify set-op appendrels (UNION/UNION ALL) and avoids mis-tagging inheritance/partitionwise-join appendrels, since those children areRTE_RELATIONor joinrels. Propagating this viaappendpath->is_unionon all the main/partial/parallel append paths is exactly what the executor needs for ROWNUM resets.Two minor suggestions:
- The name/comment say “UNION ALL appendrels” but this will also be true for plain UNION (which is what you want for Oracle ROWNUM semantics). Renaming the local to
is_unionand updating the comment would better reflect actual behavior.generate_orderedappend_pathsstill createsAppendPath/MergeAppendPathinstances without touching anyis_unionflag. If the ROWNUM reset logic depends on that flag on ordered append/mergeappend plans as well, double‑check that those nodes get tagged elsewhere (e.g., increate_merge_append_pathorcreateplan.c); otherwise, ordered UNION queries may not reset ROWNUM per branch.If you want to cross‑check, a quick grep for
is_uniononAppendPath/MergeAppendPathplus where the executor inspects it will confirm you’ve covered all relevant plan shapes.Also applies to: 1547-1556, 1559-1568, 1607-1614, 1659-1665
src/backend/executor/execExpr.c (1)
384-413: LGTM! Helper functions correctly detect ROWNUM in expressions.The walker pattern is implemented correctly and handles nested ROWNUM expressions properly through
expression_tree_walker.Optional optimization: The target list is scanned twice—once during projection building (lines 441-550) and again here (line 562). You could detect ROWNUM during the first scan by adding a boolean flag that gets set when a
RownumExpris encountered. However, the current implementation is clearer and the performance impact is negligible since this runs at plan initialization, not per-row execution.Apply this diff if you want to optimize:
ProjectionInfo * ExecBuildProjectionInfo(List *targetList, ExprContext *econtext, TupleTableSlot *slot, PlanState *parent, TupleDesc inputDesc) { ProjectionInfo *projInfo = makeNode(ProjectionInfo); ExprState *state; ExprEvalStep scratch = {0}; ListCell *lc; + bool has_rownum = false; projInfo->pi_exprContext = econtext; /* We embed ExprState into ProjectionInfo instead of doing extra palloc */ projInfo->pi_state.type = T_ExprState; state = &projInfo->pi_state; state->expr = (Expr *) targetList; state->parent = parent; state->ext_params = NULL; state->resultslot = slot; /* Insert setup steps as needed */ ExecCreateExprSetupSteps(state, (Node *) targetList); /* Now compile each tlist column */ foreach(lc, targetList) { TargetEntry *tle = lfirst_node(TargetEntry, lc); + + /* Check if this column contains ROWNUM */ + if (!has_rownum && expression_contains_rownum_walker((Node *) tle->expr, NULL)) + has_rownum = true; /* ... rest of the loop ... */ } scratch.opcode = EEOP_DONE_NO_RETURN; ExprEvalPushStep(state, &scratch); 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); + projInfo->pi_needsMaterialization = has_rownum; return projInfo; }src/backend/executor/execExprInterp.c (1)
3353-3359: Redundant null check can be simplified.The
if (planstate)check on line 3357 is unnecessary becauseplanstateis assigned fromstate->parent, and we're already inside theif (state && state->parent)block guaranteeing it's non-null./* Safely get the PlanState and EState */ if (state && state->parent) { planstate = state->parent; - if (planstate) - estate = planstate->state; + estate = planstate->state; }src/backend/optimizer/plan/createplan.c (1)
2083-2229: ROWNUM-before-Sort rewrite is logically sound but can be slightly tightenedThe transformation to evaluate ROWNUM below
Sort/IncrementalSortand then reference it via Vars in the top projection is correct and matches Oracle semantics (ROWNUM assigned before ORDER BY). The flow:
- Detects
ROWNUMin the projection tlist only when a separate Result node is needed.- Extends the sort input’s tlist with one
RownumExprper occurrence in the final tlist.- Uses
change_plan_targetlistso non–projection-capable inputs get a Result node under the Sort.- Appends corresponding
Vars to the Sort’s own tlist and then replaces allRownumExproccurrences in the final tlist with those Vars.This gives per-row, pre-sort ROWNUM values while keeping the top-level node purely a projection over Vars, which is what the executor expects.
A couple of minor, deferrable nits:
- You recompute
count_rownum_exprsfor eachTargetEntrytwice (once when addingRownumExprs, once when buildingrownum_vars). You could cache the counts in a small side list or struct if you ever care about shaving a tree walk, but it’s probably not worth complicating the code given realistic ROWNUM usage.- The extra ROWNUM columns propagated through Sort are internal plumbing; marking those added
TargetEntrys asresjunk = truewould make their intent clearer, though the separate Result node already hides them from consumers.Functionally, the approach looks good.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
src/oracle_test/regress/expected/rownum.outis excluded by!**/*.outsrc/pl/plisql/src/expected/plisql_rownum.outis excluded by!**/*.out
📒 Files selected for processing (29)
src/backend/executor/execExpr.c(3 hunks)src/backend/executor/execExprInterp.c(3 hunks)src/backend/executor/execUtils.c(1 hunks)src/backend/executor/nodeAppend.c(2 hunks)src/backend/executor/nodeMergeAppend.c(2 hunks)src/backend/executor/nodeResult.c(1 hunks)src/backend/executor/nodeSubplan.c(6 hunks)src/backend/executor/nodeSubqueryscan.c(3 hunks)src/backend/nodes/nodeFuncs.c(5 hunks)src/backend/optimizer/path/allpaths.c(4 hunks)src/backend/optimizer/plan/createplan.c(5 hunks)src/backend/optimizer/plan/planner.c(4 hunks)src/backend/optimizer/prep/prepunion.c(3 hunks)src/backend/optimizer/util/clauses.c(2 hunks)src/backend/optimizer/util/pathnode.c(2 hunks)src/backend/oracle_parser/ora_gram.y(3 hunks)src/backend/parser/parse_expr.c(1 hunks)src/backend/utils/adt/ruleutils.c(2 hunks)src/include/executor/execExpr.h(2 hunks)src/include/executor/execScan.h(3 hunks)src/include/nodes/execnodes.h(5 hunks)src/include/nodes/pathnodes.h(2 hunks)src/include/nodes/plannodes.h(2 hunks)src/include/nodes/primnodes.h(1 hunks)src/include/oracle_parser/ora_kwlist.h(1 hunks)src/oracle_test/regress/parallel_schedule(1 hunks)src/oracle_test/regress/sql/rownum.sql(1 hunks)src/pl/plisql/src/Makefile(1 hunks)src/pl/plisql/src/sql/plisql_rownum.sql(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (14)
- src/include/nodes/plannodes.h
- src/include/nodes/pathnodes.h
- src/backend/optimizer/prep/prepunion.c
- src/backend/utils/adt/ruleutils.c
- src/backend/executor/nodeResult.c
- src/oracle_test/regress/parallel_schedule
- src/backend/executor/execUtils.c
- src/include/nodes/execnodes.h
- src/backend/parser/parse_expr.c
- src/pl/plisql/src/Makefile
- src/backend/executor/nodeAppend.c
- src/backend/optimizer/util/clauses.c
- src/backend/oracle_parser/ora_gram.y
- src/pl/plisql/src/sql/plisql_rownum.sql
🧰 Additional context used
🧬 Code graph analysis (6)
src/backend/optimizer/plan/planner.c (4)
src/include/nodes/pg_list.h (1)
list_length(151-155)src/backend/utils/cache/lsyscache.c (1)
get_opname(1455-1472)src/include/postgres.h (2)
DatumGetInt64(389-397)DatumGetInt32(206-210)src/backend/nodes/makefuncs.c (2)
makeBoolConst(408-414)makeConst(350-379)
src/include/executor/execExpr.h (1)
src/backend/executor/execExprInterp.c (2)
EEOP_ROWNUM(1597-1607)ExecEvalRownum(3346-3371)
src/backend/executor/nodeSubqueryscan.c (1)
src/include/executor/executor.h (1)
ExecProcNode(310-317)
src/backend/executor/execExpr.c (1)
src/backend/executor/execExprInterp.c (1)
EEOP_ROWNUM(1597-1607)
src/include/executor/execScan.h (2)
src/include/executor/executor.h (1)
ExecProject(479-503)src/include/executor/tuptable.h (1)
ExecMaterializeSlot(475-479)
src/backend/optimizer/plan/createplan.c (2)
src/include/nodes/pg_list.h (2)
list_length(151-155)list_nth(298-303)src/backend/nodes/makefuncs.c (2)
makeTargetEntry(289-313)makeVar(66-100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: oracle_pg_regression (ubuntu-latest)
- GitHub Check: oracle_regression (ubuntu-latest)
- GitHub Check: contrib_regression
- GitHub Check: build (ubuntu-latest)
- GitHub Check: pg_regression (ubuntu-latest)
- GitHub Check: meson_build (ubuntu-latest)
🔇 Additional comments (28)
src/backend/executor/nodeSubqueryscan.c (2)
49-52: ROWNUM reset-on-first-call semantics inSubqueryNextThe first-call guard around resetting
es_rownumafterExecProcNode(node->subplan)cleanly addresses the “blocking subplan prefetch” issue described in the comment and matches the intended Oracle-compatible behavior for inline views whose inner plans buffer on the first call.One thing to double‑check: this unconditionally zeros
node->ss.ps.state->es_rownumon the first call for thisSubqueryScan, regardless of any prior ROWNUM activity in the sameEState. That’s correct as long as no already‑materialized, user‑visible tuples for this query block have been assigned ROWNUM beforeSubqueryNext’s first call. If there are plan shapes where aSubqueryScanstarts producing tuples only after other nodes in the same query block have already emitted ROWNUM-bearing rows, this reset would retroactively renumber subsequent rows.If your planner invariants guarantee that this
SubqueryScan’s first tuple is also the first time this query block’s ROWNUM should start counting (for the patterns you’re targeting here, e.g. inline views with blocking children), consider adding a brief note to that effect, or otherwise gating the reset behind a flag that encodes that assumption at plan time.Also applies to: 59-72
133-134: Initialization and rescan ofrownum_resetlook consistentInitializing
subquerystate->rownum_reset = falseinExecInitSubqueryScanand resetting it tofalseinExecReScanSubqueryScancorrectly restores the “first call” behavior on each (re)scan, so the ROWNUM counter is re-zeroed once per scan as intended.This is aligned with how other per-scan flags are managed in the executor and should play well with parameterized rescans and correlated use cases that drive repeated rescans of the same
SubqueryScanState.Also applies to: 204-209
src/backend/nodes/nodeFuncs.c (1)
294-297: ROWNUM type and collation handling look consistent
- Mapping
T_RownumExprtoINT8OIDand treating it as non‑collatable (InvalidOidinexprCollation, assert inexprSetCollation) is internally consistent and matches the intended “numeric pseudocolumn” semantics.Also applies to: 1079-1082, 1340-1343
src/include/oracle_parser/ora_kwlist.h (1)
461-461: ROWNUM keyword entry is correctly classified and ordered
- ASCII ordering between
"rowid"and"rows"is preserved.UNRESERVED_KEYWORD, BARE_LABELmatches typical treatment of a pseudocolumn that should not be fully reserved.src/include/nodes/primnodes.h (1)
2150-2163: RownumExpr node definition is minimal and compatible with existing infrastructure
- Struct layout (just
Expr xprplusParseLoc location) matches how it’s treated as a primitive, leaf node in walkers/mutators.- Doc comment clearly scopes semantics (Oracle ROWNUM, only in Oracle‑compat mode), which is helpful for future readers.
src/backend/optimizer/util/pathnode.c (2)
1363-1363: LGTM!Default initialization of
is_union = falsewith clear guidance for callers to override for UNION paths. This follows a sensible pattern where the path creation function provides a safe default.
1576-1577: LGTM!Consistent with the
create_append_pathinitialization pattern. The comment clearly indicates caller responsibility.src/include/executor/execExpr.h (2)
888-888: LGTM!The declaration matches the implementation signature shown in the relevant code snippets from
execExprInterp.c.
203-203: Verify dispatch_table synchronization in execExprInterp.cThe
EEOP_ROWNUMopcode placement in the enum is correct, but per the comment at lines 71-72, the corresponding handler must be registered in thedispatch_table[]array inexecExprInterp.c:ExecInterpExpr()at the same index position. Ensure the handler entry is present and properly positioned to maintain synchronization between the enum and dispatch table.src/backend/executor/nodeMergeAppend.c (2)
205-207: LGTM!Correctly copies the
is_unionflag from the plan node to the executor state for runtime ROWNUM handling.
252-258: The ROWNUM reset timing is correct; Sort nodes are guaranteed for UNION branches.The assumption that each child will buffer all input tuples is sound. The PostgreSQL planner enforces that all MergeAppend children are presorted on the merge key by injecting Sort or Incremental Sort nodes as needed. For UNION branches, the planner always ensures Sort nodes exist on unsorted children to maintain the merge ordering guarantee. This means the ROWNUM reset before
ExecProcNodewill correctly reset per branch since each branch is backed by a Sort node that buffers its input.src/include/executor/execScan.h (4)
208-216: LGTM - Oracle-compatible pre-increment semantics.Pre-incrementing
es_rownumbefore the qual check ensures that conditions likeROWNUM <= 5see the correct value. The increment is reverted on qual failure (lines 274-275), so only passing rows consume ROWNUM values.The defensive check
if (node->ps.state)is harmless, thoughps.stateshould always be non-NULL for an active scan node.
241-256: LGTM - Materialization preserves ROWNUM values.Materializing the slot when
pi_needsMaterializationis true correctly captures ROWNUM values at projection time, preventing re-evaluation when the tuple is accessed by outer queries. This matches Oracle's semantics where ROWNUM in SELECT lists is captured as a constant.
266-278: LGTM - Correct revert on qual failure.Decrementing
es_rownumwhen the row fails the qual ensures only qualified rows consume ROWNUM values, matching Oracle's behavior.
167-171: Potential ROWNUM inconsistency on early return path.When both
qualandprojInfoare NULL, this path returns without incrementinges_rownum. If ROWNUM is referenced in a higher-level node's projection (e.g., an outer query referencing ROWNUM from a subquery), the counter won't reflect rows passing through this scan.Consider whether this is intentional. If ROWNUM is only valid when there's a projection containing it at this level, this is fine. But if ROWNUM can be pushed down while projInfo remains NULL here, rows would get stale ROWNUM values.
src/oracle_test/regress/sql/rownum.sql (1)
1-659: ROWNUM regression coverage and cleanup look solid.The test script exercises ROWNUM across a wide range of constructs (basic predicates, ORDER BY, DISTINCT/GROUP BY/aggregates, correlated/nested subqueries, UNION/UNION ALL, EXISTS/IN, DML, projection-capability edge cases, and empty tables) and explicitly documents current INTERSECT/EXCEPT and LATERAL limitations. Object lifecycle is well managed: every helper table (including
rownum_test,dept,test1/2,lat_test,del_test,upd_test,empty_test, etc.) is dropped by the end, so the file is self-contained.No syntax or obvious semantic issues stand out from a planner/executor perspective.
src/backend/executor/nodeSubplan.c (1)
69-92: ROWNUM save/reset/restore around subplan execution is consistent and correct; minor redundancy is acceptable.The new logic in
ExecSubPlanandExecSetParamPlan:
- Saves
estate->es_rownum,- Forces
es_rownum = 0for the duration of the subplan/initplan execution, and- Restores the saved value afterward,
ensures that each correlated subquery or initplan gets its own ROWNUM sequence starting at 0, without perturbing the caller’s ROWNUM (outer query block or enclosing subplan). This matches the intended Oracle-compatible behavior.
ExecScanSubPlan’s extraplanstate->state->es_rownum = 0immediately afterExecReScan(planstate)is technically redundant givenExecSubPlan’s earlier reset, but it does no harm and makes the function self-contained if it were ever reused.Two small follow-ups to keep behavior uniform:
- Verify that other subplan-like execution paths (e.g., any special CTE or SetOp execution code that might evaluate subqueries with their own ROWNUM) also follow the same save/reset/restore pattern on
es_rownum, as you did here forExecSubPlanandExecSetParamPlan.- If you prefer to avoid redundancy, you could drop the reset in
ExecScanSubPlanand rely solely onExecSubPlanfor the outer save/reset/restore.Also applies to: 99-102, 274-279, 1122-1123, 1143-1148, 1283-1285
src/backend/executor/execExpr.c (2)
2691-2697: LGTM! T_RownumExpr handling follows the correct pattern.The implementation correctly follows the existing pattern for pseudocolumns (similar to
NextValueExprat lines 2679-2689). The EEOP_ROWNUM opcode is properly defined and handled inexecExprInterp.cas shown in the relevant code snippets.
557-562: Verify that materialization correctly preserves ROWNUM values across query boundaries.The logic to set
pi_needsMaterializationwhen ROWNUM is present makes sense—ROWNUM values are assigned during execution and must be preserved to prevent re-evaluation in outer query contexts. However, verify:
- Does the materialization actually prevent ROWNUM re-evaluation in subqueries accessed by outer queries?
- Should resjunk columns containing ROWNUM also trigger materialization? Currently they do, but resjunk columns are discarded and might not need materialization.
Ensure that materialization applies to the entire projection if ANY column contains ROWNUM, and that sufficient test coverage exists for ROWNUM in subquery and outer query contexts.
src/backend/executor/execExprInterp.c (3)
555-555: Dispatch table entry correctly placed.The EEOP_ROWNUM entry follows the established pattern and is positioned consistently with the ExprEvalOp enum ordering. The static assert at line 613 will catch any ordering issues at compile time.
1597-1607: Handler follows established patterns.The EEOP_ROWNUM case correctly follows the pattern used by similar opcodes like EEOP_NEXTVALUEEXPR, delegating to an out-of-line helper function and advancing with EEO_NEXT().
3365-3370: ROWNUM evaluation logic looks correct.The fallback to 1 when
es_rownum <= 0handles edge cases where ROWNUM is evaluated before the counter is initialized. This defensive approach aligns with Oracle semantics where ROWNUM starts at 1.src/backend/optimizer/plan/planner.c (3)
62-64: New GUC/Oracle-compat includes are appropriateIncluding
utils/guc.handutils/ora_compatible.hhere is correct for accessingdatabase_modeandDB_ORACLE; no issues spotted.
252-252: Statictransform_rownum_to_limitdeclaration is well-scopedDeclaring
transform_rownum_to_limitasstatickeeps the helper local to this translation unit and matches the surrounding style.
656-717: ROWNUM→LIMIT transformation and predicate folding look semantically soundThe overall transform is careful and matches Oracle semantics for the handled forms:
- Guarded by
database_mode == DB_ORACLEandcan_use_limitso only simple SELECT/DML blocks without ORDER BY/DISTINCT/GROUP BY/window/setops/target SRFs are rewritten toLIMIT, avoiding semantic shifts where PostgreSQL’s LIMIT position differs from Oracle’s ROWNUM.- RHS type check to
INT8OID/INT4OID/INT2OIDplus per-typeDatumGetInt*prevents unsafe reinterpretation of arbitrary Const Datums.- Unsatisfiable/tautological predicates are folded aggressively regardless of
can_use_limit:
ROWNUM <= 0,ROWNUM < 1→ always FALSE;ROWNUM = N, N != 1→ always FALSE;ROWNUM > N, N >= 1andROWNUM >= N, N > 1→ always FALSE;ROWNUM > N, N < 1andROWNUM >= N, N <= 1→ tautologies (qual removed).- For satisfiable
<=,<, and= 1cases, the chosenlimit_valueand removal of the corresponding ROWNUM qual fromjointree->qualsare correct; the AND-list rebuild handles 0/1/many-qual cases safely and relies on later preprocessing to constant-fold e.g.other_qual AND FALSE.- Early returns after rewriting unsatisfiable predicates mean you don’t continue mutating a list you’re iterating, which is important given
foreach+list_delete_ptr.Only one LIMIT-producing ROWNUM qual per query block is used (first matching
ROWNUM <= N,ROWNUM < N, orROWNUM = 1), with any remaining ROWNUM predicates evaluated normally at execution. That still preserves correctness, even when combining bounds (e.g.,ROWNUM <= 10 AND ROWNUM > 1becomesLIMIT 10plus an unsatisfiable runtimeROWNUM > 1filter), so this is a planner-optimization choice rather than a semantic risk.Overall, the function is robust and integrates cleanly with later preprocessing.
Also applies to: 718-997, 1000-1031
src/backend/optimizer/plan/createplan.c (3)
82-92: ROWNUM helper declarations and context struct look consistentThe new prototypes and
replace_rownum_contextstruct are self-contained and match the usage patterns later in the file. No issues here.
1280-1288: Propagatingis_unionfromAppendPathtoAppendis correctSetting
plan->is_union = best_path->is_union;ensures executor state can distinguish UNION-style appends from other Append uses (e.g., partition scans) without changing existing behavior whenis_unionis false.
1573-1581:MergeAppendnow carriesis_unionas well — consistent with AppendCopying
best_path->is_unionintonode->is_unionmirrors the Append behavior and lets the executor apply per-UNION-branch ROWNUM semantics for both Append and MergeAppend uniformly.
- Fix count_rownum_exprs_walker signature to use void* context (matches expression_tree_walker callback convention) - Use get_typbyval(INT8OID) instead of hardcoded true for constbyval (follows PostgreSQL coding conventions) Both changes are stylistic improvements that align with PostgreSQL coding standards. The existing code worked due to macro casts and INT8 being pass-by-value on 64-bit systems, but proper signatures and API usage improve maintainability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/backend/optimizer/plan/createplan.c (1)
2091-2227: ROWNUM-before-Sort transformation is logically soundThe transformation that:
- pushes
RownumExprevaluation down into the Sort input,- materializes one scan-level ROWNUM column per occurrence,
- threads them through Sort as extra columns, and
- rewrites the projected tlist to refer to those columns
correctly ensures ROWNUM is evaluated before Sort/IncrementalSort while preserving expression structure (including multiple or nested ROWNUM uses). Attribute numbering and use of
change_plan_targetlist/OUTER_VARlook consistent with existing planner patterns. Any further cleanup (e.g., extracting this block into a dedicated helper for readability) is purely optional.src/backend/optimizer/plan/planner.c (2)
620-1032: ROWNUM→LIMIT transform is logically solid; consider deeper AND handling and multiple‑predicate tighteningThe overall structure is good: Oracle-only guard, early bail if
limitCountalready present, conservativecan_use_limitpredicate, strict RHS type checks, and correct handling of Oracle’s special cases (ROWNUM > N,ROWNUM >= N,ROWNUM = N≠1) as unsatisfiable/tautological predicates. UsingINT2/4/8+DatumGetInt*andget_typbyval(INT8OID)for the generated LIMITConstis also in line with the type system.A few non-blocking improvements to consider:
- You only look at the top-level
BoolExprANDarguments without flattening. For queries likeWHERE rownum <= 10 AND a = 1 AND b = 2, the parser produces nestedANDtrees, so theRownumExpris buried in a childBoolExprand this code will never see it. If you want better coverage, either:
- call
canonicalize_qual/make_ands_implicitonjointree->qualshere, or- recursively descend into
BoolExprnodes withboolop == AND_EXPRinstead of only treating the first level.- You track a single
rownum_qual/limit_valueand leave any other ROWNUM conjuncts untouched. That’s semantically safe (remaining ROWNUM quals still gate rows) but can be suboptimal: for multiple upper bounds (e.g.ROWNUM <= 10 AND ROWNUM < 5) you could choose the tightest effective LIMIT and optionally strip all redundant ROWNUM quals once the LIMIT is installed.- Matching comparison operators via
get_opname()+strcmpon"<=,"=","<",">",">="works for built-ins, but if you ever extend ROWNUM to non-integer or cross-type operators it would be more robust to check the operator’s btree strategy (and perhaps opfamily) instead of relying on the textual name.These are all enhancement/coverage issues rather than correctness problems; the existing logic is safe as-is.
1128-1134: Early call totransform_rownum_to_limitinsubquery_planneris well‑placed but redundant with the function’s own recursionRunning
transform_rownum_to_limit(parse)right after initializingPlannerInfo(and before CTE processing, join pull‑up, and expression preprocessing) is a sensible place to do the rewrite so that both the current query and its descendants see any LIMITs derived from ROWNUM early.Note, though, that
transform_rownum_to_limit()itself already walksparse->rtableand recurses intoRTE_SUBQUERYentries. When each subquery later goes through its ownsubquery_plannerinvocation, the transform runs twice on those trees (the second time it quickly bails due toparse->limitCountbeing set). If you want to simplify control flow and avoid the extra pass, you could:
- either keep this top-level
subquery_plannercall and drop the explicit recursion overrtableinsidetransform_rownum_to_limit, or- remove the
subquery_plannercall and rely purely on per-Query invocations (top-level + subqueries) instead of recursing manually.Not urgent, but it would make the execution order easier to reason about.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/backend/optimizer/plan/createplan.c(5 hunks)src/backend/optimizer/plan/planner.c(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/backend/optimizer/plan/planner.c (4)
src/include/nodes/pg_list.h (1)
list_length(151-155)src/backend/utils/cache/lsyscache.c (1)
get_opname(1455-1472)src/include/postgres.h (3)
DatumGetInt64(389-397)DatumGetInt32(206-210)DatumGetInt16(166-170)src/backend/nodes/makefuncs.c (2)
makeBoolConst(408-414)makeConst(350-379)
src/backend/optimizer/plan/createplan.c (2)
src/include/nodes/pg_list.h (2)
list_length(151-155)list_nth(298-303)src/backend/nodes/makefuncs.c (2)
makeTargetEntry(289-313)makeVar(66-100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: build (ubuntu-latest)
- GitHub Check: pg_regression (ubuntu-latest)
- GitHub Check: oracle_pg_regression (ubuntu-latest)
- GitHub Check: contrib_regression
- GitHub Check: oracle_regression (ubuntu-latest)
- GitHub Check: meson_build (ubuntu-latest)
🔇 Additional comments (5)
src/backend/optimizer/plan/createplan.c (2)
82-92: ROWNUM helper declarations are well-scoped and consistentStatic prototypes and
replace_rownum_contextare appropriately introduced here and keep the ROWNUM machinery internal tocreateplan.c. No issues with visibility or basic structure.
1286-1287: is_union propagation for Append/MergeAppend looks correctCopying
best_path->is_unionintoAppendandMergeAppendplan nodes is the right place to carry set-op/UNION context down to the executor. Assuming the path and plan node structs both have the newis_unionfield, this wiring is straightforward and correct.Also applies to: 1577-1578
src/backend/optimizer/plan/planner.c (3)
62-64: New includes for Oracle compatibility mode are appropriateBringing in
utils/guc.handutils/ora_compatible.hhere makes sense given the new dependency ondatabase_mode/DB_ORACLEintransform_rownum_to_limit(), andutils/lsyscache.halready coversget_opname/get_typbyval. No issues spotted.
252-252: Static declaration placement fortransform_rownum_to_limitis fineDeclaring
transform_rownum_to_limit(Query *parse)alongside the other file-local planner helpers keeps the interface clear and correctly limits scope to this translation unit.
1002-1006:makeConstfor LIMIT uses correct by‑val flagConstructing
limitCountas anINT8OIDConstwithsizeof(int64),Int64GetDatum(limit_value), andconstbyval = get_typbyval(INT8OID)is the right pattern and avoids the previousFLOAT8PASSBYVALhack. This should interoperate cleanly withpreprocess_limit()andlimit_needed(), which assume a pass‑by‑value 8‑byte integerConst.
Change replace_rownum_expr_mutator to use void* context parameter to match expression_tree_mutator callback convention. This is the same fix applied to count_rownum_exprs_walker in the previous commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/backend/optimizer/plan/createplan.c (2)
2084-2229: ROWNUM-before-Sort rewrite is logically sound; consider tightening parallel safety and minor detailsThe special-case block that, when
needs_result_nodeandsubplanisSort/IncrementalSortand the projection tlist containsROWNUM, does the following:
- Pushes one
RownumExprper occurrence into the sort input (sortinput) viachange_plan_targetlist, so ROWNUM is evaluated on the unsorted stream.- Builds matching
Vars (rownum_vars) pointing at those new scan outputs.- Extends the Sort/IncrementalSort’s own targetlist with TLEs referencing those Vars, so the ROWNUM values flow through the sort.
- Rewrites the final projection tlist by replacing every
RownumExprwith the correspondingVarusingreplace_rownum_expr_mutator.That preserves Oracle semantics (“assign ROWNUM before ORDER BY”) while still letting the projection happen in a
Resultnode above the sort, and handles multiple and nested ROWNUM occurrences correctly. Thecount_rownum_exprs+rownum_idxmapping is consistent and safe (and all per-rownum columns have the same value anyway).Two non-blocking points you may want to tighten:
Parallel safety propagation: the call
sortinput = change_plan_targetlist(sortinput, new_input_tlist, sortinput->parallel_safe);reuses the existing
parallel_safeflag as thetlist_parallel_safeargument. IfRownumExpris treated as parallel-unsafe, it would be safer to either:
- pass
falsehere when you’re injecting ROWNUM, or- compute
tlist_parallel_safewithis_parallel_safe(root, (Node *) new_input_tlist)(or at least for the addedRownumExprs).
Otherwise there’s a risk of marking this subtree parallel‑safe even though we just introduced a parallel‑unsafe expression. This may already be guarded elsewhere in the ROWNUM plumbing, but it’s worth confirming.Minor optimization/nit: inside
replace_rownum_expr_mutator, you repeatedly calllist_length(ctx->rownum_vars)on everyRownumExprvisit. If you care about micro‑efficiency with many occurrences, caching that length inreplace_rownum_context(e.g.,int rownum_count) avoids repeated O(n) walks; not critical given typical usage.Functionally, though, the transformation looks correct and in line with the rest of the plan‑building patterns in this file.
7575-7687: ROWNUM helper implementations match walker/mutator contracts and handle nested usage correctly
contain_rownum_expr_walker/contain_rownum_exprcorrectly useexpression_tree_walkerand short‑circuit onRownumExpr.replace_rownum_expr_mutatornow has the propervoid *contextsignature, casts toreplace_rownum_context *, and walks withexpression_tree_mutator, replacing eachRownumExprwith the nextVarfromrownum_vars, with a defensiveelog(ERROR)if the count underflows.count_rownum_exprs_walker/count_rownum_exprscorrectly count allRownumExprnodes using the walker pattern.collect_rownum_exprscleanly gathersTargetEntrys whose expressions containROWNUM, covering nested occurrences.These are idiomatic uses of the nodeFuncs walker/mutator APIs and line up with the prototypes at the top of the file. The only tiny polish you might consider (purely optional) is storing the list length in
replace_rownum_contextinstead of callinglist_lengthin each replacement.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/backend/optimizer/plan/createplan.c(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/backend/optimizer/plan/createplan.c (2)
src/include/nodes/pg_list.h (2)
list_length(151-155)list_nth(298-303)src/backend/nodes/makefuncs.c (2)
makeTargetEntry(289-313)makeVar(66-100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: pg_regression (ubuntu-latest)
- GitHub Check: build (ubuntu-latest)
- GitHub Check: oracle_regression (ubuntu-latest)
- GitHub Check: oracle_pg_regression (ubuntu-latest)
- GitHub Check: meson_build (ubuntu-latest)
- GitHub Check: contrib_regression
🔇 Additional comments (3)
src/backend/optimizer/plan/createplan.c (3)
82-93: ROWNUM helper declarations and context wiring are consistentThe forward declarations for
contain_rownum_expr,count_rownum_exprs,collect_rownum_exprs, andreplace_rownum_expr_mutator, plusreplace_rownum_context, all line up with the implementations later in the file and with the walker/mutator API expectations. No issues here.
1280-1287: Propagatingis_unionfromAppendPathtoAppendlooks correct – just ensure executor side is wiredSetting
plan->is_union = best_path->is_union;is the right place to carry the planner flag into the plan node for UNION‑specific ROWNUM behavior. Locally this is self‑contained; just make sure corresponding fields and logic exist in theAppendnode struct, copyfuncs/outfuncs, and executor (e.g.,ExecInitAppend/ExecAppend) to actually honor it.
1574-1578:MergeAppendis_unionpropagation mirrorsAppendand is fine
node->is_union = best_path->is_union;correctly propagates the UNION flag forMergeAppendbranches, which is needed for ROWNUM reset semantics when inputs are merged via a sorted append. As withAppend, just confirm theMergeAppendstruct, serialization, and executor logic are updated to use this flag.
|
Hi, Rophy, Your implementation uses a shared es_rownum counter in the EState structure, combined with the timing of resets in SubqueryNext for nested subqueries. The reset (es_rownum = 0) occurs after fetching the first tuple from the subplan, which works for simple cases but may have problem in multiple nested scenarios. I tested this with the following case: CREATE TABLE eemp( INSERT INTO eemp VALUES (7369,'SMITH','CLERK',7902, to_date('17-10-1980','dd-mm-yyyy'),800,NULL,20); -- Pagination with ROWNUM Expected result: Actually got: The result has one less than correct result and skips to even values like 8, 10, 12, 14. I made a fix for this issue as attached. Please let me know if it is appropriate. fix_multiple_nested_subquery.patch Thanks, |
|
With regard to the database_mode and compatible_mode, they are different in IvorySQL. I believe the following code should uniformly use compatible_mode. transform_rownum_to_limit()
transformColumnRefInternal() struct EState
|
|
Using the same test data in my first comment, another issue is found. SQL: Expected result: Actually got:
|
Add test case for Oracle pagination pattern with nested subqueries. This exposes a bug where shared es_rownum counter causes skipped values (8,10,12,14 instead of 8,9,10,11,12) in nested subquery scans.
Each SubqueryScan now maintains its own sub_rownum counter instead of sharing es_rownum globally. On each tuple fetch, the outer query's counter is saved, this subquery's counter is swapped in, the subplan executes, then the outer counter is restored. This fixes the Oracle pagination pattern where nested subqueries caused skipped ROWNUM values (8,10,12,14 instead of 8,9,10,11,12). Co-authored-by: Steven <bigplaice@users.noreply.github.com>
Change ROWNUM checks from database_mode == DB_ORACLE to compatible_db == ORA_PARSER. This allows ROWNUM to work based on the session's compatible_mode setting rather than the cluster's database_mode. - database_mode: set at initdb, cannot change at runtime - compatible_mode: can be changed per-session Co-authored-by: Steven <bigplaice@users.noreply.github.com>
ROWNUM must appear in the GROUP BY clause or be used in an aggregate function, matching Oracle's behavior. Without this check, queries like "SELECT rownum FROM t GROUP BY category" would incorrectly return results instead of raising an error. Co-authored-by: Steven <bigplaice@users.noreply.github.com>
When ROWNUM is used inside aggregate functions like MIN(rownum),
MAX(rownum), SUM(rownum), AVG(rownum), the planner now adds ROWNUM
to the subplan's target list. This ensures ROWNUM is evaluated
during the scan (where it increments for each row) rather than
during aggregation (where it would return the same value).
The fix modifies create_agg_plan() to detect ROWNUM in aggregate
arguments and add a RownumExpr to the scan's target list. The
setrefs.c code then matches the RownumExpr in aggregate arguments
with the one in the subplan and creates appropriate Var references.
Before: SELECT MIN(rownum), MAX(rownum), SUM(rownum) FROM t;
-> 1, 1, 6 (for 6 rows - ROWNUM always evaluated as 1)
After: SELECT MIN(rownum), MAX(rownum), SUM(rownum) FROM t;
-> 1, 6, 21 (correct values for 6 rows)
Adds test cases for: - Subquery with aggregate containing ROWNUM - Aggregate of subquery with ROWNUM in SELECT - Nested subquery aggregates with GROUP BY - Multiple levels of aggregation
When using ROWNUM inside aggregates with GROUP BY and ORDER BY, queries would fail with "Aggref found in non-Agg plan node" if the ROWNUM-containing aggregate wasn't the last column in the SELECT list. The fix skips special ROWNUM handling when ROWNUM only appears inside aggregate function arguments, since the Agg node correctly evaluates ROWNUM from its input. Also removes broken targetlist reordering logic that attempted to reorder Sort output (Sort is non-projection-capable). Added tests for: - Aggregate + GROUP BY + ORDER BY with various column orders - Multi-layer nested aggregation with ROWNUM (verified against Oracle)
Added a test case in commit 3a9f276 to cover the issue, and applied your patch in commit 11a8ed6, works great.
Fixed in commit 2ecbfb7
Fixed in commit 5e4b64b
Commit 91730a3 and commit 0455343 attempted to support aggregate functions, group by / order by and nested. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @src/backend/optimizer/plan/planner.c:
- Around line 719-997: The loop over ROWNUM clauses in the foreach scanning
andlist exits early because the branches handling opname "<=", "=", and "<" use
break, which leaves later ROWNUM predicates (e.g., ones that make the query
always false) unexamined; change those break statements to continue so the
foreach loop keeps scanning all quals (ensure you preserve setting limit_value
and rownum_qual, and that opname is still pfree'd on every path), i.e., in the
branches that currently do "if (can_use_limit) { limit_value = n; rownum_qual =
qual; } pfree(opname); break;" (and the "<" variant using n-1) replace the final
break with continue so the loop examines remaining quals in andlist.
🧹 Nitpick comments (5)
src/backend/executor/nodeSubqueryscan.c (1)
200-204: Minor: Comment could be more precise.The comment states "Each rescan starts with ROWNUM = 1" but the counter is reset to 0. While functionally correct (the increment happens during scanning), the comment could better reflect the actual reset value.
Suggested clarification
/* * Reset local ROWNUM counter for Oracle compatibility. - * Each rescan starts with ROWNUM = 1. + * Reset to 0 so the first emitted row will have ROWNUM = 1 after increment. */ node->sub_rownum = 0;src/backend/optimizer/plan/planner.c (1)
1128-1134: Good placement oftransform_rownum_to_limitinsubquery_planner, with minor coverage noteCalling
transform_rownum_to_limit(parse)very early insubquery_planner(before CTE processing, MERGE rewrite, jointree normalization, and expression preprocessing) is a sensible choice:
- It sees the original
jointree->qualsshape before canonicalization and constant‑folding.- The recursive walk over
parse->rtableensures ROWNUM predicates in subquery RTEs are handled uniformly.- Running before
preprocess_expressionandpreprocess_qual_conditionsavoids fighting with later rewrites.One thing to be aware of (not necessarily for this PR) is that this hook does not currently touch:
- ROWNUM predicates inside SubLinks (Subquery expressions in quals/targets), or
- ROWNUM inside CTE query trees (
cteList), which are expanded bySS_process_ctesafter this call.Those cases will still get correct runtime semantics from the executor, but they won’t benefit from the LIMIT/unsatisfiable rewrites. If you ever need planner‑level ROWNUM behavior there too, you’d want an expression‑tree walker or a second entry point that runs over SubLink/CTE
Querynodes.src/backend/optimizer/plan/createplan.c (3)
2086-2243: ROWNUM-before-Sort rewrite is correct but dense; consider small clarificationsThe ROWNUM+Sort/IncrementalSort special-case in
create_projection_planlooks logically sound:
- It only triggers when we actually need a separate Result node and the immediate subplan is
SortorIncrementalSort.- It skips cases where ROWNUM occurs only inside aggregate arguments (
aggref_args_contain_rownum), which are handled viacreate_agg_plan.- It:
- Adds one
RownumExprper occurrence to the sort input's tlist viachange_plan_targetlist, so ROWNUM is evaluated in the scan/producer.- Builds a parallel list of
OUTER_VARVars (rownum_vars) that point at those new columns.- Extends the Sort’s own targetlist with Vars so the ROWNUM values propagate through the Sort.
- Replaces all
RownumExprnodes in the final projection tlist with those Vars, ensuring ROWNUM is computed before the sort but only referenced above it.Two things worth tightening or at least documenting:
Mixed aggregate and non-aggregate ROWNUM with Sort
When the same SELECT list contains both aggregates over ROWNUM and bare ROWNUM,
aggref_args_contain_rownum(tlist)will be true, so this block is skipped and the non-aggregate ROWNUM will again be evaluated above Sort. If such queries are meant to be valid under your “ROWNUM must be grouped or aggregated” rule (e.g., ROWNUM appears in both GROUP BY and an aggregate), you may want to:
- Either explicitly reject this pattern in parse analysis, or
- Relax the
!aggref_args_contain_rownum(tlist)guard and handle both cases in a unified way.Readability / maintenance
The ROWNUM rewrite is a sizeable chunk inside
create_projection_plan. Extracting it into a helper like:static void pushdown_rownum_through_sort(Plan *sort_plan, List **ptlist);would make
create_projection_planeasier to follow and reduce the chance of regressions if Sort handling changes later.Functionally this all looks correct; these are mostly about clarifying semantics and future-proofing.
It’d be useful to add or confirm tests for cases like:
SELECT ROWNUM, SUM(ROWNUM) FROM t GROUP BY ROWNUM ORDER BY some_col;SELECT ROWNUM FROM (SELECT ... ORDER BY ...) ORDER BY ...;to ensure mixed aggregate/non-aggregate ROWNUM with ORDER BY behaves as intended with this guard in place.
2496-2526: Agg plan ROWNUM support is good; consider extending detection beyond SELECT tlistAdding a single
RownumExprto the Agg subplan’s targetlist whenaggref_args_contain_rownum(tlist)is true is a clean way to make ROWNUM visible toAgg(and tosetrefs.cfor Var substitution).A couple of edge cases to double-check:
ROWNUM aggregates not in the SELECT list
aggref_args_contain_rownum(tlist)only inspects expressions reachable from the Agg’s output tlist. If you allow constructs likeHAVING max(ROWNUM) < 10orORDER BY max(ROWNUM)where the aggregate over ROWNUM appears only inbest_path->qualor sort clauses, those won’t currently trigger this block. In that case, you may want to:
- Either extend the detection to also walk
best_path->qual(and possibly relevant ORDER BY expressions), or- Formally disallow such uses and enforce that aggregates over ROWNUM must appear in the SELECT list.
GROUPING SETS / ROLLUP / CUBE
create_groupingsets_planbuilds Agg nodes separately and doesn’t currently callaggref_args_contain_rownum, so ROWNUM inside aggregates in grouping sets queries may not get the same subplan-tlist ROWNUM column. If you expect Oracle-style ROWNUM-in-aggregate semantics to work there too, you probably need a similar injection increate_groupingsets_plan.None of this blocks the current patch, but clarifying the intended support surface (simple Agg only vs. also HAVING and GROUPING SETS) and adding a few regression tests around those cases would be valuable.
7623-7794: ROWNUM walker/mutator helpers are correct; only minor efficiency nitsThe new helpers:
contain_rownum_expr[_walker]replace_rownum_expr_mutatorcount_rownum_exprs[_walker]collect_rownum_exprsaggref_args_contain_rownum[_walker]all have the right
bool (Node *, void *)/Node *(Node *, void *)signatures and useexpression_tree_walker/mutatoridiomatically; they avoid recursing intoRownumExprwhere appropriate (for counting) and correctly limitaggref_args_contain_rownumto ROWNUM occurrences insideAggrefarguments/direct args/filters.Two small, optional cleanups:
aggref_args_contain_rownum_walkercurrently callscontain_rownum_expron Aggref args and then also invokesexpression_tree_walkeron the node, which re-walks the same subtree in the no-ROWNUM case. Given these are planner-time only and trees are small, it’s fine, but you could drop the extraexpression_tree_walkerfor Aggref branches (returningfalseinstead) to avoid double work.
collect_rownum_exprsfollowed bycount_rownum_exprsmeans each TLE containing ROWNUM is traversed twice. That’s also harmless, but if this ever shows up in hot planner paths you might fold counting intocollect_rownum_exprsto track per-TLE counts in one walk.As written, the helpers are correct and maintainable; the above is purely micro-optimization.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
src/oracle_test/regress/expected/rownum.outis excluded by!**/*.out
📒 Files selected for processing (7)
src/backend/executor/nodeSubqueryscan.csrc/backend/optimizer/plan/createplan.csrc/backend/optimizer/plan/planner.csrc/backend/parser/parse_agg.csrc/backend/parser/parse_expr.csrc/include/nodes/execnodes.hsrc/oracle_test/regress/sql/rownum.sql
🚧 Files skipped from review as they are similar to previous changes (2)
- src/backend/parser/parse_expr.c
- src/oracle_test/regress/sql/rownum.sql
🧰 Additional context used
🧬 Code graph analysis (3)
src/backend/executor/nodeSubqueryscan.c (1)
src/include/executor/executor.h (1)
ExecProcNode(310-317)
src/backend/parser/parse_agg.c (2)
src/backend/utils/error/elog.c (2)
errcode(853-864)errmsg(1070-1086)src/backend/parser/parse_node.c (1)
parser_errposition(108-123)
src/backend/optimizer/plan/createplan.c (2)
src/include/nodes/pg_list.h (2)
list_length(151-155)list_nth(298-303)src/backend/nodes/makefuncs.c (2)
makeTargetEntry(289-313)makeVar(66-100)
🔇 Additional comments (11)
src/backend/parser/parse_agg.c (1)
1417-1433: LGTM — Correct implementation of ROWNUM validation in GROUP BY context.The logic correctly mirrors the existing
Varhandling pattern:
- At the current query level (
sublevels_up == 0), ROWNUM must appear in GROUP BY or be aggregated- For outer query references, the expression is returned as-is (not our problem to validate)
The error message and error code are appropriate for Oracle compatibility.
src/backend/executor/nodeSubqueryscan.c (2)
49-67: LGTM — Correct save/restore pattern for nested subquery ROWNUM independence.The implementation correctly addresses the nested subquery pagination bug mentioned in the PR comments:
- Saves outer query's
es_rownum- Swaps in subquery's local
sub_rownum- Executes subplan (which may increment
es_rownum)- Captures updated counter back to
sub_rownum- Restores outer query's counter
This ensures each subquery maintains an independent ROWNUM sequence.
129-129: LGTM — Proper initialization of the local ROWNUM counter.Initializing to 0 is correct since the counter is pre-incremented during scan execution, making the first emitted row have ROWNUM=1.
src/include/nodes/execnodes.h (5)
383-384: LGTM — Appropriate field for tracking materialization needs.The
pi_needsMaterializationflag is well-placed and the comment correctly indicates its purpose for volatile expressions like ROWNUM.
752-757: LGTM — Correct type and documentation for ROWNUM counter.Using
int64fores_rownumis appropriate to handle large row counts. The comment correctly documents the Oracle compatibility context and when this field is active.
1521-1521: LGTM — Union flag for Append ROWNUM handling.The
as_is_unionflag enables proper ROWNUM reset on UNION branch switches, consistent with Oracle semantics where each UNION branch has independent ROWNUM sequences.
1551-1551: LGTM — Consistent union flag for MergeAppend.The
ms_is_unionflag mirrorsas_is_unionin AppendState, maintaining consistency for ROWNUM handling across both append node types.
1951-1959: LGTM — Well-documented local ROWNUM counter for subqueries.The
sub_rownumfield and accompanying documentation clearly explain the purpose: enabling independent ROWNUM sequences for nested subqueries, which is essential for Oracle-compatible behavior.src/backend/optimizer/plan/planner.c (1)
1000-1006:makeConstfor LIMIT uses correct by‑val flagUsing
get_typbyval(INT8OID)as theconstbyvalargument in the synthesized LIMIT Const is the right direction and avoids hard‑coding assumptions likeFLOAT8PASSBYVALfor anINT8OIDvalue. This keeps the code aligned with the type system and previous static‑analysis feedback.src/backend/optimizer/plan/createplan.c (2)
82-95: ROWNUM helper declarations and context struct look consistentThe new ROWNUM-related prototypes and
replace_rownum_contextstruct are well-aligned with the implementations below and follow the usual nodeFuncs walker/mutator patterns. No issues here.
1288-1288: Propagation ofis_unionfrom paths to Append/MergeAppend plansCopying
best_path->is_unionintoAppend.plan->is_unionandMergeAppend.node->is_unionis straightforward and matches the described executor-side ROWNUM reset behavior for UNION branches. GivenmakeNode(Append/MergeAppend)zero-initializes memory, non-UNION cases will safely seefalsehere.Please double-check that all UNION-style Append/MergeAppend paths that should drive per-branch ROWNUM counters are going through these create_*_plan paths (and not only via
make_append), and that no non-UNION callers expectis_unionto ever be non-zero. A quick grep over Append/MergeAppend constructors plus existing ROWNUM tests should be enough.Also applies to: 1579-1579
| foreach(lc, andlist) | ||
| { | ||
| Node *qual = (Node *) lfirst(lc); | ||
| OpExpr *opexpr; | ||
| Node *leftop; | ||
| Node *rightop; | ||
| char *opname; | ||
| Const *constval; | ||
| int64 n; | ||
|
|
||
| /* We're looking for OpExpr nodes (comparison operators) */ | ||
| if (!IsA(qual, OpExpr)) | ||
| continue; | ||
|
|
||
| opexpr = (OpExpr *) qual; | ||
|
|
||
| /* Need exactly 2 arguments */ | ||
| if (list_length(opexpr->args) != 2) | ||
| continue; | ||
|
|
||
| leftop = (Node *) linitial(opexpr->args); | ||
| rightop = (Node *) lsecond(opexpr->args); | ||
|
|
||
| /* Check if left operand is ROWNUM */ | ||
| if (!IsA(leftop, RownumExpr)) | ||
| continue; | ||
|
|
||
| /* Right operand must be a constant */ | ||
| if (!IsA(rightop, Const)) | ||
| continue; | ||
|
|
||
| /* Get the operator name */ | ||
| opname = get_opname(opexpr->opno); | ||
| if (opname == NULL) | ||
| continue; | ||
|
|
||
| /* Now handle different operators */ | ||
| constval = (Const *) rightop; | ||
|
|
||
| if (constval->constisnull) | ||
| { | ||
| pfree(opname); | ||
| continue; | ||
| } | ||
|
|
||
| /* | ||
| * Validate that the constant is a numeric type we can safely convert | ||
| * to int64. This prevents undefined behavior from unexpected types. | ||
| */ | ||
| if (constval->consttype != INT8OID && | ||
| constval->consttype != INT4OID && | ||
| constval->consttype != INT2OID) | ||
| { | ||
| pfree(opname); | ||
| continue; | ||
| } | ||
|
|
||
| /* Extract the integer value based on type */ | ||
| switch (constval->consttype) | ||
| { | ||
| case INT8OID: | ||
| n = DatumGetInt64(constval->constvalue); | ||
| break; | ||
| case INT4OID: | ||
| n = (int64) DatumGetInt32(constval->constvalue); | ||
| break; | ||
| case INT2OID: | ||
| n = (int64) DatumGetInt16(constval->constvalue); | ||
| break; | ||
| default: | ||
| /* Should not reach here due to check above */ | ||
| pfree(opname); | ||
| continue; | ||
| } | ||
|
|
||
| if (strcmp(opname, "<=") == 0) | ||
| { | ||
| /* | ||
| * ROWNUM <= N: | ||
| * N <= 0: always false (ROWNUM starts at 1) | ||
| * N > 0: can be optimized to LIMIT N (only for simple queries) | ||
| */ | ||
| if (n <= 0) | ||
| { | ||
| /* Always false - rewrite as FALSE constant */ | ||
| BoolExpr *newand; | ||
| Const *falseconst = (Const *) makeBoolConst(false, false); | ||
|
|
||
| andlist = list_delete_ptr(andlist, qual); | ||
| andlist = lappend(andlist, falseconst); | ||
|
|
||
| /* Rebuild WHERE clause */ | ||
| if (list_length(andlist) == 0) | ||
| jointree->quals = NULL; | ||
| else if (list_length(andlist) == 1) | ||
| jointree->quals = (Node *) linitial(andlist); | ||
| else | ||
| { | ||
| newand = makeNode(BoolExpr); | ||
| newand->boolop = AND_EXPR; | ||
| newand->args = andlist; | ||
| newand->location = -1; | ||
| jointree->quals = (Node *) newand; | ||
| } | ||
|
|
||
| pfree(opname); | ||
| return; | ||
| } | ||
| else if (can_use_limit) | ||
| { | ||
| limit_value = n; | ||
| rownum_qual = qual; | ||
| } | ||
| pfree(opname); | ||
| break; | ||
| } | ||
| else if (strcmp(opname, "=") == 0) | ||
| { | ||
| /* | ||
| * ROWNUM = 1 can be optimized to LIMIT 1 (only for simple queries). | ||
| * ROWNUM = N where N != 1 is always false (Oracle semantics) - always process. | ||
| */ | ||
| if (n == 1 && can_use_limit) | ||
| { | ||
| limit_value = n; | ||
| rownum_qual = qual; | ||
| } | ||
| else if (n != 1) | ||
| { | ||
| /* ROWNUM = N where N != 1 is always false */ | ||
| BoolExpr *newand; | ||
| Const *falseconst; | ||
|
|
||
| falseconst = (Const *) makeBoolConst(false, false); | ||
|
|
||
| /* Replace this qual with FALSE in the AND list */ | ||
| andlist = list_delete_ptr(andlist, qual); | ||
| andlist = lappend(andlist, falseconst); | ||
|
|
||
| /* Rebuild the WHERE clause */ | ||
| if (list_length(andlist) == 0) | ||
| { | ||
| jointree->quals = NULL; | ||
| } | ||
| else if (list_length(andlist) == 1) | ||
| { | ||
| jointree->quals = (Node *) linitial(andlist); | ||
| } | ||
| else | ||
| { | ||
| newand = makeNode(BoolExpr); | ||
| newand->boolop = AND_EXPR; | ||
| newand->args = andlist; | ||
| newand->location = -1; | ||
| jointree->quals = (Node *) newand; | ||
| } | ||
|
|
||
| pfree(opname); | ||
| return; | ||
| } | ||
| pfree(opname); | ||
| break; | ||
| } | ||
| else if (strcmp(opname, "<") == 0) | ||
| { | ||
| /* | ||
| * ROWNUM < N: | ||
| * N <= 1: always false (ROWNUM starts at 1, so < 1 is impossible) | ||
| * N > 1: can be optimized to LIMIT N-1 (only for simple queries) | ||
| */ | ||
| if (n <= 1) | ||
| { | ||
| /* Always false - rewrite as FALSE constant */ | ||
| BoolExpr *newand; | ||
| Const *falseconst = (Const *) makeBoolConst(false, false); | ||
|
|
||
| andlist = list_delete_ptr(andlist, qual); | ||
| andlist = lappend(andlist, falseconst); | ||
|
|
||
| /* Rebuild WHERE clause */ | ||
| if (list_length(andlist) == 0) | ||
| jointree->quals = NULL; | ||
| else if (list_length(andlist) == 1) | ||
| jointree->quals = (Node *) linitial(andlist); | ||
| else | ||
| { | ||
| newand = makeNode(BoolExpr); | ||
| newand->boolop = AND_EXPR; | ||
| newand->args = andlist; | ||
| newand->location = -1; | ||
| jointree->quals = (Node *) newand; | ||
| } | ||
|
|
||
| pfree(opname); | ||
| return; | ||
| } | ||
| else if (can_use_limit) | ||
| { | ||
| limit_value = n - 1; | ||
| rownum_qual = qual; | ||
| } | ||
| pfree(opname); | ||
| break; | ||
| } | ||
| else if (strcmp(opname, ">") == 0) | ||
| { | ||
| /* | ||
| * ROWNUM > N: | ||
| * N >= 1: always false | ||
| * N < 1: tautology, remove qual | ||
| */ | ||
| BoolExpr *newand; | ||
|
|
||
| andlist = list_delete_ptr(andlist, qual); | ||
|
|
||
| if (n >= 1) | ||
| { | ||
| /* Always false - add FALSE constant */ | ||
| Const *falseconst = (Const *) makeBoolConst(false, false); | ||
| andlist = lappend(andlist, falseconst); | ||
| } | ||
| /* else: tautology, just remove qual */ | ||
|
|
||
| /* Rebuild WHERE clause */ | ||
| if (list_length(andlist) == 0) | ||
| jointree->quals = NULL; | ||
| else if (list_length(andlist) == 1) | ||
| jointree->quals = (Node *) linitial(andlist); | ||
| else | ||
| { | ||
| newand = makeNode(BoolExpr); | ||
| newand->boolop = AND_EXPR; | ||
| newand->args = andlist; | ||
| newand->location = -1; | ||
| jointree->quals = (Node *) newand; | ||
| } | ||
|
|
||
| pfree(opname); | ||
| return; | ||
| } | ||
| else if (strcmp(opname, ">=") == 0) | ||
| { | ||
| /* | ||
| * ROWNUM >= N: | ||
| * N > 1: always false | ||
| * N <= 1: tautology, remove qual | ||
| */ | ||
| BoolExpr *newand; | ||
|
|
||
| andlist = list_delete_ptr(andlist, qual); | ||
|
|
||
| if (n > 1) | ||
| { | ||
| /* Always false - add FALSE constant */ | ||
| Const *falseconst = (Const *) makeBoolConst(false, false); | ||
| andlist = lappend(andlist, falseconst); | ||
| } | ||
| /* else: tautology, just remove qual */ | ||
|
|
||
| /* Rebuild WHERE clause */ | ||
| if (list_length(andlist) == 0) | ||
| jointree->quals = NULL; | ||
| else if (list_length(andlist) == 1) | ||
| jointree->quals = (Node *) linitial(andlist); | ||
| else | ||
| { | ||
| newand = makeNode(BoolExpr); | ||
| newand->boolop = AND_EXPR; | ||
| newand->args = andlist; | ||
| newand->location = -1; | ||
| jointree->quals = (Node *) newand; | ||
| } | ||
|
|
||
| pfree(opname); | ||
| return; | ||
| } | ||
|
|
||
| pfree(opname); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's verify the file exists and read the relevant lines
wc -l src/backend/optimizer/plan/planner.cRepository: IvorySQL/IvorySQL
Length of output: 102
🏁 Script executed:
# Read the specific lines mentioned in the review
sed -n '719,997p' src/backend/optimizer/plan/planner.c | head -100Repository: IvorySQL/IvorySQL
Length of output: 2389
🏁 Script executed:
# Let's see more context around get_opname call
sed -n '719,850p' src/backend/optimizer/plan/planner.cRepository: IvorySQL/IvorySQL
Length of output: 3064
🏁 Script executed:
# Check the complete structure of the foreach loop to understand break statements
sed -n '719,997p' src/backend/optimizer/plan/planner.cRepository: IvorySQL/IvorySQL
Length of output: 6390
🏁 Script executed:
# Verify the exact line locations of break statements to confirm loop exit behavior
sed -n '780,850p' src/backend/optimizer/plan/planner.c | grep -n "break\|return" | head -20Repository: IvorySQL/IvorySQL
Length of output: 127
🏁 Script executed:
# Check if andlist is modified during the loop iterations that have break
sed -n '780,850p' src/backend/optimizer/plan/planner.c | grep -n "andlist"Repository: IvorySQL/IvorySQL
Length of output: 323
🏁 Script executed:
# Double-check the exact sequence: when is get_opname called vs when Const is validated?
sed -n '745,770p' src/backend/optimizer/plan/planner.c | cat -nRepository: IvorySQL/IvorySQL
Length of output: 836
🏁 Script executed:
# Verify all paths that call get_opname also free it before exiting
sed -n '719,997p' src/backend/optimizer/plan/planner.c | grep -B5 -A5 "pfree(opname)"Repository: IvorySQL/IvorySQL
Length of output: 1915
🏁 Script executed:
# Trace the exact control flow: get_opname is called when?
sed -n '745,760p' src/backend/optimizer/plan/planner.cRepository: IvorySQL/IvorySQL
Length of output: 370
🏁 Script executed:
# Count how many places pfree(opname) appears and in what contexts
grep -n "pfree(opname)" src/backend/optimizer/plan/planner.c | head -20Repository: IvorySQL/IvorySQL
Length of output: 327
Address the early loop exit that can miss subsequent ROWNUM predicates
The loop structure has one legitimate issue: the break statements in the <=, =, and < branches (after recording a transformable ROWNUM predicate) exit the foreach loop prematurely. This means that if a later ROWNUM predicate in the same AND-list requires an Oracle-semantic "always false" rewrite (like ROWNUM > 1 or ROWNUM = 2), it won't be examined.
For example, if the andlist contains [ROWNUM <= 10, ROWNUM > 1]:
- The first predicate matches the
<=branch, setslimit_value=10, and executesbreak. - The second predicate is never seen, so the
>branch never executes to fold it toFALSE. - The query incorrectly proceeds with both conditions instead of being marked unsatisfiable.
Replace the break statements in the <=, =, and < branches with continue so the loop scans all remaining ROWNUM predicates in the AND-list.
Note: The get_opname() function is called after validating the right operand is a non-null integer Const, and all code paths properly free it before continuing or returning, so there is no memory leak.
🤖 Prompt for AI Agents
In @src/backend/optimizer/plan/planner.c around lines 719 - 997, The loop over
ROWNUM clauses in the foreach scanning andlist exits early because the branches
handling opname "<=", "=", and "<" use break, which leaves later ROWNUM
predicates (e.g., ones that make the query always false) unexamined; change
those break statements to continue so the foreach loop keeps scanning all quals
(ensure you preserve setting limit_value and rownum_qual, and that opname is
still pfree'd on every path), i.e., in the branches that currently do "if
(can_use_limit) { limit_value = n; rownum_qual = qual; } pfree(opname); break;"
(and the "<" variant using n-1) replace the final break with continue so the
loop examines remaining quals in andlist.
|
Thanks for so quick fix! This is the procedure to reproduce problem:
call prepare_test_cursor();
FETCH LAST IN C1;
FETCH NEXT IN C1; |
|
A general comment: For example, create or replace function test_f(rownum in out varchar2) return integer is declare Another effect of specifying 'rownum' as keyword may be: select empno, ename, job rownum from eemp; -- should get a syntax error but successfully run with wrong name of third column(should be job). |
Documents bug where FETCH PRIOR/NEXT re-evaluates ROWNUM instead of using materialized values from original query. Ref: IvorySQL#1000 (comment)
For scroll cursors containing ROWNUM expressions, materialize all results into a tuplestore on first execution. This ensures FETCH PRIOR/NEXT/RELATIVE return the same ROWNUM values computed during initial scan, rather than re-evaluating (which caused incorrect incrementing values). Added plan_contains_rownum() to detect ROWNUM in plan trees, handling SubqueryScan, Append, and MergeAppend nodes. Fixes: IvorySQL#1000 (comment)
Documents bug where FETCH PRIOR/NEXT re-evaluates ROWNUM instead of using materialized values from original query. Ref: IvorySQL#1000 (comment)
For scroll cursors containing ROWNUM expressions, materialize all results into a tuplestore on first execution. This ensures FETCH PRIOR/NEXT/RELATIVE return the same ROWNUM values computed during initial scan, rather than re-evaluating (which caused incorrect incrementing values). Added plan_contains_rownum() to detect ROWNUM in plan trees, handling SubqueryScan, Append, and MergeAppend nodes. Fixes: IvorySQL#1000 (comment)
Tests that FETCH PRIOR/NEXT preserve both outer_rn and inner_rn values when using scroll cursors with multiple ROWNUM nesting levels.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/oracle_test/regress/sql/rownum.sql (1)
577-612: Consider adding explicit test markers for known limitations.The INTERSECT/EXCEPT (lines 577-594) and LATERAL (lines 597-612) sections document known limitations where IvorySQL behavior differs from Oracle. While the comments are clear, consider adding visual markers like
-- KNOWN LIMITATION TESTat the start of each section to make it obvious these tests document current (non-Oracle) behavior rather than validate correct Oracle semantics.📝 Suggested enhancement for clarity
-- +-- KNOWN LIMITATION TEST -- INTERSECT and EXCEPT with ROWNUM -- NOTE: Oracle resets ROWNUM for each side of INTERSECT/EXCEPT independently.-- +-- KNOWN LIMITATION TEST -- LATERAL join with ROWNUM -- NOTE: Oracle resets ROWNUM for each outer row in LATERAL/CROSS APPLY.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
src/oracle_test/regress/expected/rownum.outis excluded by!**/*.out
📒 Files selected for processing (2)
src/backend/tcop/pquery.csrc/oracle_test/regress/sql/rownum.sql
🧰 Additional context used
🧬 Code graph analysis (1)
src/backend/tcop/pquery.c (2)
src/backend/utils/mmgr/portalmem.c (1)
PortalCreateHoldStore(330-361)src/backend/utils/sort/tuplestore.c (1)
tuplestore_rescan(1284-1314)
🔇 Additional comments (16)
src/oracle_test/regress/sql/rownum.sql (12)
1-40: LGTM! Clean test setup and basic ROWNUM coverage.The initial setup and basic ROWNUM tests provide a solid foundation. The test patterns correctly cover the simple cases that should trigger LIMIT optimization (ROWNUM <= N, = 1, < N).
659-663: LGTM! Oracle type usage is appropriate for compatibility testing.Using Oracle-specific types (NUMBER, VARCHAR2) in the oracle_test directory correctly validates IvorySQL's Oracle type compatibility.
680-712: LGTM! Critical pagination pattern test.This test validates the nested-subquery pagination fix mentioned in PR objectives (bigplaice's reported bug). The pattern and expected results are well-documented, making it an excellent regression test.
882-955: LGTM! Comprehensive scroll cursor regression tests.These tests validate the scroll cursor materialization fix (preserving ROWNUM values across FETCH PRIOR/NEXT/RELATIVE operations) mentioned in the PR objectives. Coverage is excellent with both simple and deeply nested ROWNUM scenarios.
790-879: LGTM! Thorough aggregate function testing.Excellent coverage of aggregate functions with ROWNUM, GROUP BY, and ORDER BY. These tests validate fixes for the reported aggregate issues (lines 799-817 test column order sensitivity, lines 825-879 test multi-layer aggregation). The variety of aggregate functions and nesting levels provides strong regression protection.
512-570: LGTM! Essential UNION ROWNUM reset tests.These tests validate the is_union flag functionality mentioned in PR objectives, ensuring each UNION branch maintains independent ROWNUM counters (Oracle behavior). Good coverage of both UNION ALL (Append) and UNION (MergeAppend + Unique) cases, plus multiple-branch scenarios.
321-381: LGTM! Comprehensive correlated subquery tests.Excellent validation of ROWNUM reset behavior in correlated subqueries (Issue #14 mentioned in PR objectives). Tests cover all critical scenarios: single, multiple, nested, and JOIN cases. This ensures the ExecSubPlan save/restore logic works correctly.
199-246: LGTM! Critical optimizer transformation verification.These EXPLAIN tests validate the planner's ROWNUM→LIMIT transformations mentioned in PR objectives. Particularly important are the Issue #12 tests (lines 237-245) ensuring the optimizer correctly avoids LIMIT transformation when same-level ORDER BY/DISTINCT/aggregation would change semantics.
615-641: LGTM! DML operations with ROWNUM validated.Tests cover DELETE and UPDATE with ROWNUM as mentioned in PR objectives. Coverage is sufficient for regression detection, with verification queries confirming correct behavior.
116-150: LGTM! Comprehensive edge case coverage.Excellent testing of Oracle ROWNUM semantics edge cases: tautologies (ROWNUM > 0), impossible conditions (ROWNUM = 2), negative values, and complex predicates. These validate both runtime behavior and optimizer handling.
1-962: Excellent comprehensive test suite for ROWNUM implementation.This test file provides outstanding coverage of the ROWNUM feature:
✅ Core functionality: Basic queries, ORDER BY, JOINs, subqueries
✅ Critical bug fixes: Pagination (bigplaice's nested-subquery bug), scroll cursors, aggregates, GROUP BY
✅ Oracle semantics: Edge cases, tautologies, impossible conditions
✅ PR objectives: UNION resets, correlated subquery resets, DML operations, optimizer transformations
✅ Known limitations: INTERSECT/EXCEPT and LATERAL documented with tests
✅ Verification: EXPLAIN tests confirm optimizer transformationsThe test organization is logical, well-commented, and includes verification queries. This will provide strong regression protection for the ROWNUM feature.
723-724: No action needed. The error case is already properly validated through the expected output file (rownum.outline 1232), which documents the expected ERROR message. PostgreSQL regression tests validate by comparing actual output against expected output, so the presence of the ERROR message in the expected file ensures the test will fail if the error doesn't occur.Likely an incorrect or invalid review comment.
src/backend/tcop/pquery.c (4)
25-26: LGTM!The added includes are necessary for the new ROWNUM detection and materialization logic:
nodes/nodeFuncs.hprovidesexpression_tree_walkernodes/plannodes.hprovides Plan node type definitions
152-188: LGTM - Materialization trade-off is necessary for correctness.The function correctly materializes scroll cursor results by:
- Running the query to completion
- Storing all tuples in a tuplestore
- Resetting the store for subsequent fetches
- Properly managing snapshots and receivers
This ensures ROWNUM values remain stable across FETCH PRIOR/NEXT operations. While full materialization can be expensive for large result sets, the tuplestore uses work_mem limits and can spill to disk, and this approach is necessary to preserve ROWNUM semantics for scroll cursors.
1021-1034: LGTM - Conditional materialization correctly integrated.The integration logic correctly detects scroll cursors with ROWNUM and triggers materialization on first execution:
- Checks are appropriately ordered with short-circuit on
holdStore == NULLplan_contains_rownumis only called once per cursor (before materialization)- Non-scroll cursors are unaffected, as forward-only fetches don't need materialization
- Subsequent fetches automatically use
RunFromStore(line 1070)The comment clearly explains the rationale for this approach.
69-150: No concerns identified; separate executor mechanisms correctly handle ROWNUM in InitPlan/SubPlan contexts.The
plan_contains_rownumfunction is correctly scoped to detect ROWNUM in the main plan's targetlist and child plans. ROWNUM in InitPlan/SubPlan lists is handled separately at execution time via the es_rownum save/restore mechanism (nodeSubplan.c lines 69, 101, 1122, 1284; nodeSubqueryscan.c lines 50, 58, 66, 67). Since InitPlan/SubPlan don't appear in targetlists, they don't affect the scroll cursor materialization decision made by this planner-time check.
Documents current IvorySQL behavior vs Oracle for ROWNUM: 1. ROWNUM as column/table alias (rownum.sql): - Oracle: Does NOT allow ROWNUM as alias (ERROR) - IvorySQL: Allows it (differs from Oracle) 2. ROWNUM as PL/iSQL parameter/variable (ora_plisql.sql): - Oracle: Allows, treats as regular identifier - IvorySQL: Creates function but treats 'rownum' as pseudocolumn Ref: IvorySQL#1000 (comment)
Add semantic validation to reject ROWNUM and ROWID as aliases in Oracle compatibility mode, matching Oracle's behavior: - Column alias: SELECT 1 rownum → ERROR - Column alias with AS: SELECT 1 AS rownum → ERROR - Table alias: SELECT * FROM t rownum → ERROR - Subquery alias: SELECT * FROM (SELECT 1) rownum → ERROR Validation added in: - parse_target.c:transformTargetEntry() for column aliases - parse_relation.c:addRangeTableEntry() for table aliases - parse_relation.c:addRangeTableEntryForSubquery() for subquery aliases
In PL/iSQL's pre-columnref hook, check if ROWNUM or ROWID identifiers resolve to declared variables. This prevents the SQL parser from converting them to pseudocolumn references, matching Oracle behavior where these can be used as variable/parameter names in PL/SQL. The fix adds special handling in plisql_pre_column_ref() to check ROWNUM/ROWID names even when resolve_option is not PLISQL_RESOLVE_VARIABLE, since the SQL parser would otherwise intercept them first.
Verify that Oracle-specific keywords (ROWNUM, ROWID, etc.) have no special meaning in PostgreSQL mode: - Can be used as column names - Can be used as column/table aliases - Can be used as function parameters and variables - Can be used as CTE names This ensures Oracle-specific functionality doesn't leak into PostgreSQL mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/oracle_test/regress/sql/rownum.sql (1)
980-985: Consider using CASCADE for dept table cleanup.The
depttable is used in JOIN tests earlier in the file. UsingCASCADEensures cleanup succeeds even if there are dependencies, matching the pattern used forrownum_test.Minor cleanup improvement
DROP TABLE rownum_test CASCADE; -DROP TABLE dept CASCADE; +DROP TABLE dept;Actually, looking again, both already use CASCADE which is correct. No change needed.
The cleanup is already correct with CASCADE on both tables.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
src/oracle_test/regress/expected/ora_plisql.outis excluded by!**/*.outsrc/oracle_test/regress/expected/rownum.outis excluded by!**/*.outsrc/test/regress/expected/ora_keywords_pg.outis excluded by!**/*.out
📒 Files selected for processing (7)
src/backend/parser/parse_relation.csrc/backend/parser/parse_target.csrc/oracle_test/regress/sql/ora_plisql.sqlsrc/oracle_test/regress/sql/rownum.sqlsrc/pl/plisql/src/pl_comp.csrc/test/regress/parallel_schedulesrc/test/regress/sql/ora_keywords_pg.sql
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-24T08:19:29.453Z
Learnt from: OreoYang
Repo: IvorySQL/IvorySQL PR: 931
File: src/backend/oracle_parser/ora_gram.y:0-0
Timestamp: 2025-10-24T08:19:29.453Z
Learning: In IvorySQL’s parser (src/backend/oracle_parser/ora_gram.y and related headers), downcase_identifier has the 4-argument signature:
extern char *downcase_identifier(const char *ident, int len, bool warn, bool truncate);
Applied to files:
src/backend/parser/parse_target.c
🧬 Code graph analysis (3)
src/backend/parser/parse_target.c (3)
src/backend/utils/error/elog.c (2)
errcode(853-864)errmsg(1070-1086)src/backend/parser/parse_node.c (1)
parser_errposition(108-123)src/backend/nodes/nodeFuncs.c (1)
exprLocation(1422-1857)
src/backend/parser/parse_relation.c (2)
src/backend/utils/error/elog.c (2)
errcode(853-864)errmsg(1070-1086)src/backend/parser/parse_node.c (1)
parser_errposition(108-123)
src/pl/plisql/src/pl_comp.c (1)
src/include/nodes/pg_list.h (1)
list_length(151-155)
🔇 Additional comments (13)
src/pl/plisql/src/pl_comp.c (2)
37-37: LGTM: Include added for Oracle compatibility checks.The include of
utils/ora_compatible.his necessary for thecompatible_dbandORA_PARSERchecks added below.
614-642: LGTM: Correct handling of ROWNUM/ROWID as PL/iSQL variable names.The logic correctly allows ROWNUM and ROWID to be used as PL/iSQL variable/parameter names in Oracle compatibility mode by:
- Intercepting in the pre-columnref hook before the SQL parser converts them to pseudocolumns
- Attempting to resolve them as PL/iSQL variables first
- Only applying this special handling for unqualified single-field references in ORA_PARSER mode
- Falling back to normal parsing (pseudocolumn creation) if no variable is found
This matches Oracle's PL/SQL behavior where ROWNUM/ROWID can be used as variable names.
src/backend/parser/parse_target.c (2)
32-34: LGTM: Includes added for Oracle compatibility checks.The includes are necessary for the
compatible_dbandORA_PARSERchecks added below.
99-116: Table alias validation for ROWNUM/ROWID in Oracle mode is already properly implemented insrc/backend/parser/parse_relation.c(lines 1521-1533 for table aliases and lines 1746-1756 for subquery aliases), with comprehensive tests insrc/oracle_test/regress/sql/rownum.sql(lines 958-977) andsrc/test/regress/sql/ora_keywords_pg.sql(lines 29-33). The implementation is complete across all contexts required by the PR objectives.src/test/regress/sql/ora_keywords_pg.sql (1)
1-83: LGTM: Comprehensive PostgreSQL-mode keyword tests.This test file thoroughly validates that Oracle-specific keywords (ROWNUM, ROWID) have no special meaning in PostgreSQL mode and are treated as regular identifiers. The coverage includes:
- Column names and aliases (with and without AS)
- Table and subquery aliases
- Expressions and function parameters
- PL/pgSQL variables
- CTEs
- Proper cleanup
The tests complement the Oracle-mode tests by ensuring Oracle functionality doesn't leak into PostgreSQL mode.
src/test/regress/parallel_schedule (1)
144-145: LGTM: Test added to parallel schedule.The new test is appropriately placed at the end of the schedule with a clear comment describing its purpose.
src/backend/parser/parse_relation.c (2)
1516-1533: LGTM with a minor suggestion: consider using alias location for more precise error positioning.The error position uses
relation->locationwhich points to the relation name, not the alias. Using the alias's location (if available) would provide more accurate error positioning for the user. However, sinceAliasdoesn't have a location field,relation->locationis a reasonable fallback.
1741-1756: Missingparser_errpositionin subquery alias validation, but proposed fix references non-existent field.This block omits
parser_errposition()in the error reporting, making it inconsistent with the similar check inaddRangeTableEntry(lines 1524-1532). However, the proposed fix of usingparser_errposition(pstate, alias->location)is not feasible—theAliasstruct does not have alocationfield (it only containstype,aliasname, andcolnames).To address this, either:
- Add a
locationfield to theAliasstruct, or- Modify
addRangeTableEntryForSubqueryto accept and use a location parameter passed fromtransformRangeSubselectsrc/oracle_test/regress/sql/ora_plisql.sql (1)
1460-1497: Well-structured tests for ROWNUM as PL/iSQL identifier.The tests comprehensively cover:
- Function parameter named
rownum- Local variable named
rownum- Named parameter syntax (
rownum => 'value')Good defensive cleanup with
DROP FUNCTION IF EXISTS. The GitHub issue reference provides helpful context.src/oracle_test/regress/sql/rownum.sql (4)
1-24: LGTM: Test setup is clean and comprehensive.Good foundational test data with varied values for id, name, and value columns to support diverse test scenarios.
571-612: Excellent documentation of known limitations.The comments clearly explain the behavioral differences between Oracle and IvorySQL for INTERSECT/EXCEPT and LATERAL joins. This helps users understand expected behavior and provides a roadmap for future improvements. The "NOTE" sections effectively communicate that these are known limitations, not bugs.
880-955: Thorough scroll cursor testing for ROWNUM materialization.These tests verify that ROWNUM values are materialized and preserved across cursor movements (FETCH PRIOR/NEXT), which is critical for correct Oracle-compatible behavior. The nested ROWNUM cursor test adds extra coverage for multi-level scenarios.
956-978: Alias restriction tests align with parse_relation.c changes.These tests validate the new Oracle compatibility checks that reject ROWNUM/ROWID as column and table aliases. The error comments reference Oracle's actual error messages, which is helpful for verification.

Overview
ROWNUM is an Oracle pseudocolumn that assigns a unique number to each row as it is retrieved from a query. IvorySQL implements ROWNUM to provide Oracle compatibility.
Oracle ROWNUM Semantics
In Oracle:
Key Oracle Behaviors
IvorySQL Implementation
Architecture
The ROWNUM counter is stored in the executor state (
EState->es_rownum) and is managed during query execution.Key Source Files
src/include/nodes/execnodes.hes_rownumin EState,rownum_resetin SubqueryScanStatesrc/include/executor/execScan.hsrc/backend/executor/execExprInterp.csrc/backend/executor/nodeSubqueryscan.csrc/backend/executor/nodeSubplan.csrc/backend/executor/execUtils.cExecution Flow
Basic ROWNUM in WHERE Clause
For a query like
SELECT * FROM t WHERE ROWNUM <= 5:ROWNUM <= Nto aLimitnode (optimization)es_rownumROWNUM with Non-ROWNUM Filters
For a query like
SELECT ROWNUM, id FROM t WHERE id >= 5:This ensures ROWNUM is only assigned to rows that pass non-ROWNUM filters.
ROWNUM with ROWNUM Conditions
For a query like
SELECT * FROM t WHERE ROWNUM <= 5 AND id > 2:id > 2fails, es_rownum is revertedROWNUM <= 5fails (after 5 rows), execution continues but all subsequent rows failSubqueryScan with ORDER BY
For a query like
SELECT ROWNUM FROM (SELECT * FROM t ORDER BY value DESC) sub:Correlated Subqueries
For a query like:
Code Changes Summary
execScan.h - Pre-increment with Revert
nodeSubqueryscan.c - Reset on First Tuple
nodeSubplan.c - Save/Restore for Correlated Subqueries
Oracle Compatibility Test Results
All tests verified against Oracle Database 23.26 Free container.
Passing Tests (Match Oracle)
WHERE ROWNUM <= 5WHERE ROWNUM = 1WHERE ROWNUM < 4SELECT ROWNUM, id WHERE ROWNUM <= 3SELECT * FROM (... ORDER BY) WHERE ROWNUM <= 3WHERE ROWNUM > 0WHERE ROWNUM > 5WHERE ROWNUM = 2WHERE id >= 5WHERE ROWNUM <= 3 AND id >= 5SELECT COUNT(*) WHERE ROWNUM <= 5WHERE ROWNUM <= 5 ORDER BY value(SELECT ROWNUM FROM sub WHERE ...)UNION/UNION ALL ROWNUM Support
Oracle Behavior:
IvorySQL matches this behavior by resetting
es_rownumwhen the executor switches between UNION branches.Implementation
The UNION ROWNUM fix adds an
is_unionflag to both Append and MergeAppend nodes:AppendnodesMergeAppend+UniquenodesPlanner changes:
is_unionflag is set on AppendPath/MergeAppendPath nodes created for UNION operationspull_up_simple_union_alland detected inadd_paths_to_append_relby checking if all child rels havertekind == RTE_SUBQUERYExecutor changes:
ExecAppend: Resetses_rownumwhen switching to a new subplan (during iteration)ExecMergeAppend: Resetses_rownumbefore each child in the initialization loop (Sort nodes buffer all input upfront)Key files:
src/include/nodes/plannodes.his_unionto Append and MergeAppend structssrc/include/nodes/pathnodes.his_unionto AppendPath and MergeAppendPath structssrc/include/nodes/execnodes.has_is_unionto AppendState,ms_is_unionto MergeAppendStatesrc/backend/optimizer/util/pathnode.cis_union=falsein create functionssrc/backend/optimizer/prep/prepunion.cis_union=trueingenerate_union_pathssrc/backend/optimizer/path/allpaths.cis_union=truefor UNION ALL appendrelssrc/backend/optimizer/plan/createplan.cis_unionfrom path to plansrc/backend/executor/nodeAppend.ces_rownumon branch switchsrc/backend/executor/nodeMergeAppend.ces_rownumbefore each child in initMergeAppend Specifics
MergeAppend requires special handling because it initializes all children during
ExecInitMergeAppend:Each Sort child buffers all its input tuples during initialization, so ROWNUM must be reset before each child starts.
Performance Considerations
Pre-increment/Revert Overhead: Each filtered row requires an increment and a decrement. This is minimal overhead (two integer operations).
Optimizer Transformations: The planner transforms simple ROWNUM conditions to Limit nodes, avoiding the need for runtime ROWNUM checking in many cases.
SubqueryScan Reset: The
rownum_resetflag ensures the reset only happens once per scan, not per tuple.Testing
The ROWNUM implementation is tested via:
src/oracle_test/regress/sql/rownum.sql- Comprehensive test casessrc/oracle_test/regress/expected/rownum.out- Expected outputRun tests with:
cd src/oracle_test/regress make oracle-checkDML with ROWNUM
IvorySQL supports ROWNUM in DELETE and UPDATE statements, matching Oracle behavior:
Known Limitations
The following Oracle ROWNUM behaviors are not yet implemented in IvorySQL:
INTERSECT/EXCEPT ROWNUM Reset
Issue: #23
Oracle resets ROWNUM independently for each side of INTERSECT/EXCEPT operations. IvorySQL shares the counter across both sides.
Technical: INTERSECT/EXCEPT use
SetOpnodes with two child paths. The fix would require resettinges_rownumbefore executing the second child.LATERAL Subquery ROWNUM Reset
Issue: #24
Oracle resets ROWNUM for each outer row in LATERAL/CROSS APPLY subqueries. IvorySQL continues the counter across outer rows.
Technical: LATERAL subqueries are executed via
SubqueryScannodes. The existing reset logic may need to be extended for LATERAL joins.Future Work
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.