Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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<TestCase> 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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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.
*
* <p>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<TestCase> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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<Case> 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<TestCase> 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());
}
}
}
}
Loading