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 @@ -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.

Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
*
* <ol>
* <li>{@code <SIDE>_HTTP_PATH} — full path, escape hatch.
* <li>{@code <SIDE>_CLUSTER} — {@code orgId:clusterId} → {@code /sql/protocolv1/o/<orgId>/<clusterId>}.
* <li>{@code <SIDE>_WAREHOUSE} — {@code <warehouseId>} → {@code /sql/1.0/warehouses/<warehouseId>}.
* <li>{@code <SIDE>_CLUSTER} — {@code orgId:clusterId} → {@code
* /sql/protocolv1/o/<orgId>/<clusterId>}.
* <li>{@code <SIDE>_WAREHOUSE} — {@code <warehouseId>} → {@code
* /sql/1.0/warehouses/<warehouseId>}.
* </ol>
*
* <p>If none of the above is set on either side, callers should fall back to the legacy
Expand Down Expand Up @@ -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");
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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