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 6d52e2876..f9e5fa31c 100644
--- a/src/test/java/com/databricks/jdbc/comparator/COMPARATOR_README.md
+++ b/src/test/java/com/databricks/jdbc/comparator/COMPARATOR_README.md
@@ -250,4 +250,13 @@ If both are present for a method, **runOnly takes precedence**: argument combina
| `NULL_HANDLING` | wasNull() verification |
| `VOLUME_OPERATIONS` | UC Volume PUT / GET / DELETE |
| `DATABASE_METADATA` | All DatabaseMetaData methods (~13,500 argument combinations) |
+| `NEGATIVE_STATEMENT_SELECT` | Error-provoking SELECTs (missing table/column, syntax, cast, wrong method) |
+| `NEGATIVE_STATEMENT_OTHER` | Error-provoking SHOW / DESCRIBE / EXPLAIN / SET + JDBC-API misuse |
+| `NEGATIVE_PARAM_BINDING` | Bad PreparedStatement bindings (index, count, type, precision) |
+| `NEGATIVE_PREPARED_METADATA` | clearParameters + unbound execute; getMetaData on invalid SQL |
+| `NEGATIVE_TYPE_CONVERSION` | Incompatible ResultSet.getX() conversions (overflow, wrong target) |
+
+Negative suites compare each endpoint's **error behavior** (exception class, SQLState, vendor code,
+message) via the `ERROR_COMPARISON_MODE` gate (default `shadow`). See
+[`error/`](error/) for the comparison engine.
diff --git a/src/test/java/com/databricks/jdbc/comparator/config/Endpoint.java b/src/test/java/com/databricks/jdbc/comparator/config/Endpoint.java
index c22f9cbe5..5c076c4e2 100644
--- a/src/test/java/com/databricks/jdbc/comparator/config/Endpoint.java
+++ b/src/test/java/com/databricks/jdbc/comparator/config/Endpoint.java
@@ -9,8 +9,10 @@
*
*
* - {@code _HTTP_PATH} — full path, escape hatch.
- *
- {@code _CLUSTER} — {@code orgId:clusterId} → {@code /sql/protocolv1/o//}.
- *
- {@code _WAREHOUSE} — {@code } → {@code /sql/1.0/warehouses/}.
+ *
- {@code _CLUSTER} — {@code orgId:clusterId} → {@code
+ * /sql/protocolv1/o//}.
+ *
- {@code _WAREHOUSE} — {@code } → {@code
+ * /sql/1.0/warehouses/}.
*
*
* If none of the above is set on either side, callers should fall back to the legacy
@@ -95,7 +97,10 @@ private static String resolvePath(String prefix) {
throw new IllegalArgumentException(
prefix + "CLUSTER must be of the form orgId:clusterId, got: " + cluster);
}
- return "/sql/protocolv1/o/" + cluster.substring(0, colon) + "/" + cluster.substring(colon + 1);
+ return "/sql/protocolv1/o/"
+ + cluster.substring(0, colon)
+ + "/"
+ + cluster.substring(colon + 1);
}
String warehouse = System.getProperty(prefix + "WAREHOUSE");
@@ -114,8 +119,7 @@ private static String normalizeTransport(String transport) {
if (transport == null) return SEA;
String lower = transport.toLowerCase();
if (!SEA.equals(lower) && !THRIFT.equals(lower)) {
- throw new IllegalArgumentException(
- "transport must be 'sea' or 'thrift', got: " + transport);
+ throw new IllegalArgumentException("transport must be 'sea' or 'thrift', got: " + transport);
}
return lower;
}
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 5b95794ea..8c8b8b3c4 100644
--- a/src/test/java/com/databricks/jdbc/comparator/config/TestSuite.java
+++ b/src/test/java/com/databricks/jdbc/comparator/config/TestSuite.java
@@ -20,7 +20,14 @@ public enum TestSuite {
GEOSPATIAL(new GeospatialProvider()),
NULL_HANDLING(new NullHandlingProvider()),
VOLUME_OPERATIONS(new VolumeOperationsProvider()),
- DATABASE_METADATA(new DatabaseMetaDataProvider());
+ DATABASE_METADATA(new DatabaseMetaDataProvider()),
+
+ // Negative (error-provoking) suites — read-only, run on the shared connections.
+ NEGATIVE_STATEMENT_SELECT(new NegativeStatementSelectProvider()),
+ NEGATIVE_STATEMENT_OTHER(new NegativeStatementOtherProvider()),
+ NEGATIVE_PARAM_BINDING(new NegativeParamBindingProvider()),
+ NEGATIVE_PREPARED_METADATA(new NegativePreparedMetadataProvider()),
+ NEGATIVE_TYPE_CONVERSION(new NegativeTypeConversionProvider());
private final SuiteProvider provider;
diff --git a/src/test/java/com/databricks/jdbc/comparator/suite/NegativeParamBindingProvider.java b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeParamBindingProvider.java
new file mode 100644
index 000000000..6f001c98b
--- /dev/null
+++ b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeParamBindingProvider.java
@@ -0,0 +1,103 @@
+package com.databricks.jdbc.comparator.suite;
+
+import com.databricks.jdbc.comparator.ComparisonResult;
+import com.databricks.jdbc.comparator.ResultSetComparator;
+import com.databricks.jdbc.comparator.error.Captures;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Negative parameter-binding cases — bad {@code setXxx} usage or missing bindings that should fail
+ * at bind or execute time. The failure can originate at the setter or at {@code executeQuery}, so
+ * the whole prepare → set → execute sequence is captured per side as one unit and the endpoints'
+ * error behavior is compared.
+ *
+ *
Read-only: each case only reads (or fails before reading), so it is safe on the shared
+ * connections.
+ */
+public class NegativeParamBindingProvider implements SuiteProvider {
+
+ private static final String TABLE = "comparator_tests.oss_jdbc_tests.test_result_set_types";
+
+ /** A binding action that may throw at set or execute time; returns a ResultSet on success. */
+ @FunctionalInterface
+ private interface BindAndRun {
+ Object run(PreparedStatement ps) throws Exception;
+ }
+
+ private static final class Case {
+ final String description;
+ final String sql;
+ final BindAndRun action;
+
+ Case(String description, String sql, BindAndRun action) {
+ this.description = description;
+ this.sql = sql;
+ this.action = action;
+ }
+ }
+
+ private static final List CASES =
+ Arrays.asList(
+ new Case(
+ "setInt at out-of-range parameter index (99)",
+ "SELECT integer_column FROM " + TABLE + " WHERE integer_column = ?",
+ ps -> {
+ ps.setInt(99, 42);
+ return ps.executeQuery();
+ }),
+ new Case(
+ "Fewer parameters bound than placeholders",
+ "SELECT * FROM " + TABLE + " WHERE integer_column = ? AND bigint_column = ?",
+ ps -> {
+ ps.setInt(1, 42); // second placeholder left unbound
+ return ps.executeQuery();
+ }),
+ new Case(
+ "Type mismatch: setString into a numeric column",
+ "SELECT integer_column FROM " + TABLE + " WHERE integer_column = ?",
+ ps -> {
+ ps.setString(1, "not_a_number");
+ return ps.executeQuery();
+ }),
+ new Case(
+ "setBigDecimal exceeding column precision/scale",
+ "SELECT decimal_column FROM " + TABLE + " WHERE decimal_column = ?",
+ ps -> {
+ ps.setBigDecimal(1, new BigDecimal("123456789012345.678901234567890"));
+ return ps.executeQuery();
+ }));
+
+ @Override
+ public List getTestCases() {
+ return CASES.stream()
+ .map(c -> new TestCase(c.description, c.description))
+ .collect(java.util.stream.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 (PreparedStatement ps1 = conn1.prepareStatement(c.sql);
+ PreparedStatement ps2 = conn2.prepareStatement(c.sql)) {
+ Object r1 = Captures.resultOrThrowable(() -> c.action.run(ps1));
+ Object r2 = Captures.resultOrThrowable(() -> c.action.run(ps2));
+ try {
+ return ResultSetComparator.compare(label, c.description, testCase.getArgs(), r1, r2);
+ } finally {
+ Captures.closeIfResultSet(r1);
+ Captures.closeIfResultSet(r2);
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/databricks/jdbc/comparator/suite/NegativePreparedMetadataProvider.java b/src/test/java/com/databricks/jdbc/comparator/suite/NegativePreparedMetadataProvider.java
new file mode 100644
index 000000000..de961ae44
--- /dev/null
+++ b/src/test/java/com/databricks/jdbc/comparator/suite/NegativePreparedMetadataProvider.java
@@ -0,0 +1,91 @@
+package com.databricks.jdbc.comparator.suite;
+
+import com.databricks.jdbc.comparator.ComparisonResult;
+import com.databricks.jdbc.comparator.ResultSetComparator;
+import com.databricks.jdbc.comparator.error.Captures;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Negative prepared-statement metadata cases — {@code clearParameters()} then execute with a
+ * placeholder unbound, and {@code getMetaData()} before execute on invalid SQL / a missing table /
+ * a missing column. Each side's outcome (a metadata/result value or a thrown error) is compared.
+ *
+ * Read-only and safe on the shared connections.
+ */
+public class NegativePreparedMetadataProvider implements SuiteProvider {
+
+ private static final String TABLE = "comparator_tests.oss_jdbc_tests.test_result_set_types";
+
+ @FunctionalInterface
+ private interface PsAction {
+ Object run(PreparedStatement ps) throws Exception;
+ }
+
+ private static final class Case {
+ final String description;
+ final String sql;
+ final PsAction action;
+
+ Case(String description, String sql, PsAction action) {
+ this.description = description;
+ this.sql = sql;
+ this.action = action;
+ }
+ }
+
+ private static final List CASES =
+ Arrays.asList(
+ new Case(
+ "clearParameters() then execute with placeholder unbound",
+ "SELECT integer_column FROM " + TABLE + " WHERE integer_column = ?",
+ ps -> {
+ ps.setInt(1, 42);
+ ps.clearParameters();
+ return ps.executeQuery();
+ }),
+ new Case(
+ "getMetaData() before execute on invalid SQL",
+ "SELET integer_column FROM " + TABLE,
+ PreparedStatement::getMetaData),
+ new Case(
+ "getMetaData() before execute on a missing table",
+ "SELECT * FROM comparator_tests.oss_jdbc_tests.__no_such_table__",
+ PreparedStatement::getMetaData),
+ new Case(
+ "getMetaData() before execute on a missing column",
+ "SELECT __no_such_column__ FROM " + TABLE,
+ PreparedStatement::getMetaData));
+
+ @Override
+ public List getTestCases() {
+ return CASES.stream()
+ .map(c -> new TestCase(c.description, c.description))
+ .collect(java.util.stream.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 (PreparedStatement ps1 = conn1.prepareStatement(c.sql);
+ PreparedStatement ps2 = conn2.prepareStatement(c.sql)) {
+ Object r1 = Captures.resultOrThrowable(() -> c.action.run(ps1));
+ Object r2 = Captures.resultOrThrowable(() -> c.action.run(ps2));
+ try {
+ return ResultSetComparator.compare(label, c.description, testCase.getArgs(), r1, r2);
+ } finally {
+ Captures.closeIfResultSet(r1);
+ Captures.closeIfResultSet(r2);
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementOtherProvider.java b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementOtherProvider.java
new file mode 100644
index 000000000..3d1556c0d
--- /dev/null
+++ b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementOtherProvider.java
@@ -0,0 +1,112 @@
+package com.databricks.jdbc.comparator.suite;
+
+import com.databricks.jdbc.comparator.ComparisonResult;
+import com.databricks.jdbc.comparator.ResultSetComparator;
+import com.databricks.jdbc.comparator.error.Captures;
+import java.sql.Connection;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Negative utility-command cases — SHOW / DESCRIBE / EXPLAIN / SET against bad targets, plus a
+ * couple of JDBC-API misuse cases ({@code getMoreResults} after results are consumed, {@code
+ * getUpdateCount} before {@code execute}). Each side's outcome (value or thrown error) is compared.
+ *
+ * Read-only: provokes errors without mutating server or session state, so it is safe on the
+ * shared connections. {@code USE CATALOG/SCHEMA} is intentionally excluded (session-state mutation)
+ * and belongs to the connection-state suite.
+ */
+public class NegativeStatementOtherProvider implements SuiteProvider {
+
+ private static final String TABLE = "comparator_tests.oss_jdbc_tests.test_result_set_types";
+ private static final String GET_MORE_RESULTS = "GET_MORE_RESULTS_AFTER_CONSUMED";
+ private static final String GET_UPDATE_COUNT = "GET_UPDATE_COUNT_BEFORE_EXECUTE";
+
+ @Override
+ public List getTestCases() {
+ return Arrays.asList(
+ new TestCase(
+ "SHOW TABLES IN comparator_tests.__no_such_schema__",
+ "SHOW TABLES in a non-existent schema"),
+ new TestCase(
+ "SHOW SCHEMAS IN __no_such_catalog__", "SHOW SCHEMAS in a non-existent catalog"),
+ new TestCase(
+ "DESCRIBE TABLE comparator_tests.oss_jdbc_tests.__no_such_table__",
+ "DESCRIBE a non-existent table"),
+ new TestCase("SET = 'x'", "SET with an invalid (empty) parameter name"),
+ new TestCase(
+ "EXPLAIN SELECT __no_such_column__ FROM " + TABLE, "EXPLAIN of an invalid query"),
+ new TestCase(GET_MORE_RESULTS, "getMoreResults() after all results are consumed"),
+ new TestCase(GET_UPDATE_COUNT, "getUpdateCount() before execute()"));
+ }
+
+ @Override
+ public ComparisonResult execute(
+ Connection conn1, Connection conn2, TestCase testCase, String label) throws Exception {
+ String id = testCase.getIdentifier();
+ if (GET_MORE_RESULTS.equals(id)) {
+ return compareApi(
+ conn1, conn2, label, id, testCase, NegativeStatementOtherProvider::moreResults);
+ }
+ if (GET_UPDATE_COUNT.equals(id)) {
+ return compareApi(
+ conn1,
+ conn2,
+ label,
+ id,
+ testCase,
+ NegativeStatementOtherProvider::updateCountBeforeExecute);
+ }
+ // SQL command cases: capture execute() per side and compare.
+ try (Statement s1 = conn1.createStatement();
+ Statement s2 = conn2.createStatement()) {
+ Object r1 = Captures.resultOrThrowable(() -> s1.execute(id));
+ Object r2 = Captures.resultOrThrowable(() -> s2.execute(id));
+ return ResultSetComparator.compare(label, id, testCase.getArgs(), r1, r2);
+ }
+ }
+
+ /** Runs a per-connection API action on both sides and compares the captured outcomes. */
+ private ComparisonResult compareApi(
+ Connection conn1,
+ Connection conn2,
+ String label,
+ String id,
+ TestCase testCase,
+ ApiAction action) {
+ Object r1 = Captures.resultOrThrowable(() -> action.run(conn1));
+ Object r2 = Captures.resultOrThrowable(() -> action.run(conn2));
+ try {
+ return ResultSetComparator.compare(label, id, testCase.getArgs(), r1, r2);
+ } catch (Exception e) {
+ // ResultSetComparator only throws SQLException while comparing ResultSets; these API actions
+ // return a Boolean/Integer or a Throwable, so this is unreachable in practice.
+ throw new RuntimeException(e);
+ } finally {
+ Captures.closeIfResultSet(r1);
+ Captures.closeIfResultSet(r2);
+ }
+ }
+
+ @FunctionalInterface
+ private interface ApiAction {
+ Object run(Connection conn) throws Exception;
+ }
+
+ /** Consumes a small ResultSet fully, then calls getMoreResults() and returns its Boolean. */
+ private static Object moreResults(Connection conn) throws Exception {
+ try (Statement s = conn.createStatement()) {
+ s.executeQuery("SELECT 1");
+ // Drain is unnecessary for getMoreResults semantics; call it after the single result set.
+ return s.getMoreResults();
+ }
+ }
+
+ /** Calls getUpdateCount() on a fresh statement before any execute(). */
+ private static Object updateCountBeforeExecute(Connection conn) throws Exception {
+ try (Statement s = conn.createStatement()) {
+ return s.getUpdateCount();
+ }
+ }
+}
diff --git a/src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementSelectProvider.java b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementSelectProvider.java
new file mode 100644
index 000000000..f3c82ba29
--- /dev/null
+++ b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeStatementSelectProvider.java
@@ -0,0 +1,55 @@
+package com.databricks.jdbc.comparator.suite;
+
+import com.databricks.jdbc.comparator.ComparisonResult;
+import com.databricks.jdbc.comparator.ResultSetComparator;
+import com.databricks.jdbc.comparator.error.Captures;
+import java.sql.Connection;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Negative SELECT cases — inputs that should make {@code executeQuery} fail, so the two endpoints'
+ * error behavior (class, SQLState, code, message) is compared instead of their success output.
+ *
+ * Read-only: every case runs on the shared connections and provokes an error without mutating
+ * server state, so it cannot poison later suites.
+ */
+public class NegativeStatementSelectProvider implements SuiteProvider {
+
+ private static final String TABLE = "comparator_tests.oss_jdbc_tests.test_result_set_types";
+
+ @Override
+ public List getTestCases() {
+ return Arrays.asList(
+ new TestCase(
+ "SELECT * FROM comparator_tests.oss_jdbc_tests.__no_such_table__",
+ "SELECT from a non-existent table"),
+ new TestCase("SELET 1", "Syntax error (SELET typo)"),
+ new TestCase("SELECT 1/0", "Division by zero"),
+ new TestCase("SELECT CAST('x' AS INT)", "Runtime cast failure (non-numeric to INT)"),
+ new TestCase(
+ "SELECT __no_such_column__ FROM " + TABLE, "Reference to a non-existent column"),
+ // Wrong method: executeQuery on a statement that does not produce a ResultSet.
+ new TestCase(
+ "INSERT INTO " + TABLE + " (id) VALUES (1)",
+ "Wrong method: executeQuery on an INSERT"));
+ }
+
+ @Override
+ public ComparisonResult execute(
+ Connection conn1, Connection conn2, TestCase testCase, String label) throws Exception {
+ String query = testCase.getIdentifier();
+ try (Statement stmt1 = conn1.createStatement();
+ Statement stmt2 = conn2.createStatement()) {
+ Object r1 = Captures.resultOrThrowable(() -> stmt1.executeQuery(query));
+ Object r2 = Captures.resultOrThrowable(() -> stmt2.executeQuery(query));
+ try {
+ return ResultSetComparator.compare(label, query, testCase.getArgs(), r1, r2);
+ } finally {
+ Captures.closeIfResultSet(r1);
+ Captures.closeIfResultSet(r2);
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/databricks/jdbc/comparator/suite/NegativeTypeConversionProvider.java b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeTypeConversionProvider.java
new file mode 100644
index 000000000..525251341
--- /dev/null
+++ b/src/test/java/com/databricks/jdbc/comparator/suite/NegativeTypeConversionProvider.java
@@ -0,0 +1,114 @@
+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 java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Negative type-conversion cases — both endpoints return a row successfully, but a {@code
+ * ResultSet.getX()} call converts a cell to an incompatible target and may fail. Compares how each
+ * endpoint surfaces the conversion (a value, or a thrown error compared by class/SQLState/code).
+ *
+ * Read-only: only SELECTs and getters, safe on the shared connections. The query selects a
+ * single known row so both sides read identical cell values before the conversion is attempted.
+ */
+public class NegativeTypeConversionProvider implements SuiteProvider {
+
+ private static final String TABLE = "comparator_tests.oss_jdbc_tests.test_result_set_types";
+
+ /** Reads column 1 of the first row via a getter that may fail to convert. */
+ @FunctionalInterface
+ private interface Getter {
+ Object get(ResultSet rs) throws Exception;
+ }
+
+ private static final class Case {
+ final String description;
+ final String query;
+ final Getter getter;
+
+ Case(String description, String query, Getter getter) {
+ this.description = description;
+ this.query = query;
+ this.getter = getter;
+ }
+ }
+
+ private static final List CASES =
+ Arrays.asList(
+ new Case(
+ "getInt() on an out-of-int-range BIGINT",
+ "SELECT bigint_column FROM " + TABLE + " WHERE bigint_column > 3000000000 LIMIT 1",
+ rs -> rs.getInt(1)),
+ new Case(
+ "getByte() on an out-of-byte-range INTEGER",
+ "SELECT integer_column FROM " + TABLE + " WHERE integer_column > 1000 LIMIT 1",
+ rs -> rs.getByte(1)),
+ new Case(
+ "getInt() on a non-numeric VARCHAR",
+ "SELECT varchar_column FROM "
+ + TABLE
+ + " WHERE varchar_column IS NOT NULL AND varchar_column NOT RLIKE '^[0-9]+$'"
+ + " LIMIT 1",
+ rs -> rs.getInt(1)),
+ new Case(
+ "getObject(column, Integer.class) on a VARCHAR",
+ "SELECT varchar_column FROM " + TABLE + " WHERE varchar_column IS NOT NULL LIMIT 1",
+ rs -> rs.getObject(1, Integer.class)),
+ new Case(
+ "getInt() on an ARRAY column",
+ "SELECT array_column FROM " + TABLE + " WHERE array_column IS NOT NULL LIMIT 1",
+ rs -> rs.getInt(1)),
+ new Case(
+ "getBigDecimal() on a STRUCT column",
+ "SELECT struct_column FROM " + TABLE + " WHERE struct_column IS NOT NULL LIMIT 1",
+ rs -> rs.getBigDecimal(1)));
+
+ @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 s1 = conn1.createStatement();
+ Statement s2 = conn2.createStatement();
+ ResultSet rs1 = s1.executeQuery(c.query);
+ ResultSet rs2 = s2.executeQuery(c.query)) {
+ // Both queries must return the same shape; if a side has no row, the getter can't run — that
+ // is a harness/data problem, so let it propagate (fail loudly) rather than compare noise.
+ boolean has1 = rs1.next();
+ boolean has2 = rs2.next();
+ if (!has1 || !has2) {
+ throw new IllegalStateException(
+ "Type-conversion case '"
+ + c.description
+ + "' expected a row on both sides but got "
+ + "left="
+ + has1
+ + " right="
+ + has2);
+ }
+ CapturedOutcome left = Captures.capture(() -> c.getter.get(rs1));
+ CapturedOutcome right = Captures.capture(() -> c.getter.get(rs2));
+ return Captures.compareCall(
+ label, c.description, testCase.getArgs(), left, right, v -> "value " + v);
+ }
+ }
+}