Skip to content

GH-55614: [C++][Gandiva] Build the expression cache key from the expression tree - #55608

Open
dmitry-chirkov-dremio wants to merge 3 commits into
apache:mainfrom
dmitry-chirkov-dremio:pr2-structural-cache-key
Open

dmitry-chirkov-dremio wants to merge 3 commits into
apache:mainfrom
dmitry-chirkov-dremio:pr2-structural-cache-key

Conversation

@dmitry-chirkov-dremio

@dmitry-chirkov-dremio dmitry-chirkov-dremio commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Rationale for this change

The cache key is built from Expression::ToString(). Different expressions can print the same text, so the second Projector::Make reuses the compiled code of the first. The result is wrong, with no error. Four cases on main:

  • a AND (b OR c) and (a AND b) OR c print the same text.
  • String literals are not escaped, so two IF expressions can print the same text.
  • The key has no output field type. An int64 output skips validation after an int32 one was compiled, and returns garbage.
  • The key ignores the nullability of the fields in the expression. A field node with the wrong nullability is rejected on the first Make, but accepted when a valid one was compiled before.

What changes are included in this PR?

The PR has the tests first, then the fix in expression_cache_key.h.

The fix builds the key with a visitor over the expression tree, with the output field type. It is a compact binary form. Every node starts with a tag, tokens carry their length, and child lists carry their count. Types and names that repeat are written once and then as a short reference. It also sorts IN lists and finds like by node, not by text. The key handles null child nodes, so it no longer crashes before validation.

Are these changes tested?

Yes. 15 new tests: key unit tests in cache_test.cc, and end-to-end tests for Projector and Filter in projector_test.cc, filter_test.cc, boolean_expr_test.cc, if_expr_test.cc and projector_build_validation_test.cc. They cover the cases above, IN value lists, type parameters, floating point literals, null child nodes, and two schemas that differ only in nullability.

On main, 12 of them fail (one crashes with SIGSEGV). Three pass on both keys: FloatingPointBitsAndNulls, because float and double literals already print their bits, and the two tests for schemas that differ only in nullability, which record current behavior.

See the comments for the time to build the key and its size.

Are there any user-facing changes?

No.

This PR contains a "Critical Fix". It fixes a bug that returned incorrect data without an error.

Was AI used for this PR?

AI tools helped investigate the bug and write the fix, tests, and description.

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

The tests cover Boolean AND/OR structure, string literal boundaries, IN value
lists, the output field type and field nullability, and the compact forms of
type parameters and floating point literals. Most of them fail on main because
different expressions get the same cache key. The fix is in the next commit.
…ession tree

The cache key was built from Expression::ToString(), so different expressions
could print the same text and share one compiled entry. Build the key from the
expression tree and the output field type instead, with length-prefixed tokens
and child counts.
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

This pull request has been automatically converted to a draft because its title doesn't match Arrow's required format.

If this is not a minor PR, could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose

Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename the pull request title in the following format?

GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

After updating the title, you can mark the pull request as ready for review.

See also:

@dmitry-chirkov-dremio dmitry-chirkov-dremio changed the title [C++][Gandiva] Build the expression cache key from the expression tree [GH-TBD] [C++][Gandiva] Build the expression cache key from the expression tree Nov 14, 2026
@dmitry-chirkov-dremio

Copy link
Copy Markdown
Contributor Author

Benchmark of building the cache key.
The key is built on every Projector::Make, on a cache hit and on a miss, so this cost is paid each time.

Measured locally on an Apple M3 (macOS arm64), compiled with -O2. Each number is the median of repeated runs. The benchmark calls the key constructor only. It does not compile anything.

Expression Old time New time Old key size New key size
nested IF, 3 arms 0.003 ms 0.0009 ms 238 B 137 B
nested IF, 20 arms 0.021 ms 0.0027 ms 1.5 KB 0.7 KB
nested IF, 673 arms 2.44 ms 0.074 ms 51.6 KB 22.9 KB
nested IF, 5000 arms 71.5 ms 0.54 ms 393 KB 170 KB
flat OR, 100 KB 0.71 ms 0.11 ms 141 KB 121 KB
flat OR, 1 MB 7.2 ms 1.2 ms 1.43 MB 1.23 MB

What this shows:

  • Building the key is faster in every case. The old ToString() is quadratic on a deep chain.
  • The key is smaller in every case, so the cache does not use more memory than before. In the flat OR the size is mostly the string literals, which are stored as they are.

@dmitry-chirkov-dremio dmitry-chirkov-dremio changed the title [GH-TBD] [C++][Gandiva] Build the expression cache key from the expression tree GH-TBD: [C++][Gandiva] Build the expression cache key from the expression tree Nov 14, 2026
…ty and null nodes

Add tests for projectors and filters built from two schemas that differ only
in nullability, for a FieldNode whose nullability differs from the schema
field, and for null child nodes in the cache key.

Two schemas that differ only in nullability already get separate cache
entries. A cached FieldNode with the wrong nullability was accepted on the
old key and is rejected now. A null child node crashed the old key.
@dmitry-chirkov-dremio dmitry-chirkov-dremio changed the title GH-TBD: [C++][Gandiva] Build the expression cache key from the expression tree GH-55614: [C++][Gandiva] Build the expression cache key from the expression tree Nov 14, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #55614 has been automatically assigned in GitHub to PR creator.

@dmitry-chirkov-dremio

Copy link
Copy Markdown
Contributor Author

CI results for the new tests.

Tests-only commit, before the fix: 10 of the 11 new tests fail, as predicted. gandiva-internals-test and gandiva-projector-test fail on all four platforms (ARM64 Ubuntu 22.04 LLVM 18.1.8, AMD64 Ubuntu 24.04 ASAN UBSAN LLVM 18.1.3, AMD64 macOS 15-intel LLVM 18.1.8, ARM64 macOS 26 LLVM 23.1.4). The same tests fail everywhere:

  • TestExpressionCacheKey: IfLiteralBoundaries, OutputTypeAndAlias, InSetsAreDelimitedAndOrdered, BooleanStructure, LikeThreadUniquifier, TypeParametersAreKept, RepeatedNamesAndTypesStayDistinct
  • TestBooleanExpr.CacheKeepsAndOrStructure
  • TestIfExpr.CacheKeepsStringLiteralBoundaries
  • ValidationTestProjector.TestCachedOutputTypeValidation

None of them crash. They fail on an expectation, for example two different expressions with the same key, or wrong output after a cache hit.

TestExpressionCacheKey.FloatingPointBitsAndNulls passes on main, because float and double literals already print raw bits. It guards the new key.

With the fix commit, both test binaries pass on all four platforms.

I added four more regression tests after that, checked locally on the old key:

  • ValidationTestProjector.TestCachedFieldNullabilityValidation fails on the old key. A field node with the wrong nullability is rejected on the first Make and accepted after a valid one was cached.
  • TestExpressionCacheKey.NullChildNodes crashes the old key with SIGSEGV.
  • TestProjector.TestProjectCacheSchemasDifferingInNullability and TestFilter.TestCacheSchemasDifferingInNullability pass on both keys. Schemas that differ only in nullability already get separate cache entries. They record that behavior.

On the current head (cb1781d33e), the Gandiva tests pass on all four platforms.

A few known failures seen previously also appeared. They are not related to this PR.

@dmitry-chirkov-dremio
dmitry-chirkov-dremio marked this pull request as ready for review November 14, 2026 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant