-
Notifications
You must be signed in to change notification settings - Fork 156
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
WalkthroughAdds Oracle ROWNUM support across parser, planner, and executor: new RownumExpr node and EEOP_ROWNUM opcode; optimizer transforms/rewrites ROWNUM predicates (including LIMIT conversion); executor tracks and manages es_rownum, resets across subplans/UNION branches, and materializes projections when needed. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant Parser
participant Optimizer
participant Executor
participant EState as EState(es_rownum)
Client->>Parser: Send query with ROWNUM
Parser->>Parser: Build RownumExpr node
Parser->>Optimizer: Pass query tree
Optimizer->>Optimizer: detect ROWNUM predicate
Optimizer->>Optimizer: transform_rownum_to_limit() or rewrite projection
Optimizer->>Executor: Emit plan (may include LIMIT / is_union flags)
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()
Executor->>Executor: If pi_needsMaterialization: ExecMaterializeSlot()
Executor->>Client: return row
else qual fails
Executor->>EState: es_rownum-- (revert)
end
end
sequenceDiagram
participant OuterExec
participant Subplan
participant EState as EState(es_rownum)
OuterExec->>EState: save es_rownum
OuterExec->>EState: reset es_rownum=0
OuterExec->>Subplan: execute subplan (correlated/init)
loop subplan tuples
Subplan->>EState: es_rownum++ / evaluate / emit
end
OuterExec->>EState: restore es_rownum
sequenceDiagram
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🎯 4 (Complex) | ⏱️ ~60 minutes Areas needing extra attention:
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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.
| /* | ||
| * contain_rownum_expr | ||
| * Check whether a node tree contains any ROWNUM expressions. | ||
| * | ||
| * This is used to detect when we need special handling for Oracle ROWNUM | ||
| * pseudocolumn in combination with Sort nodes. | ||
| */ | ||
| static bool | ||
| contain_rownum_expr_walker(Node *node, void *context) | ||
| { | ||
| if (node == NULL) | ||
| return false; | ||
|
|
||
| if (IsA(node, RownumExpr)) | ||
| return true; | ||
|
|
||
| return expression_tree_walker(node, contain_rownum_expr_walker, context); | ||
| } | ||
|
|
||
| static bool | ||
| contain_rownum_expr(Node *node) | ||
| { | ||
| return contain_rownum_expr_walker(node, NULL); | ||
| } | ||
|
|
||
| /* | ||
| * replace_rownum_expr_mutator | ||
| * Replace all RownumExpr nodes with corresponding Vars from context. | ||
| * | ||
| * This handles nested ROWNUM expressions within complex expressions, | ||
| * not just top-level RownumExpr in target entries. | ||
| */ | ||
| static Node * | ||
| replace_rownum_expr_mutator(Node *node, replace_rownum_context *context) | ||
| { | ||
| if (node == NULL) | ||
| return NULL; | ||
|
|
||
| if (IsA(node, RownumExpr)) | ||
| { | ||
| /* Replace with the next Var from our list */ | ||
| if (context->rownum_idx < list_length(context->rownum_vars)) | ||
| { | ||
| Var *replacement = (Var *) list_nth(context->rownum_vars, | ||
| context->rownum_idx); | ||
| context->rownum_idx++; | ||
| return (Node *) copyObject(replacement); | ||
| } | ||
| /* Should not happen if we counted correctly */ | ||
| elog(ERROR, "ran out of replacement Vars for ROWNUM expressions"); | ||
| } | ||
|
|
||
| return expression_tree_mutator(node, replace_rownum_expr_mutator, context); | ||
| } | ||
|
|
||
| /* | ||
| * count_rownum_exprs_walker | ||
| * Count the number of RownumExpr nodes in an expression tree. | ||
| */ | ||
| static bool | ||
| count_rownum_exprs_walker(Node *node, int *count) | ||
| { | ||
| if (node == NULL) | ||
| return false; | ||
|
|
||
| if (IsA(node, RownumExpr)) | ||
| { | ||
| (*count)++; | ||
| return false; /* Don't recurse into RownumExpr */ | ||
| } | ||
|
|
||
| return expression_tree_walker(node, count_rownum_exprs_walker, count); | ||
| } | ||
|
|
||
| /* | ||
| * count_rownum_exprs | ||
| * Count how many RownumExpr nodes are in an expression. | ||
| */ | ||
| static int | ||
| count_rownum_exprs(Node *node) | ||
| { | ||
| int count = 0; | ||
| count_rownum_exprs_walker(node, &count); | ||
| return count; | ||
| } | ||
|
|
||
| /* | ||
| * collect_rownum_exprs | ||
| * Collect all ROWNUM expressions from a target list. | ||
| * | ||
| * Returns a list of TargetEntry nodes that contain ROWNUM expressions | ||
| * (either top-level or nested). | ||
| */ | ||
| static List * | ||
| collect_rownum_exprs(List *tlist) | ||
| { | ||
| List *rownum_tles = NIL; | ||
| ListCell *lc; | ||
|
|
||
| foreach(lc, tlist) | ||
| { | ||
| TargetEntry *tle = lfirst_node(TargetEntry, lc); | ||
|
|
||
| if (contain_rownum_expr((Node *) tle->expr)) | ||
| rownum_tles = lappend(rownum_tles, tle); | ||
| } | ||
|
|
||
| return rownum_tles; | ||
| } |
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.
Fix walker callback signature for count_rownum_exprs_walker
The helper walkers are otherwise correct, but count_rownum_exprs_walker has a mismatched callback signature for expression_tree_walker. The walker framework expects bool (*)(Node *node, void *context), but the function is currently declared with int *count as the second parameter. That will at least trigger warnings, and on stricter builds can be a compile error.
You can fix this by switching the parameter type to void *context and casting inside:
-static bool
-count_rownum_exprs_walker(Node *node, int *count)
+static bool
+count_rownum_exprs_walker(Node *node, void *context)
{
- if (node == NULL)
- return false;
-
- if (IsA(node, RownumExpr))
- {
- (*count)++;
- return false; /* Don't recurse into RownumExpr */
- }
-
- return expression_tree_walker(node, count_rownum_exprs_walker, count);
+ int *count = (int *) context;
+
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, RownumExpr))
+ {
+ (*count)++;
+ return false; /* Don't recurse into RownumExpr */
+ }
+
+ return expression_tree_walker(node, count_rownum_exprs_walker, context);
}The rest of the ROWNUM helpers (contain_rownum_expr*, replace_rownum_expr_mutator, collect_rownum_exprs) are fine and consistent with existing walker/mutator patterns.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /* | |
| * contain_rownum_expr | |
| * Check whether a node tree contains any ROWNUM expressions. | |
| * | |
| * This is used to detect when we need special handling for Oracle ROWNUM | |
| * pseudocolumn in combination with Sort nodes. | |
| */ | |
| static bool | |
| contain_rownum_expr_walker(Node *node, void *context) | |
| { | |
| if (node == NULL) | |
| return false; | |
| if (IsA(node, RownumExpr)) | |
| return true; | |
| return expression_tree_walker(node, contain_rownum_expr_walker, context); | |
| } | |
| static bool | |
| contain_rownum_expr(Node *node) | |
| { | |
| return contain_rownum_expr_walker(node, NULL); | |
| } | |
| /* | |
| * replace_rownum_expr_mutator | |
| * Replace all RownumExpr nodes with corresponding Vars from context. | |
| * | |
| * This handles nested ROWNUM expressions within complex expressions, | |
| * not just top-level RownumExpr in target entries. | |
| */ | |
| static Node * | |
| replace_rownum_expr_mutator(Node *node, replace_rownum_context *context) | |
| { | |
| if (node == NULL) | |
| return NULL; | |
| if (IsA(node, RownumExpr)) | |
| { | |
| /* Replace with the next Var from our list */ | |
| if (context->rownum_idx < list_length(context->rownum_vars)) | |
| { | |
| Var *replacement = (Var *) list_nth(context->rownum_vars, | |
| context->rownum_idx); | |
| context->rownum_idx++; | |
| return (Node *) copyObject(replacement); | |
| } | |
| /* Should not happen if we counted correctly */ | |
| elog(ERROR, "ran out of replacement Vars for ROWNUM expressions"); | |
| } | |
| return expression_tree_mutator(node, replace_rownum_expr_mutator, context); | |
| } | |
| /* | |
| * count_rownum_exprs_walker | |
| * Count the number of RownumExpr nodes in an expression tree. | |
| */ | |
| static bool | |
| count_rownum_exprs_walker(Node *node, int *count) | |
| { | |
| if (node == NULL) | |
| return false; | |
| if (IsA(node, RownumExpr)) | |
| { | |
| (*count)++; | |
| return false; /* Don't recurse into RownumExpr */ | |
| } | |
| return expression_tree_walker(node, count_rownum_exprs_walker, count); | |
| } | |
| /* | |
| * count_rownum_exprs | |
| * Count how many RownumExpr nodes are in an expression. | |
| */ | |
| static int | |
| count_rownum_exprs(Node *node) | |
| { | |
| int count = 0; | |
| count_rownum_exprs_walker(node, &count); | |
| return count; | |
| } | |
| /* | |
| * collect_rownum_exprs | |
| * Collect all ROWNUM expressions from a target list. | |
| * | |
| * Returns a list of TargetEntry nodes that contain ROWNUM expressions | |
| * (either top-level or nested). | |
| */ | |
| static List * | |
| collect_rownum_exprs(List *tlist) | |
| { | |
| List *rownum_tles = NIL; | |
| ListCell *lc; | |
| foreach(lc, tlist) | |
| { | |
| TargetEntry *tle = lfirst_node(TargetEntry, lc); | |
| if (contain_rownum_expr((Node *) tle->expr)) | |
| rownum_tles = lappend(rownum_tles, tle); | |
| } | |
| return rownum_tles; | |
| } | |
| static bool | |
| count_rownum_exprs_walker(Node *node, void *context) | |
| { | |
| int *count = (int *) context; | |
| if (node == NULL) | |
| return false; | |
| if (IsA(node, RownumExpr)) | |
| { | |
| (*count)++; | |
| return false; /* Don't recurse into RownumExpr */ | |
| } | |
| return expression_tree_walker(node, count_rownum_exprs_walker, context); | |
| } | |
| static int | |
| count_rownum_exprs(Node *node) | |
| { | |
| int count = 0; | |
| count_rownum_exprs_walker(node, &count); | |
| return count; | |
| } |
🤖 Prompt for AI Agents
In src/backend/optimizer/plan/createplan.c around lines 7575-7683, the function
count_rownum_exprs_walker currently uses the wrong second parameter type (int
*count) for expression_tree_walker; change its signature to bool
count_rownum_exprs_walker(Node *node, void *context) and inside cast the context
to int *count = (int *) context, increment *count when encountering a
RownumExpr, and pass the same context through to expression_tree_walker; ensure
count_rownum_exprs continues to call the walker with &count as the context and
return the counted value.
| /* Create the LIMIT constant (INT8 is pass-by-value on 64-bit systems) */ | ||
| parse->limitCount = (Node *) makeConst(INT8OID, -1, InvalidOid, | ||
| sizeof(int64), | ||
| Int64GetDatum(limit_value), | ||
| false, true); |
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 look at the actual code in the file
cat -n src/backend/optimizer/plan/planner.c | sed -n '995,1015p'Repository: IvorySQL/IvorySQL
Length of output: 178
🏁 Script executed:
# Search for FLOAT8PASSBYVAL usage in the codebase
rg "FLOAT8PASSBYVAL" --type cRepository: IvorySQL/IvorySQL
Length of output: 178
🏁 Script executed:
# Search for other makeConst calls with INT8OID
rg "makeConst.*INT8OID" --type c -A 3 -B 1Repository: IvorySQL/IvorySQL
Length of output: 178
🏁 Script executed:
# Look for typbyval lookups in the codebase
rg "typbyval" --type c | head -20Repository: IvorySQL/IvorySQL
Length of output: 178
🏁 Script executed:
# Check if there are configuration headers that define FLOAT8PASSBYVAL
fd "config.h" --type f | head -5Repository: IvorySQL/IvorySQL
Length of output: 178
🌐 Web query:
PostgreSQL int8 pass-by-value FLOAT8PASSBYVAL Datum
💡 Result:
Short answer: On current PostgreSQL builds, int8 (bigint) is a pass-by-value 8-byte type (it is stored in a Datum and passed/returned by value). The historical compile-time option FLOAT8PASSBYVAL (and USE_FLOAT8_BYVAL) controlled whether 8‑byte types (float8, int8, timestamp) were passed by value; modern Postgres requires Datum to be wide enough and treats built‑in 8‑byte types as pass-by-value, so extensions should assume int8/float8 are passed by value. See the C-language type docs and the notes about FLOAT8/Datum sizing. [1][2][3]
References
- PostgreSQL C functions / base types (docs). [1]
- Recent source discussion / pg_config_manual: FLOAT8PASSBYVAL / USE_FLOAT8_BYVAL history. [2]
- CREATE TYPE / PASSEDBYVALUE note (Datum size requirement). [3]
🌐 Web query:
PostgreSQL makeConst INT8OID constbyval handling
💡 Result:
Short answer: makeConst sets constbyval from the type entry (typeByVal), so an INT8OID Const will have constbyval = true (i.e. passed by value) according to pg_type. See parse_coerce.c where makeConst assigns newcon->constbyval = typeByVal(baseType). [1][2]
Sources
- makeConst in parse_coerce.c (assigns constbyval = typeByVal(baseType)). [1]
- pg_type documentation (typbyval meaning; fixed-size 1/2/4/8 bytes can be pass-by-value). [2]
Use typeByVal() to determine INT8OID's constbyval instead of hardcoding true
The call hardcodes true for the constbyval parameter:
parse->limitCount = (Node *) makeConst(INT8OID, -1, InvalidOid,
sizeof(int64),
Int64GetDatum(limit_value),
false, true);While int8 is guaranteed pass-by-value on all modern PostgreSQL builds (and IvorySQL), the standard pattern is to look up the type's typbyval attribute via typeByVal() rather than hardcoding a literal. This ensures consistency with PostgreSQL conventions and makes the code more maintainable. Consider:
parse->limitCount = (Node *) makeConst(INT8OID, -1, InvalidOid,
sizeof(int64),
Int64GetDatum(limit_value),
false, typeByVal(INT8OID));🤖 Prompt for AI Agents
In src/backend/optimizer/plan/planner.c around lines 1002 to 1006, the makeConst
call hardcodes the const-by-value flag as true for INT8OID; replace the literal
with a lookup using typeByVal(INT8OID) so the constbyval is determined from the
type system (i.e., call typeByVal(INT8OID) for that parameter) to follow the
standard pattern and avoid hardcoding.
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
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.