Description
AbstractReadRel exposes both getFilter() and getBestEffortFilter(), but the four read-relation visits in RelCopyOnWriteVisitor rewrite only the first: visit(NamedScan), visit(LocalFiles), visit(VirtualTableScan) and visit(ExtensionTable) each compute visitOptionalExpression(rel.getFilter(), context), decide "unchanged" from that alone, and rebuild without ever visiting the best-effort filter. A rewrite that lands there is discarded, and because the visit then returns Optional.empty(), the traversal above the read collapses to "unchanged" as well. (getProjection() is a MaskExpression and carries no expressions, so there is nothing for the visitor to rewrite in it.)
This is the same shape as #1231 and the MultiBucketExchange expression fixed alongside it: a child that is never visited, and so cannot reach the allEmpty(...) decision. It is distinct from #1204, which is about isthmus dropping these fields when converting to and from Calcite rather than when rewriting.
Reproduction
Using the negateI32LiteralsVisitor helper already in RelCopyOnWriteVisitorTest, over the same predicate in both cases:
NamedScan base =
(NamedScan) sb.namedScan(Arrays.asList("t"), Arrays.asList("a"), Arrays.asList(R.I32));
Expression pred = sb.equal(sb.i32(5), sb.i32(5));
NamedScan.builder().from(base).filter(pred).build()
.accept(negateI32LiteralsVisitor(), EmptyVisitationContext.INSTANCE);
// => Optional[NamedScan{..., filter=equal:any_any(I32Literal{value=-5}, I32Literal{value=-5}), ...}]
NamedScan.builder().from(base).bestEffortFilter(pred).build()
.accept(negateI32LiteralsVisitor(), EmptyVisitationContext.INSTANCE);
// => Optional.empty
Measured on main at 944b921 — pre-existing, not a regression.
Suggested fix
Visit getBestEffortFilter() alongside getFilter() in all four read-relation visits, fold it into the allEmpty(...) check and set it on the builder — the shape those methods already use for filter.
🤖 Generated with AI
Description
AbstractReadRelexposes bothgetFilter()andgetBestEffortFilter(), but the four read-relation visits inRelCopyOnWriteVisitorrewrite only the first:visit(NamedScan),visit(LocalFiles),visit(VirtualTableScan)andvisit(ExtensionTable)each computevisitOptionalExpression(rel.getFilter(), context), decide "unchanged" from that alone, and rebuild without ever visiting the best-effort filter. A rewrite that lands there is discarded, and because the visit then returnsOptional.empty(), the traversal above the read collapses to "unchanged" as well. (getProjection()is aMaskExpressionand carries no expressions, so there is nothing for the visitor to rewrite in it.)This is the same shape as #1231 and the
MultiBucketExchangeexpression fixed alongside it: a child that is never visited, and so cannot reach theallEmpty(...)decision. It is distinct from #1204, which is about isthmus dropping these fields when converting to and from Calcite rather than when rewriting.Reproduction
Using the
negateI32LiteralsVisitorhelper already inRelCopyOnWriteVisitorTest, over the same predicate in both cases:Measured on
mainat 944b921 — pre-existing, not a regression.Suggested fix
Visit
getBestEffortFilter()alongsidegetFilter()in all four read-relation visits, fold it into theallEmpty(...)check and set it on the builder — the shape those methods already use forfilter.🤖 Generated with AI