From 9bdf721c4cf364f77c45f225e6458b4c2c61efb9 Mon Sep 17 00:00:00 2001 From: Sreekanth Vadigi Date: Fri, 3 Jul 2026 13:06:00 +0000 Subject: [PATCH] Add specialized negative suites: truncated / resultset / async (PR 6/8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The final batch of negative suites — row-limit rejection, ResultSet misuse, and the Databricks async extension API. - NEGATIVE_STATEMENT_SELECT_TRUNCATED: invalid setMaxRows/setLargeMaxRows values (negative, MIN_VALUE) — the set-then-execute sequence is captured since the failure may originate at either point. - NEGATIVE_RESULTSET: ResultSet misuse — next()/getObject() after the ResultSet or Statement is closed, and an out-of-range column index. The originally-planned CloudFetch link-expiry / chunk-download failure is intentionally omitted (not deterministically reproducible from a client test; would need server-side fault injection) — documented in the provider. - NEGATIVE_ASYNC: Databricks async extension via stmt.unwrap(IDatabricksStatement.class) — getExecutionResult() before any async execution, executeAsync() on invalid SQL (polls getExecutionResult so the async failure is actually captured, not the successful submit), and getExecutionResult() after the statement is closed. Validated live against the peco serverless warehouse (Thrift-vs-SEA, shadow): positive STATEMENT_SELECT runs in the same JVM and still PASSes. Each case was probe-verified to hit a real error path (setMaxRows(-1) -> ValidationException, getExecutionResult-before-exec -> "No execution available", after-close -> "ResultSet is closed"); the all-PASS cases are genuine agreement, not vacuous. getObject(999) surfaced a real 3-field mismatch. Also caught and fixed a vacuous first cut of the async invalid-SQL case that captured only the submit. ErrorComparatorTest: 20/20. Isaac Review: 0 findings. NO_CHANGELOG=true (test-only comparator tooling). Co-authored-by: Isaac Signed-off-by: Sreekanth Vadigi --- .../jdbc/comparator/COMPARATOR_README.md | 3 + .../jdbc/comparator/config/TestSuite.java | 7 +- .../suite/NegativeAsyncProvider.java | 92 ++++++++++++++++ .../suite/NegativeResultSetProvider.java | 100 ++++++++++++++++++ ...ativeStatementSelectTruncatedProvider.java | 94 ++++++++++++++++ 5 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/databricks/jdbc/comparator/suite/NegativeAsyncProvider.java create mode 100644 src/test/java/com/databricks/jdbc/comparator/suite/NegativeResultSetProvider.java create mode 100644 src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementSelectTruncatedProvider.java diff --git a/src/test/java/com/databricks/jdbc/comparator/COMPARATOR_README.md b/src/test/java/com/databricks/jdbc/comparator/COMPARATOR_README.md index 4a172f690..323b86ba3 100644 --- a/src/test/java/com/databricks/jdbc/comparator/COMPARATOR_README.md +++ b/src/test/java/com/databricks/jdbc/comparator/COMPARATOR_README.md @@ -263,6 +263,9 @@ If both are present for a method, **runOnly takes precedence**: argument combina | `NEGATIVE_CONNECTION` | connect failures — bad token, unknown host, bad warehouse, bad ConnCatalog/ConnSchema (own broken connections) | | `NEGATIVE_CANCEL_TIMEOUT` | cancel() mid/after/double + setQueryTimeout(1) on a slow query (own fresh connections) | | `NEGATIVE_VOLUME` | GET/PUT/REMOVE failures + op-after-close (runs under the `VOLUME_OPERATIONS` config) | +| `NEGATIVE_STATEMENT_SELECT_TRUNCATED` | Invalid setMaxRows/setLargeMaxRows values (negative row limits) | +| `NEGATIVE_RESULTSET` | ResultSet misuse — next()/getObject() after close, out-of-range column (CloudFetch link-expiry not deterministically reproducible; see provider javadoc) | +| `NEGATIVE_ASYNC` | Databricks async extension — getExecutionResult before/after execute, executeAsync on invalid SQL | Negative suites compare each endpoint's **error behavior** (exception class, SQLState, vendor code, message) via the `ERROR_COMPARISON_MODE` gate (default `shadow`). See diff --git a/src/test/java/com/databricks/jdbc/comparator/config/TestSuite.java b/src/test/java/com/databricks/jdbc/comparator/config/TestSuite.java index 2dad8bd46..f5cd29422 100644 --- a/src/test/java/com/databricks/jdbc/comparator/config/TestSuite.java +++ b/src/test/java/com/databricks/jdbc/comparator/config/TestSuite.java @@ -46,7 +46,12 @@ public enum TestSuite { // VOLUME_OPERATIONS suite. NEGATIVE_CONNECTION(new NegativeConnectionProvider()), NEGATIVE_CANCEL_TIMEOUT(new NegativeCancelTimeoutProvider()), - NEGATIVE_VOLUME(new NegativeVolumeProvider()); + NEGATIVE_VOLUME(new NegativeVolumeProvider()), + + // Specialized negative suites — row-limit rejection, ResultSet misuse, async extension API. + NEGATIVE_STATEMENT_SELECT_TRUNCATED(new NegativeStatementSelectTruncatedProvider()), + NEGATIVE_RESULTSET(new NegativeResultSetProvider()), + NEGATIVE_ASYNC(new NegativeAsyncProvider()); private final SuiteProvider provider; diff --git a/src/test/java/com/databricks/jdbc/comparator/suite/NegativeAsyncProvider.java b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeAsyncProvider.java new file mode 100644 index 000000000..2e8db62dd --- /dev/null +++ b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeAsyncProvider.java @@ -0,0 +1,92 @@ +package com.databricks.jdbc.comparator.suite; + +import com.databricks.jdbc.api.IDatabricksStatement; +import com.databricks.jdbc.comparator.ComparisonResult; +import com.databricks.jdbc.comparator.error.CapturedOutcome; +import com.databricks.jdbc.comparator.error.Captures; +import com.databricks.jdbc.comparator.error.ErrorDiffs; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Arrays; +import java.util.List; + +/** + * Negative async-execution cases for the Databricks extension API ({@link IDatabricksStatement}): + * {@code getExecutionResult()} before any async execution, {@code executeAsync} on invalid SQL, and + * {@code getExecutionResult()} after the async statement was closed. Compares how the two endpoints + * surface each failure via {@link ErrorDiffs}. + * + *

Obtains the extension via {@code stmt.unwrap(IDatabricksStatement.class)}. Read-only; safe on + * the shared connections. The statement is closed in a {@code finally}. + */ +public class NegativeAsyncProvider implements SuiteProvider { + + private static final String TABLE = "comparator_tests.oss_jdbc_tests.test_result_set_types"; + + private static final String RESULT_BEFORE_EXEC = + "getExecutionResult() before any async execution"; + private static final String ASYNC_INVALID_SQL = "executeAsync() on invalid SQL"; + private static final String RESULT_AFTER_CLOSE = + "getExecutionResult() after the statement is closed"; + + @Override + public List getTestCases() { + return Arrays.asList( + new TestCase(RESULT_BEFORE_EXEC, RESULT_BEFORE_EXEC), + new TestCase(ASYNC_INVALID_SQL, ASYNC_INVALID_SQL), + new TestCase(RESULT_AFTER_CLOSE, RESULT_AFTER_CLOSE)); + } + + @Override + public ComparisonResult execute( + Connection conn1, Connection conn2, TestCase testCase, String label) throws Exception { + String id = testCase.getIdentifier(); + CapturedOutcome left = Captures.capture(() -> runCase(conn1, id)); + CapturedOutcome right = Captures.capture(() -> runCase(conn2, id)); + ComparisonResult result = new ComparisonResult(label, id, testCase.getArgs()); + ErrorDiffs.foldInto(result, left, right, "result ", ""); + return result; + } + + private Object runCase(Connection conn, String id) throws Exception { + Statement stmt = conn.createStatement(); + IDatabricksStatement async = stmt.unwrap(IDatabricksStatement.class); + try { + switch (id) { + case RESULT_BEFORE_EXEC: + return async.getExecutionResult(); // no async execution has been submitted + case ASYNC_INVALID_SQL: + { + // executeAsync only SUBMITS; the syntax error surfaces on result retrieval. Poll + // getExecutionResult and drain it so the async failure is actually captured (not the + // successful submit). + async.executeAsync("SELET 1 FROM " + TABLE); // syntax error + for (int i = 0; i < 60; i++) { + ResultSet rs = async.getExecutionResult(); // throws if the async query failed + if (rs.next()) { + return "returned a row"; // unexpected — no error + } + // No row yet; if the query is still running, wait and re-poll. + Thread.sleep(500); + } + return "no error after polling"; + } + case RESULT_AFTER_CLOSE: + { + async.executeAsync("SELECT id FROM " + TABLE + " LIMIT 1"); + stmt.close(); + return async.getExecutionResult(); // statement closed + } + default: + throw new IllegalArgumentException("Unknown case: " + id); + } + } finally { + try { + stmt.close(); + } catch (Exception ignored) { + // best-effort; some cases close it deliberately above + } + } + } +} diff --git a/src/test/java/com/databricks/jdbc/comparator/suite/NegativeResultSetProvider.java b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeResultSetProvider.java new file mode 100644 index 000000000..8aacf926a --- /dev/null +++ b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeResultSetProvider.java @@ -0,0 +1,100 @@ +package com.databricks.jdbc.comparator.suite; + +import com.databricks.jdbc.comparator.ComparisonResult; +import com.databricks.jdbc.comparator.error.CapturedOutcome; +import com.databricks.jdbc.comparator.error.Captures; +import com.databricks.jdbc.comparator.error.ErrorDiffs; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Arrays; +import java.util.List; + +/** + * Negative ResultSet-iteration cases — mis-using a ResultSet after it (or its statement) is closed, + * and reading a column out of range. Compares how the two endpoints surface each failure via {@link + * ErrorDiffs}. + * + *

Best-effort note: the originally-intended case — {@code next()} failing mid-iteration due to + * CloudFetch presigned-link expiry or a chunk-download failure — cannot be provoked + * deterministically from a client test (links are valid for the query's lifetime and expiry is + * time/infra dependent), so it is intentionally NOT included here. The cases below are the + * reliably-reproducible ResultSet misuse errors; CloudFetch-expiry comparison, if ever needed, + * would require server-side fault injection out of scope for this harness. + * + *

Read-only, safe on the shared connections. + */ +public class NegativeResultSetProvider implements SuiteProvider { + + private static final String TABLE = "comparator_tests.oss_jdbc_tests.test_result_set_types"; + + private static final String NEXT_AFTER_CLOSE = "next() after the ResultSet is closed"; + private static final String GET_AFTER_CLOSE = "getObject() after the ResultSet is closed"; + private static final String GET_AFTER_STMT_CLOSE = "next() after the Statement is closed"; + private static final String COLUMN_OUT_OF_RANGE = "getObject() on an out-of-range column index"; + + @Override + public List getTestCases() { + return Arrays.asList( + new TestCase(NEXT_AFTER_CLOSE, NEXT_AFTER_CLOSE), + new TestCase(GET_AFTER_CLOSE, GET_AFTER_CLOSE), + new TestCase(GET_AFTER_STMT_CLOSE, GET_AFTER_STMT_CLOSE), + new TestCase(COLUMN_OUT_OF_RANGE, COLUMN_OUT_OF_RANGE)); + } + + @Override + public ComparisonResult execute( + Connection conn1, Connection conn2, TestCase testCase, String label) throws Exception { + String id = testCase.getIdentifier(); + CapturedOutcome left = Captures.capture(() -> runCase(conn1, id)); + CapturedOutcome right = Captures.capture(() -> runCase(conn2, id)); + ComparisonResult result = new ComparisonResult(label, id, testCase.getArgs()); + ErrorDiffs.foldInto(result, left, right, "result ", ""); + return result; + } + + private Object runCase(Connection conn, String id) throws Exception { + switch (id) { + case NEXT_AFTER_CLOSE: + { + Statement s = conn.createStatement(); + ResultSet rs = s.executeQuery("SELECT id FROM " + TABLE + " LIMIT 1"); + rs.close(); + try { + return rs.next(); // ResultSet closed + } finally { + s.close(); + } + } + case GET_AFTER_CLOSE: + { + Statement s = conn.createStatement(); + ResultSet rs = s.executeQuery("SELECT id FROM " + TABLE + " LIMIT 1"); + rs.next(); + rs.close(); + try { + return rs.getObject(1); // ResultSet closed + } finally { + s.close(); + } + } + case GET_AFTER_STMT_CLOSE: + { + Statement s = conn.createStatement(); + ResultSet rs = s.executeQuery("SELECT id FROM " + TABLE + " LIMIT 1"); + s.close(); // closing the statement closes the ResultSet + return rs.next(); + } + case COLUMN_OUT_OF_RANGE: + { + try (Statement s = conn.createStatement(); + ResultSet rs = s.executeQuery("SELECT id FROM " + TABLE + " LIMIT 1")) { + rs.next(); + return rs.getObject(999); // out-of-range column index + } + } + default: + throw new IllegalArgumentException("Unknown case: " + id); + } + } +} diff --git a/src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementSelectTruncatedProvider.java b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementSelectTruncatedProvider.java new file mode 100644 index 000000000..bc4443f53 --- /dev/null +++ b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementSelectTruncatedProvider.java @@ -0,0 +1,94 @@ +package com.databricks.jdbc.comparator.suite; + +import com.databricks.jdbc.comparator.ComparisonResult; +import com.databricks.jdbc.comparator.error.CapturedOutcome; +import com.databricks.jdbc.comparator.error.Captures; +import com.databricks.jdbc.comparator.error.ErrorDiffs; +import java.sql.Connection; +import java.sql.Statement; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Negative row-limit cases — invalid {@code setMaxRows}/{@code setLargeMaxRows} values that should + * be rejected, per JDBC ({@code setMaxRows} requires a non-negative limit). Compares how the two + * endpoints surface the rejection (at the setter or at execute) via {@link ErrorDiffs}. + * + *

Read-only: sets a bad limit then runs a SELECT; safe on the shared connections. The whole + * set-then-execute sequence is captured per side since the failure may originate at either point. + */ +public class NegativeStatementSelectTruncatedProvider implements SuiteProvider { + + private static final String TABLE = "comparator_tests.oss_jdbc_tests.test_result_set_types"; + private static final String QUERY = "SELECT * FROM " + TABLE + " ORDER BY id LIMIT 100"; + + /** Applies a bad row-limit to a statement then runs the query; returns a ResultSet or throws. */ + @FunctionalInterface + private interface LimitAction { + Object run(Statement stmt) throws Exception; + } + + private static final class Case { + final String description; + final LimitAction action; + + Case(String description, LimitAction action) { + this.description = description; + this.action = action; + } + } + + private static final List CASES = + Arrays.asList( + new Case( + "setMaxRows(-1) — negative row limit", + stmt -> { + stmt.setMaxRows(-1); + return stmt.executeQuery(QUERY); + }), + new Case( + "setLargeMaxRows(-1) — negative large row limit", + stmt -> { + stmt.setLargeMaxRows(-1L); + return stmt.executeQuery(QUERY); + }), + new Case( + "setMaxRows(MIN_VALUE) — extreme negative row limit", + stmt -> { + stmt.setMaxRows(Integer.MIN_VALUE); + return stmt.executeQuery(QUERY); + })); + + @Override + public List getTestCases() { + return CASES.stream() + .map(c -> new TestCase(c.description, c.description)) + .collect(Collectors.toList()); + } + + @Override + public ComparisonResult execute( + Connection conn1, Connection conn2, TestCase testCase, String label) throws Exception { + Case c = + CASES.stream() + .filter(x -> x.description.equals(testCase.getIdentifier())) + .findFirst() + .orElseThrow( + () -> new IllegalArgumentException("Unknown case: " + testCase.getIdentifier())); + + try (Statement stmt1 = conn1.createStatement(); + Statement stmt2 = conn2.createStatement()) { + CapturedOutcome left = Captures.capture(() -> c.action.run(stmt1)); + CapturedOutcome right = Captures.capture(() -> c.action.run(stmt2)); + try { + ComparisonResult result = new ComparisonResult(label, c.description, testCase.getArgs()); + ErrorDiffs.foldInto(result, left, right, "result ", ""); + return result; + } finally { + Captures.closeIfResultSet(left.value()); + Captures.closeIfResultSet(right.value()); + } + } + } +}