Skip to content

Thrift: NPE on metadata-less empty FetchResults batch with hasMoreRows=true (should skip + refetch) (#1567) - #1589

Open
peco-engineer-bot[bot] wants to merge 4 commits into
mainfrom
ai/issue-1567
Open

Thrift: NPE on metadata-less empty FetchResults batch with hasMoreRows=true (should skip + refetch) (#1567)#1589
peco-engineer-bot[bot] wants to merge 4 commits into
mainfrom
ai/issue-1567

Conversation

@peco-engineer-bot

Copy link
Copy Markdown

Summary

Automated fix for #1567 — Thrift: NPE on metadata-less empty FetchResults batch with hasMoreRows=true (should skip + refetch).

Fixed the NPE by making DatabricksThriftAccessor.executeFetchRequest (the shared initial-fetch path for both execute() and getStatementResult()) loop: when a FetchResults response has null result-set metadata but hasMoreRows=true, it is treated as a legitimate intermediate empty batch, skipped, and FetchResults is re-issued until metadata/rows arrive — matching the reference C#/ADBC drivers. Verified via the two provided tests (now green) plus the full 57-test DatabricksThriftAccessorTest class. The bug is only reproducible by injecting the crafted metadata-less empty-batch response (per the issue, done through the databricks-driver-test mitmproxy harness / RESULTFETCH-009/010 spec cases); a real warehouse does not emit this sequence on demand, so the deterministic mocked-client tests are the faithful reproduction.

Root cause & plan

Root cause: On the Thrift result path, the INITIAL TFetchResultsResp that seeds a DatabricksResultSet is assumed to always carry result-set metadata. When the server returns an empty row set with hasMoreRows=true and getResultSetMetadata()==null (a legitimate intermediate empty batch), the driver dereferences the null metadata rather than re-fetching, throwing NPE. Dereference sites reached from the initial response: ExecutionResultFactory.getResultSet (resultsResp.getResultSetMetadata().isSetIsStagingOperation()), ExecutionResultFactory.getResultHandler (getResultSetMetadata().getResultFormat()), and the DatabricksResultSet Thrift constructor (getResultSetMetadata().getResultFormat() in the switch, and passing metadata to DatabricksResultSetMetaData). Subsequent-batch loops (LazyThriftResult, AbstractRemoteChunkProvider, convertColumnarToRowBased, streaming) already tolerate empty batches because they only read getResults()/hasMoreRows, never metadata. The correct behavior per the issue and the C#/ADBC reference drivers is to skip a metadata-less empty batch and issue the next FetchResults until metadata arrives (or hasMoreRows=false). The fix belongs in DatabricksThriftAccessor where the initial fetch response is produced (the execute post-poll fetch branch and getStatementResult).
["jdbc-core/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessor.java", "jdbc-core/src/test/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessorTest.java"]
Planned coverage:

  • In DatabricksThriftAccessorTest, drive accessor.execute with a mocked TCLIService.Client whose first FetchResults returns an empty row set with hasMoreRows=true and null resultSetMetadata, and the next FetchResults returns the real batch (metadata + rows). Before the fix this throws NullPointerException; after the fix the empty batch is skipped, a follow-up FetchResults is issued, and a DatabricksResultSet is returned (no NPE). (Single metadata-less empty batch with hasMoreRows=true (RESULTFETCH-009))
  • Same setup but the mock returns TWO consecutive metadata-less empty batches (each hasMoreRows=true) before the real batch. Asserts the driver keeps issuing FetchResults across multiple empty intermediate batches until metadata/rows arrive, surfacing the result set without NPE. (Multiple consecutive metadata-less empty batches (RESULTFETCH-010))

Files changed

  • src/test/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessorTest.java
  • src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessor.java

Test plan

  • com.databricks.jdbc.dbclient.impl.thrift.DatabricksThriftAccessorTest#testExecute_skipsMetadataLessEmptyBatch — fails (red) against the original code, passes (green) after the fix
  • com.databricks.jdbc.dbclient.impl.thrift.DatabricksThriftAccessorTest#testExecute_skipsMultipleConsecutiveMetadataLessEmptyBatches — fails (red) against the original code, passes (green) after the fix

NO_CHANGELOG=true

🤖 Generated by engineer-bot (bug-fix flow) — review before merge.

eric-wang-1990 and others added 4 commits July 29, 2026 15:58
The Engineer Bot's `Run author` step consumed its entire budget fighting
the offline Maven setup rather than solving the issue. Two root causes,
both reproduced from run 30479109185 (timed out at 45m after the agent
had a correct fix at ~14m):

1. `surefire-junit-platform` was never cached — the warmup build runs
   `-DskipTests`, so the test phase (which lazily resolves the JUnit
   Platform provider) never executes. After creds are scrubbed and Maven
   goes offline, the agent's `mvn test` can't fetch it → ~25 min of dead
   retries.
2. Cached artifacts came back "present, but unavailable" — their
   `_remote.repositories` markers record `jfrog-central`, a repo id absent
   from the agent's empty offline settings.

Fix (mirrors warmMavenCache.yml, which already solves both for forks):
- Add a "Warm test dependencies" step that test-compiles, resolves
  plugins, and explicitly fetches surefire-junit-platform while JFrog
  creds are still live.
- Normalize `_remote.repositories` (jfrog-central -> central) before the
  scrub so the warmed repo resolves cleanly offline.
- Bump the job timeout 45 -> 60 min and add a 45-min step timeout on
  `Run author` so a stuck agent fails the step (letting the outcome
  comment report a real failure) instead of the job being force-cancelled.

Co-authored-by: Isaac
Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
…ment

Two review-bot findings on PR #1588, both fixed:

- F1 (medium): the surefire-junit-platform resolution had no fallback,
  unlike the mirrored warmMavenCache.yml. Under `set -euo pipefail` a
  failed/empty `help:evaluate` substitution leaves SUREFIRE_VERSION empty,
  producing a malformed dependency:get coordinate silently swallowed by
  `|| true` — re-introducing the (absent) dead-end. Default to 3.1.2 when
  the expression can't be resolved.
- F2 (low): the timeout comment's stale "~4 min" setup figure ignored the
  two new pre-author steps. Rewrote it to reflect the real setup+warmup
  cost (~10 min) and the 60-45=15 min non-author budget.

Co-authored-by: Isaac
Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
Addresses review F1 (low, r3679000177): test-compile + the explicit
surefire-junit-platform get resolve the test classpath and provider jar,
but artifacts Surefire resolves LAZILY at execution time (JUnit-Platform
engine internals, provider transitives) only land in ~/.m2 once a real
JUnit-Platform run happens. Run the same fast test warmMavenCache.yml
uses (DatabricksParameterMetaDataTest#testInitialization) while creds are
live so the offline `mvn test` in Run author can't hit an execution-time
resolution miss. `|| true` — warming the repo is the goal, not the verdict.

Co-authored-by: Isaac
Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
…s=true (should skip + refetch) (#1567)

Signed-off-by: peco-engineer-bot[bot] <3815206+peco-engineer-bot[bot]@users.noreply.github.com>
@peco-engineer-bot peco-engineer-bot Bot added the engineer-bot Maintainer-only: triggers the autonomous engineer-bot (bug-fix author on issues; follow-up on PRs) label Jul 30, 2026
@peco-engineer-bot
peco-engineer-bot Bot requested a review from a team as a code owner July 30, 2026 01:11

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium · 1 Low

Targeted fix that loops FetchResults to skip metadata-less empty batches — correct in spirit and well-tested for the reported case. One medium concern: the new while (true) loop is unbounded and, unlike every other server-wait loop in this class, has no timeout guard, so a misbehaving server sequence hangs the thread forever. One low note on a residual NPE path when a metadata-less batch has hasMoreRows=false.

Other findings

  • 🟡 Medium — The new while (true) re-fetch loop has no iteration bound and no timeout guard. If the server keeps returning metadata-less empty batches with hasMoreRows=true (e.g. a buggy/looping backend, or an offset that never advances), the driver spins indefinitely, issuing FetchResults RPCs forever with no way to abort — the calling thread hangs and never surfaces an exception.

Every other loop in this class that waits on the server is bounded by a TimeoutHandler.checkTimeout() (see the polling loop at pollTillOperationFinished, line 341, and the metadata poll at line 794). This fetch loop is the one exception. Consider bounding the number of consecutive empty-batch skips, or wiring in the statement's query timeout via a TimeoutHandler, so a pathological server sequence fails cleanly instead of hanging.

  • 🔵 Low — The skip condition is metadata == null && hasMoreRows==true. A metadata-less batch with hasMoreRows==false (a final empty batch with no metadata) still falls through to return response, and the initial-fetch consumers described in the PR (ExecutionResultFactory.getResultSet/getResultHandler, the DatabricksResultSet Thrift constructor) will then dereference the null metadata and throw the same NPE this PR is fixing. If the server can emit such a terminal metadata-less batch, this gap remains uncovered by both the fix and the two new tests. Worth confirming that case cannot occur, or handling it explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engineer-bot Maintainer-only: triggers the autonomous engineer-bot (bug-fix author on issues; follow-up on PRs)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant