Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- Fixed `?` characters inside SQL comments, string literals, and quoted identifiers being incorrectly counted as parameter placeholders when `supportManyParameters=1`. `SQLInterpolator` now uses `SqlCommentParser` to locate only real placeholders. Fixes #1331.
- Fixed `MetadataOperationTimeout` not being applied when metadata operations use SHOW commands. Operations like `getTables`, `getSchemas`, and `getColumns` now respect the `MetadataOperationTimeout` connection property instead of hanging indefinitely with no timeout.
- Reclassify transient server errors to standard SQL states (08S01, 40001) across all Thrift error sites. This ensures UC unavailability and concurrent modification errors surface consistently for better retry handling. Note: Dashboards and branching logic keyed on legacy XXUCC or 42000 must be updated.
- Fixed client-side enforcement of `maxRows` limit. When `statement.setMaxRows()` is set, `ResultSet.next()` now returns false once the row limit is reached, even if the server returns more rows. Applies to all result types (Thrift, SEA, inline, CloudFetch).

---
*Note: When making changes, please add your change under the appropriate section
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ enum ResultSetType {
// for the lifetime of a result set.
private final TelemetryCollector cachedTelemetryCollector;

// Client-side maxRows enforcement. This provides a single, implementation-agnostic
// truncation point in next(). Per-implementation maxRows enforcement (e.g. in
// StreamingInlineArrowResult, StreamingColumnarResult) is retained as defense-in-depth.
private final long maxRowsLimit;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F5 — High] setMaxRows() called after executeQuery() is silently ignored 🟧

maxRowsLimit is final and captured at construction. PostgreSQL JDBC and major drivers read maxRows lazily on each next(). ORMs (Hibernate, JdbcTemplate) commonly call stmt.setMaxRows(N) after obtaining the ResultSet — and this PR cements snapshot-at-construction as canonical behavior.

Live-verified against PECO:

rs = stmt.executeQuery("SELECT * FROM range(10)");
stmt.setMaxRows(3);                  // called AFTER executeQuery
while (rs.next()) { ... }            // delivered 10 rows ❌  (Postgres: 3)

The pre-existing per-impl enforcement had the same flaw, but the PR was an opportunity to fix it for InlineJsonResult / ArrowStreamResult (the new paths).

Fix options (either is acceptable):

  1. Read parentStatement.getLargeMaxRows() lazily inside next() — one virtual call per row, negligible cost.
  2. Document the construction-time caching in the next() javadoc and tell users setMaxRows must be called before executeQuery.

code-review-squad (architecture + devils-advocate; live confirmed)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

JDBC spec says setMaxRows affects the next execution, not an already-open ResultSet.

private long rowsReturned = 0;
// Flag to bypass maxRows check during getUpdateCount() internal iteration,
// which calls next() to sum affected-row counts for DML statements.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F7 — Medium] countingUpdateRows mutable-flag back-channel pattern 🟨

Using a mutable instance field to alter the semantics of next() (set in getUpdateCount(), read here) entangles the two methods and makes the contract of next() depend on hidden state. It works today only because JDBC ResultSet is single-threaded; fragile to refactors (e.g. return inside the DML loop, future internal-iteration paths, etc.).

Fix: Extract a private nextRaw() (or nextInternalNoLimit()) helper containing only executionResult.next() + telemetry. Public next() checks maxRows then calls nextRaw(). getUpdateCount() calls nextRaw() directly — the flag and try/finally go away. Removes ~10 lines and one field, with no behavior change.

code-review-squad (language + maintainability + architecture)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Bigger change, can be done in followup PR

private boolean countingUpdateRows = false;

// Constructor for SEA result set
public DatabricksResultSet(
StatementStatus statementStatus,
Expand Down Expand Up @@ -121,6 +130,7 @@ public DatabricksResultSet(
this.updateCount = null;
this.parentStatement = parentStatement;
this.cachedTelemetryCollector = resolveTelemetryCollector(parentStatement);
this.maxRowsLimit = resolveMaxRowsLimit(parentStatement);
this.isClosed = false;
this.wasNull = false;
}
Expand All @@ -142,6 +152,7 @@ public DatabricksResultSet(
this.updateCount = null;
this.parentStatement = parentStatement;
this.cachedTelemetryCollector = resolveTelemetryCollector(parentStatement);
this.maxRowsLimit = resolveMaxRowsLimit(parentStatement);
this.isClosed = false;
this.wasNull = false;
this.complexDatatypeSupport = complexDatatypeSupport;
Expand Down Expand Up @@ -189,6 +200,7 @@ public DatabricksResultSet(
this.updateCount = null;
this.parentStatement = parentStatement;
this.cachedTelemetryCollector = resolveTelemetryCollector(parentStatement);
this.maxRowsLimit = resolveMaxRowsLimit(parentStatement);
this.isClosed = false;
this.wasNull = false;
}
Expand Down Expand Up @@ -220,6 +232,7 @@ public DatabricksResultSet(
this.updateCount = null;
this.parentStatement = null;
this.cachedTelemetryCollector = null;
this.maxRowsLimit = 0;
this.isClosed = false;
this.wasNull = false;
}
Expand Down Expand Up @@ -251,6 +264,7 @@ public DatabricksResultSet(
this.updateCount = null;
this.parentStatement = null;
this.cachedTelemetryCollector = null;
this.maxRowsLimit = 0;
this.isClosed = false;
this.wasNull = false;
}
Expand All @@ -271,14 +285,45 @@ public DatabricksResultSet(
this.updateCount = null;
this.parentStatement = null;
this.cachedTelemetryCollector = null;
this.maxRowsLimit = 0;
this.isClosed = false;
this.wasNull = false;
}

/**
* Advances the cursor to the next row of the result set. Returns {@code false} when no more rows
* are available or when the client-side {@code maxRows} limit has been reached.
*
* <p>{@code rowsReturned} tracks how many rows have been delivered to the caller through this
* ResultSet instance. It is only incremented during normal (non-DML-counting) iteration and
* assumes single-threaded access, which is the standard JDBC contract.
*
* @return {@code true} if the new current row is valid; {@code false} if there are no more rows
* @throws SQLException if the result set is closed or an error occurs
*/
@Override
public boolean next() throws SQLException {
checkIfClosed();
// Client-side maxRows truncation: stop before delegating to the underlying result
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F10 — Medium] "Defense-in-depth" comment is misleading 🟨

The comment claims per-implementation enforcement is retained as defense-in-depth. Verified by grep: InlineJsonResult and arrow/ArrowStreamResult contain zero maxRows references. Defense-in-depth presumes two independent layers that both work; one being known-absent isn't defense, it's the mechanism. The named classes (StreamingInlineArrowResult, StreamingColumnarResult) DO enforce it, but the list is also incomplete (LazyThriftInlineArrowResult is missing) and the two layers use divergent counting semantics (globalRowIndex + 1 >= maxRows int-typed vs rowsReturned >= maxRowsLimit long-typed).

Fix: Either (a) reword the comment to reflect reality — "central enforcement; some streaming impls also enforce locally", or (b) consider removing per-impl enforcement now that tier-1 covers all paths (after integration tests prove it). Single source of truth is cleaner.

code-review-squad (maintainability + architecture + devils-advocate)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated

// implementation when the limit has been reached. This is skipped during
// getUpdateCount() internal iteration (countingUpdateRows) to avoid breaking DML
// row counting. Individual result-set implementations (e.g. StreamingInlineArrowResult,
// StreamingColumnarResult) also enforce maxRows independently as defense-in-depth,
// so truncation still works even if this central check is somehow bypassed.
if (maxRowsLimit > 0 && rowsReturned >= maxRowsLimit && !countingUpdateRows) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F3 — Critical] isAfterLast() / isLast() / getRow() violate JDBC contract after client-side truncation 🟥

After client-side truncation, executionResult.getCurrentRow() stays at the last delivered row and executionResult.hasNext() may still return true. So isAfterLast() (this line) returns false even though next() just returned false. JDBC requires isAfterLast()==true after the cursor passes the last row. Hibernate / MyBatis / Spring JdbcTemplate pagination patterns commonly use isAfterLast() to detect EOF and will misbehave.

Live-verified against PECO:

stmt.setMaxRows(3);
rs = stmt.executeQuery("SELECT * FROM range(10)");
while (rs.next()) { /* 3 rows */ }
rs.isAfterLast();   // false ❌ (JDBC spec: true)
rs.isLast();        // true
rs.getRow();        // 3

This is a new contract divergence for InlineJsonResult and ArrowStreamResult (the two impls this PR adds central truncation for).

Fix: Track truncation explicitly. Add private boolean truncated; set to true when the limit is first hit in next(). Update isAfterLast():

public boolean isAfterLast() throws SQLException {
  checkIfClosed();
  return truncated || executionResult.getCurrentRow() >= resultSetMetaData.getTotalRows();
}

Similarly update isLast(). Add tests covering all three.

code-review-squad (consensus: architecture + devils-advocate; live confirmed)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

LOGGER.debug(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F9 — Medium] Truncation DEBUG log lacks statementId — un-correlatable in fleet logs 🟨

In a connection-pooled environment with thousands of concurrent queries, this log line can't be tied back to a specific statement. The statementId field is already on the instance.

Fix:

LOGGER.debug("maxRows limit ({}) reached for statement {}; returning false from next() after {} rows",
    maxRowsLimit, statementId, rowsReturned);

code-review-squad (ops)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

"maxRows limit ({}) reached; returning false from next() after {} rows",
maxRowsLimit,
rowsReturned);
return false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F1 — Critical] Result-set consumption telemetry is silently lost on every truncated query 🟥

The early return false on the maxRows path skips the cachedTelemetryCollector.recordResultSetIteration(..., hasNext=false) call at L327-330. ResultLatency.markResultSetConsumption(false) (and therefore resultSetConsumptionLatencyMillis) only fires when hasNext==false is reported. For every truncated query the consumption-end signal is silently dropped.

Live-verified via mitmproxy: ran setMaxRows(3) on range(100) against a real PECO warehouse and captured the telemetry POSTs to /telemetry-ext. Of 8 telemetry payloads, 7 had "result_latency":{} (empty) — only 1 contained result_set_consumption_latency_millis. Dashboards keyed on this metric will undercount for the exact queries the PR was designed to fix.

Fix: Call cachedTelemetryCollector.recordResultSetIteration(statementId.toSQLExecStatementId(), resultSetMetaData.getChunkCount(), false) before the early return, e.g.:

if (maxRowsLimit > 0 && rowsReturned >= maxRowsLimit && !countingUpdateRows) {
  LOGGER.debug("maxRows limit ({}) reached for statement {}; returning false from next() after {} rows",
      maxRowsLimit, statementId, rowsReturned);
  if (cachedTelemetryCollector != null) {
    cachedTelemetryCollector.recordResultSetIteration(
        statementId.toSQLExecStatementId(), resultSetMetaData.getChunkCount(), false);
  }
  return false;
}

— from code-review-squad (multi-reviewer consensus: ops, architecture, devils-advocate)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed

}
boolean hasNext = this.executionResult.next();
// Only count rows for customer iteration, not internal DML counting
// (getUpdateCount() sets countingUpdateRows=true to iterate over affected-row counts
// without inflating the user-visible row counter).
if (hasNext && !countingUpdateRows) {
rowsReturned++;
}
if (cachedTelemetryCollector != null) {
cachedTelemetryCollector.recordResultSetIteration(
statementId.toSQLExecStatementId(), resultSetMetaData.getChunkCount(), hasNext);
Expand Down Expand Up @@ -312,6 +357,17 @@ private static TelemetryCollector resolveTelemetryCollector(
return null;
}

private static long resolveMaxRowsLimit(IDatabricksStatementInternal parentStatement) {
try {
if (parentStatement != null) {
return parentStatement.getMaxRows();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F2 — Critical] setLargeMaxRows(>Integer.MAX_VALUE) silently disables enforcement 🟥

IDatabricksStatementInternal.getMaxRows() returns int. DatabricksStatement.getMaxRows() does return (int) maxRows; where maxRows is long and settable via setLargeMaxRows(long). This PR sources from getMaxRows() (int) and stores in long maxRowsLimit — so any setLargeMaxRows(>2^31) value wraps silently.

Live-verified against PECO warehouse:

stmt.setLargeMaxRows(4_294_967_300L);       // user-intended cap ~= 4.3B
stmt.getMaxRows();                          // returns 4 (int wrap)
stmt.getLargeMaxRows();                     // returns 4_294_967_300 (correct, but unused)
rs = stmt.executeQuery("SELECT * FROM range(20)");
rows delivered: 4  ❌  (user expected all 20, well under 4.3B)

getLargeMaxRows() already exists at DatabricksStatement.java:208 returning the correct long — it's just missing from the IDatabricksStatementInternal interface (line 16 declares only int getMaxRows()).

Fix:

  1. Add long getLargeMaxRows() throws DatabricksSQLException; to IDatabricksStatementInternal.
  2. Change resolveMaxRowsLimit to call parentStatement.getLargeMaxRows().
  3. Add a regression test that calls setLargeMaxRows(3_000_000_000L) and confirms enforcement still works.

code-review-squad (multi-reviewer consensus: 7/7 reviewers; live + wire confirmed)

}
} catch (Exception e) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F4 — High] resolveMaxRowsLimit silently swallows all exceptions at TRACE 🟧

catch (Exception e) { LOGGER.trace(...); return 0; } is fail-open on a correctness feature:

  • TRACE is off in production by default — ops gets zero signal.
  • Exception is overly broad — getMaxRows() only declares DatabricksSQLException, so an unchecked exception (NPE, IllegalStateException) is silently swallowed too.
  • Result: if anything goes wrong, the user's setMaxRows(N) is silently dropped and all rows are returned. The PR's claimed-fix can be invisibly disabled by any future bug or misconfiguration in getMaxRows().

Fix: Narrow the catch, log at WARN with statementId:

private static long resolveMaxRowsLimit(IDatabricksStatementInternal parentStatement, StatementId statementId) {
  if (parentStatement == null) return 0;
  try {
    return parentStatement.getLargeMaxRows();  // also fixes F2
  } catch (DatabricksSQLException e) {
    LOGGER.warn("Failed to resolve maxRows for statement {}; client-side truncation disabled", statementId, e);
    return 0;
  }
}

Let unchecked exceptions propagate — they indicate real bugs that should surface, not silent feature loss.

code-review-squad (5/7 reviewers)

LOGGER.trace("Error resolving maxRows limit: {}", e.getMessage());
}
return 0;
}

@Override
public boolean wasNull() throws SQLException {
checkIfClosed();
Expand Down Expand Up @@ -1949,8 +2005,13 @@ public long getUpdateCount() throws SQLException {
updateCount = 0L;
} else if (hasUpdateCount()) {
long rowsUpdated = 0;
while (next()) {
rowsUpdated += this.getLong(AFFECTED_ROWS_COUNT);
countingUpdateRows = true;
try {
while (next()) {
rowsUpdated += this.getLong(AFFECTED_ROWS_COUNT);
}
} finally {
countingUpdateRows = false;
}
updateCount = rowsUpdated;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.databricks.jdbc.api.ExecutionState;
Expand Down Expand Up @@ -1407,4 +1409,159 @@ void testTelemetryCollectorCachedOnceNotPerRow() throws SQLException {
TelemetryCollectorManager.getInstance().getOrCreateCollector(verifyContext);
assertNotNull(collector);
}

// --- maxRows truncation tests ---
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F6 — High] No test exercises the actual production paths the PR claims to fix 🟧

All 9 new tests mock IExecutionResult. The PR description specifically identifies InlineJsonResult and ArrowStreamResult as the bug — neither is exercised end-to-end. testGetUpdateCountBypassesMaxRows uses mock(InlineJsonResult.class) but stubs next() entirely.

Verified: grep shows zero maxRows references in InlineJsonResult.java or arrow/ArrowStreamResult.java — so the PR's central claim that maxRows now applies to those types is not validated by any test. A future regression in ExecutionResultFactory wiring (e.g., parentStatement not propagated) would silently break the fix and no test would fail.

Existing integration tests don't cover this either:

  • ExecutionIntegrationTests.java:395 (testStatement_MaxRows) is a getter/setter test that never iterates past the limit.
  • MultiChunkExecutionIntegrationTests.java:42 sets RowsFetchedPerBlock == maxRows so the new client-side logic never engages.

Fix: Add at least one fakeservice integration test that returns more rows than setMaxRows, parameterized across InlineJsonResult (JSON inline) and ArrowStreamResult (Arrow stream) so the production iteration path is exercised.

code-review-squad


private DatabricksResultSet getResultSetWithMaxRows(int maxRows, IExecutionResult executionResult)
throws Exception {
IDatabricksStatementInternal stmt = mock(IDatabricksStatementInternal.class);
when(stmt.getMaxRows()).thenReturn(maxRows);
return new DatabricksResultSet(
new StatementStatus().setState(StatementState.SUCCEEDED),
STATEMENT_ID,
StatementType.QUERY,
stmt,
executionResult,
mockedResultSetMetadata,
false);
}

@Test
void testNextRespectsMaxRows() throws Exception {
when(mockedExecutionResult.next()).thenReturn(true);
DatabricksResultSet resultSet = getResultSetWithMaxRows(3, mockedExecutionResult);

assertTrue(resultSet.next()); // row 1
assertTrue(resultSet.next()); // row 2
assertTrue(resultSet.next()); // row 3
assertFalse(resultSet.next()); // limit reached
assertFalse(resultSet.next()); // still false
}

@Test
void testNextMaxRowsZeroNoLimit() throws Exception {
// maxRows=0 means no limit; all 100 next() calls should succeed
when(mockedExecutionResult.next()).thenReturn(true);
DatabricksResultSet resultSet = getResultSetWithMaxRows(0, mockedExecutionResult);

for (int i = 0; i < 100; i++) {
assertTrue(resultSet.next(), "next() should return true at iteration " + i);
}
}

@Test
void testNextMaxRowsNullParentNoLimit() throws Exception {
// parentStatement=null means maxRowsLimit=0 (no limit)
when(mockedExecutionResult.next()).thenReturn(true);
DatabricksResultSet resultSet =
new DatabricksResultSet(
new StatementStatus().setState(StatementState.SUCCEEDED),
STATEMENT_ID,
StatementType.QUERY,
null, // null parentStatement
mockedExecutionResult,
mockedResultSetMetadata,
false);

for (int i = 0; i < 100; i++) {
assertTrue(resultSet.next(), "next() should return true at iteration " + i);
}
}

@Test
void testNextMaxRowsOneEdge() throws Exception {
when(mockedExecutionResult.next()).thenReturn(true);
DatabricksResultSet resultSet = getResultSetWithMaxRows(1, mockedExecutionResult);

assertTrue(resultSet.next()); // row 1
assertFalse(resultSet.next()); // limit reached
}

@Test
void testNextMaxRowsDoesNotCallExecutionResultAfterLimit() throws Exception {
when(mockedExecutionResult.next()).thenReturn(true);
DatabricksResultSet resultSet = getResultSetWithMaxRows(3, mockedExecutionResult);

resultSet.next(); // row 1
resultSet.next(); // row 2
resultSet.next(); // row 3
// These should NOT delegate to executionResult
resultSet.next();
resultSet.next();

// executionResult.next() should have been called exactly 3 times
verify(mockedExecutionResult, times(3)).next();
}

@Test
void testNextMaxRowsWithEmptyResultSet() throws Exception {
// maxRows > 0 but the underlying result set is empty; next() should return false immediately
when(mockedExecutionResult.next()).thenReturn(false);
DatabricksResultSet resultSet = getResultSetWithMaxRows(5, mockedExecutionResult);

assertFalse(resultSet.next(), "next() should return false on an empty result set");
assertFalse(
resultSet.next(),
"next() should still return false on subsequent calls to an empty result set");
// executionResult.next() should have been called because rowsReturned (0) < maxRows (5)
verify(mockedExecutionResult, times(2)).next();
}

@Test
void testNextMaxRowsGreaterThanActualRows() throws Exception {
// maxRows=10 but only 3 rows exist; result set should be naturally exhausted before the limit
when(mockedExecutionResult.next()).thenReturn(true, true, true, false);
DatabricksResultSet resultSet = getResultSetWithMaxRows(10, mockedExecutionResult);

assertTrue(resultSet.next()); // row 1
assertTrue(resultSet.next()); // row 2
assertTrue(resultSet.next()); // row 3
assertFalse(resultSet.next(), "next() should return false when data is exhausted before limit");
}

@Test
void testNextMaxRowsIdempotenceAfterLimit() throws Exception {
// Calling next() many times after the limit is reached should always return false
when(mockedExecutionResult.next()).thenReturn(true);
DatabricksResultSet resultSet = getResultSetWithMaxRows(2, mockedExecutionResult);

assertTrue(resultSet.next()); // row 1
assertTrue(resultSet.next()); // row 2 (limit reached)
// All subsequent calls must consistently return false (idempotent behavior)
for (int i = 0; i < 10; i++) {
assertFalse(resultSet.next(), "next() must return false on repeated call #" + (i + 1));
}
// executionResult.next() should have been called exactly 2 times (the limit)
verify(mockedExecutionResult, times(2)).next();
}

@Test
void testGetUpdateCountBypassesMaxRows() throws Exception {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[F8 — Medium] testGetUpdateCountBypassesMaxRows doesn't verify the flag is reset after iteration 🟨

The test asserts getUpdateCount() returns 5 — but never validates that countingUpdateRows is reset to false (via the finally block in getUpdateCount). A future refactor that moves the assignment outside the finally would silently break: subsequent next() calls on the same result set would bypass maxRows. This test would still pass.

Fix: Either (a) chain an additional next() call after getUpdateCount() returns and assert maxRows is enforced for it, or (b) add a separate test that throws inside the DML iteration (e.g., getLong throws) and asserts countingUpdateRows was still reset to false (i.e., maxRows still applies on subsequent calls).

code-review-squad (test)

// Mock an UPDATE result set with maxRows=2 but 5 affected rows across 5 result rows.
// getUpdateCount() should sum all 5 rows (returning 5), proving the
// countingUpdateRows flag bypasses the maxRows limit during internal iteration.
InlineJsonResult mockExec = mock(InlineJsonResult.class);
when(mockExec.next()).thenReturn(true, true, true, true, true, false);
when(mockExec.getObject(0)).thenReturn(1L, 1L, 1L, 1L, 1L);

DatabricksResultSetMetaData mockMeta = mock(DatabricksResultSetMetaData.class);
when(mockMeta.getColumnType(1)).thenReturn(Types.BIGINT);
when(mockMeta.getColumnNameIndex(AFFECTED_ROWS_COUNT)).thenReturn(1);

IDatabricksStatementInternal stmt = mock(IDatabricksStatementInternal.class);
when(stmt.getMaxRows()).thenReturn(2);

DatabricksResultSet resultSet =
new DatabricksResultSet(
new StatementStatus().setState(StatementState.SUCCEEDED),
STATEMENT_ID,
StatementType.UPDATE,
stmt,
mockExec,
mockMeta,
false);

// getUpdateCount() must iterate all 5 rows despite maxRows=2
assertEquals(5L, resultSet.getUpdateCount());
}
}
Loading