[CALCITE-7487] ProjectJoinTransposeRule throws ArrayIndexOutOfBoundsException in PushProjector when a Join input has a zero-column row type - #5125
Conversation
…xception in PushProjector when a Join input has a zero-column row type PushProjector.locateAllRefs contains a workaround, originally added for Fennel, that arbitrarily projects the first column of a Join or SetOp input when nothing else is projected from it. The workaround assumes the input has a first column to fall back on. That assumption does not hold for a zero-column input. The canonical example is DEE, the Values with an empty row type and a single empty tuple that is the identity for cross join; it arises when an Aggregate with GROUP BY () has its output pruned to zero columns. In that case the workaround sets a bit that points past the input's fields, and createProjectRefsAndExprs later uses that bit to index into an empty field list, throwing ArrayIndexOutOfBoundsException. Guard both the left and the right workaround on the corresponding input having at least one field, so that the workaround is skipped rather than producing an out-of-range reference. Each guard tests the field count of the input it protects: nFields for the left, nFieldsRight for the right. Both directions crash, so add a regression test for each.
patientstreetlight
left a comment
There was a problem hiding this comment.
Thank you for taking the time to look at this and create a fix!
I'm not a calcite committer, so I don't have any real authority to approve the change. But I still wanted to say I took a look and I think this change looks good.
You also asked to get a second opinion about whether pushing down a zero-column project is OK (i.e. whether it's fine to undo the Fennel fix for zero-column input) - this sounds fine to me. We only reach this case when the plan already has a 0-column section, so it seems fine to me to transform it to a different plan which has a different 0-column section. That said, I should mention that I am not a calcite expert, and I don't have a good understanding of the context around Fennel, so you may want to seek another opinion if you're looking for a more authoritative answer.
| // handle 0-column projections. | ||
| // | ||
| // An input may legitimately have a zero-column row type -- for | ||
| // example DEE, the empty-row-type Values that is the identity for |
| * DEE -- a {@link org.apache.calcite.rel.core.Values} with an empty row type, | ||
| * the identity for cross join. The project must be non-identity, otherwise | ||
| * {@link RelBuilder} collapses it away and the rule never fires. */ | ||
| private static RelNode zeroColumnJoinInputRelFn(RelBuilder b, |
There was a problem hiding this comment.
Can this be reproduced with a quidem test?
Not all plans that the builder can build can surface from a real program.
DEE is not used anywhere else in the codebase, so spell out what the relation is: a Values with an empty row type and a single empty tuple.
|
Thanks for looking. Both answered, and I've pushed a commit for the first. "What is DEE?" — fair challenge: the abbreviation appears nowhere else in the codebase, so it shouldn't be in a comment. It's the standard relational-algebra name for the relation with no attributes and one tuple (the identity for cross join; DUM is its empty counterpart), but I've replaced it with a plain description — "a Values with an empty row type and a single empty tuple, which returns one row with zero columns" — in "Can this be reproduced with a quidem test?" — I tried, and I don't think it can, which I should have said in the description rather than leaving you to ask. You're right that not every builder-constructible plan surfaces from a real program. I attempted the SQL routes to a zero-column join input: Both execute correctly. The aggregate keeps a column in the plan the validator produces, so the workaround always has a first column to fall back on and the crash never triggers. So the reachable route is via |
|
|
I think you can squash the commits for merging |



PushProjector.locateAllRefscontains a workaround, originally added for Fennel, that arbitrarily projects the first column of aJoin/SetOpinput when nothing else is projected from it:It assumes the input has a first column. That fails for a zero-column input — canonically DEE, the
Valueswith an empty row type and a single empty tuple that is the identity for cross join (it arises when anAggregatewithGROUP BY ()has its output pruned to zero columns). The workaround then sets a bit past the input's fields, andcreateProjectRefsAndExprsuses it to index into an empty field list:Fix
Guard each workaround on the input it protects having at least one field:
nFields > 0for the left,nFieldsRight > 0for the right. When there is genuinely no first column, the workaround is skipped instead of producing an out-of-range reference.Two deviations from the JIRA description, both verified
1. The left-side case is not merely latent — it crashes too. The ticket notes the symmetric
nProject == 0path "has the same latent issue" but that only the right-side variant had been seen in practice. Putting DEE on the left of the cross join throws the identical exception, so there is a regression test for each direction.2. The suggested left-side guard
(nSysFields + nFields) > 0would not be correct. Given the field layout documented onnFields:the left input has
nFieldsfields, so foldingnSysFieldsinto the condition lets the workaround through whennSysFields > 0 && nFields == 0.createProjectRefsAndExprsusesoffset = nSysFieldsfor the left side, sorefIdx - offsetwould then be negative — a different crash rather than a fix. HencenFields > 0.Note on the resulting plan
Skipping the workaround means the pushed-down projection over the zero-column input is itself zero-column:
That is precisely what the Fennel workaround existed to avoid. Fennel has long been removed, and zero-column relational expressions are legal in Calcite today — DEE itself is one — so this seems right. Flagging it explicitly in case anyone disagrees, since it is the behavioural part of this change.
Out of scope, but worth recording
projRefs.set(0)andprojRefs.set(nFields)look like they should beset(nSysFields)andset(nSysFields + nFields), since the left input's fields start after the system fields. Demonstrating that needs a Join that actually has system fields, and fixing it would change which column the workaround picks, so I have deliberately kept it out of this crash fix. Happy to file it separately if it is a real issue.The reporter also wondered whether
ProjectCorrelateTransposeRulehas the same latent issue. It does construct aPushProjector(ProjectCorrelateTransposeRule.java:79-80), but the workaround is gated onchildRel instanceof Join || childRel instanceof SetOp, andCorrelate extends BiRel— it is neither. So that rule never reaches this code and is unaffected.Tests
Both new tests fail on current
mainwith theArrayIndexOutOfBoundsExceptionabove and pass with the fix.RelOptRulesTestis green (923 tests). Golden plans were produced via the standard_actual.xmlmechanism.