diff --git a/storm-core/src/main/java/st/orm/core/template/impl/DatabaseSchema.java b/storm-core/src/main/java/st/orm/core/template/impl/DatabaseSchema.java
index ae5b69862..8cac98442 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/DatabaseSchema.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/DatabaseSchema.java
@@ -23,12 +23,16 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
-import java.util.EnumSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import java.util.SortedMap;
+import java.util.SortedSet;
import java.util.TreeMap;
+import java.util.TreeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import st.orm.core.template.SqlDialect.ConstraintDiscoveryStrategy;
@@ -115,12 +119,15 @@ public record DbForeignKey(
/**
* A kind of constraint a schema read discovers.
*
- *
Each kind is read by one query (or one set of metadata calls) per strategy, so a failure applies to the
- * kind as a whole. See {@link #isDiscovered(ConstraintKind)} for why the outcome is recorded.
+ * The kinds are separate because a strategy can read one and fail on another: the JDBC metadata strategy
+ * asks for each with its own call. See {@link #isDiscovered(String, ConstraintKind)} for why the outcome is
+ * recorded.
*/
public enum ConstraintKind {
- /** Primary keys and unique keys, which every strategy reads together. */
- KEY,
+ /** Primary keys. */
+ PRIMARY_KEY,
+ /** Unique keys. */
+ UNIQUE_KEY,
/** Foreign keys. */
FOREIGN_KEY
}
@@ -131,7 +138,7 @@ public enum ConstraintKind {
private final SortedMap> uniqueKeysByTable;
private final SortedMap> foreignKeysByTable;
private final SortedMap sequences;
- private final Set discoveredConstraints;
+ private final Map> discoveredByKind;
private DatabaseSchema(
@Nonnull SortedMap> columnsByTable,
@@ -139,28 +146,45 @@ private DatabaseSchema(
@Nonnull SortedMap> uniqueKeysByTable,
@Nonnull SortedMap> foreignKeysByTable,
@Nonnull SortedMap sequences,
- @Nonnull Set discoveredConstraints
+ @Nonnull Map> discoveredByKind
) {
this.columnsByTable = columnsByTable;
this.primaryKeysByTable = primaryKeysByTable;
this.uniqueKeysByTable = uniqueKeysByTable;
this.foreignKeysByTable = foreignKeysByTable;
this.sequences = sequences;
- this.discoveredConstraints = discoveredConstraints;
+ this.discoveredByKind = discoveredByKind;
}
/**
- * Returns whether constraints of the given kind were actually read from the database.
+ * Returns whether constraints of the given kind were actually read for the given table.
*
- * A metadata query that fails leaves the corresponding map empty, which reads exactly like a schema that has
+ *
A metadata query that fails leaves the corresponding map empty, which reads exactly like a table that has
* no such constraints. Callers that would otherwise report a constraint as missing must check this first, so a
* database that cannot answer the query is reported as unknown rather than as wrong.
*
+ * The answer is per table and per kind, because a read can fail for one table while succeeding for the rest,
+ * and for one kind while succeeding for another. A failure that costs the schema its foreign keys leaves its
+ * primary keys, and every other table, still worth validating.
+ *
+ * @param tableName the table to check, case-insensitively.
* @param kind the constraint kind to check.
* @return {@code true} if the read succeeded, {@code false} if it failed and the constraints are unknown.
*/
- public boolean isDiscovered(@Nonnull ConstraintKind kind) {
- return discoveredConstraints.contains(kind);
+ public boolean isDiscovered(@Nonnull String tableName, @Nonnull ConstraintKind kind) {
+ return discoveredByKind.getOrDefault(kind, EMPTY_TABLES).contains(tableName);
+ }
+
+ private static final SortedSet EMPTY_TABLES = Collections.emptySortedSet();
+
+ /** Records that the given kind was read successfully for the given tables. */
+ private static void discovered(
+ @Nonnull Map> discoveredByKind,
+ @Nonnull ConstraintKind kind,
+ @Nonnull Collection tableNames
+ ) {
+ discoveredByKind.computeIfAbsent(kind, k -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))
+ .addAll(tableNames);
}
/**
@@ -239,14 +263,14 @@ public static DatabaseSchema read(
}
}
// Discover primary keys, unique keys, and foreign keys using the dialect-provided strategy.
- Set discoveredConstraints = EnumSet.noneOf(ConstraintKind.class);
+ Map> discoveredByKind = new EnumMap<>(ConstraintKind.class);
readConstraints(connection, metadata, catalog, schemaPattern, columnsByTable,
primaryKeysByTable, uniqueKeysByTable, foreignKeysByTable, constraintDiscoveryStrategy,
- discoveredConstraints);
+ discoveredByKind);
// Discover sequences using the dialect-provided strategy.
readSequences(connection, catalog, schemaPattern, sequences, sequenceDiscoveryStrategy);
return new DatabaseSchema(columnsByTable, primaryKeysByTable, uniqueKeysByTable, foreignKeysByTable, sequences,
- discoveredConstraints);
+ discoveredByKind);
}
// ------------------------------------------------------------------------------------------------------------------
@@ -266,7 +290,7 @@ private static void readConstraints(
@Nonnull SortedMap> uniqueKeysByTable,
@Nonnull SortedMap> foreignKeysByTable,
@Nonnull ConstraintDiscoveryStrategy strategy,
- @Nonnull Set discovered
+ @Nonnull Map> discovered
) throws SQLException {
switch (strategy) {
case JDBC_METADATA -> readConstraintsFromJdbcMetadata(
@@ -298,10 +322,8 @@ private static void readConstraintsFromJdbcMetadata(
@Nonnull SortedMap> primaryKeysByTable,
@Nonnull SortedMap> uniqueKeysByTable,
@Nonnull SortedMap> foreignKeysByTable,
- @Nonnull Set discovered
+ @Nonnull Map> discovered
) throws SQLException {
- boolean keysRead = true;
- boolean foreignKeysRead = true;
for (String tableName : new ArrayList<>(columnsByTable.keySet())) {
try (ResultSet primaryKeys = metadata.getPrimaryKeys(catalog, schemaPattern, tableName)) {
while (primaryKeys.next()) {
@@ -311,10 +333,10 @@ private static void readConstraintsFromJdbcMetadata(
primaryKeysByTable.computeIfAbsent(pkTableName, k -> new ArrayList<>())
.add(new DbPrimaryKey(pkTableName, columnName, keySeq));
}
+ discovered(discovered, ConstraintKind.PRIMARY_KEY, List.of(tableName));
} catch (SQLException e) {
- // Some databases/views may not support getPrimaryKeys; the keys stay unknown.
+ // Some databases/views may not support getPrimaryKeys; this table's primary key stays unknown.
LOGGER.debug("Failed to read primary keys for table '{}'.", tableName, e);
- keysRead = false;
}
}
for (String tableName : new ArrayList<>(columnsByTable.keySet())) {
@@ -332,10 +354,10 @@ private static void readConstraintsFromJdbcMetadata(
uniqueKeysByTable.computeIfAbsent(tableName, k -> new ArrayList<>())
.add(new DbUniqueKey(tableName, indexName, columnName, ordinalPosition));
}
+ discovered(discovered, ConstraintKind.UNIQUE_KEY, List.of(tableName));
} catch (SQLException e) {
- // Some databases/views may not support getIndexInfo; the keys stay unknown.
+ // Some databases/views may not support getIndexInfo; this table's unique keys stay unknown.
LOGGER.debug("Failed to read unique indexes for table '{}'.", tableName, e);
- keysRead = false;
}
}
for (String tableName : new ArrayList<>(columnsByTable.keySet())) {
@@ -348,18 +370,12 @@ private static void readConstraintsFromJdbcMetadata(
foreignKeysByTable.computeIfAbsent(fkTableName, k -> new ArrayList<>())
.add(new DbForeignKey(fkTableName, fkColumnName, pkTableName, pkColumnName));
}
+ discovered(discovered, ConstraintKind.FOREIGN_KEY, List.of(tableName));
} catch (SQLException e) {
- // Some databases/views may not support getImportedKeys; the keys stay unknown.
+ // Some databases/views may not support getImportedKeys; this table's foreign keys stay unknown.
LOGGER.debug("Failed to read foreign keys for table '{}'.", tableName, e);
- foreignKeysRead = false;
}
}
- if (keysRead) {
- discovered.add(ConstraintKind.KEY);
- }
- if (foreignKeysRead) {
- discovered.add(ConstraintKind.FOREIGN_KEY);
- }
}
/**
@@ -419,7 +435,7 @@ private static void readPrimaryAndUniqueKeysFromInformationSchema(
@Nonnull SortedMap> columnsByTable,
@Nonnull SortedMap> primaryKeysByTable,
@Nonnull SortedMap> uniqueKeysByTable,
- @Nonnull Set discovered
+ @Nonnull Map> discovered
) {
try {
StringBuilder sql = new StringBuilder("""
@@ -454,9 +470,10 @@ private static void readPrimaryAndUniqueKeysFromInformationSchema(
}
}
}
- discovered.add(ConstraintKind.KEY);
+ discovered(discovered, ConstraintKind.PRIMARY_KEY, columnsByTable.keySet());
+ discovered(discovered, ConstraintKind.UNIQUE_KEY, columnsByTable.keySet());
} catch (SQLException e) {
- // INFORMATION_SCHEMA views not available; the keys stay unknown.
+ // INFORMATION_SCHEMA views not available; the primary and unique keys stay unknown.
LOGGER.debug("Failed to read primary and unique keys from INFORMATION_SCHEMA.", e);
}
}
@@ -474,7 +491,7 @@ private static void readConstraintsFromInformationSchema(
@Nonnull SortedMap> primaryKeysByTable,
@Nonnull SortedMap> uniqueKeysByTable,
@Nonnull SortedMap> foreignKeysByTable,
- @Nonnull Set discovered
+ @Nonnull Map> discovered
) {
readPrimaryAndUniqueKeysFromInformationSchema(
connection, catalog, schemaPattern, columnsByTable, primaryKeysByTable, uniqueKeysByTable, discovered);
@@ -510,7 +527,7 @@ private static void readConstraintsFromInformationSchema(
.add(new DbForeignKey(fkTableName, fkColumnName, pkTableName, pkColumnName));
}
}
- discovered.add(ConstraintKind.FOREIGN_KEY);
+ discovered(discovered, ConstraintKind.FOREIGN_KEY, columnsByTable.keySet());
} catch (SQLException e) {
// REFERENTIAL_CONSTRAINTS not available; the foreign keys stay unknown.
LOGGER.debug("Failed to read foreign keys from INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS.", e);
@@ -529,7 +546,7 @@ private static void readConstraintsFromInformationSchemaReferencing(
@Nonnull SortedMap> primaryKeysByTable,
@Nonnull SortedMap> uniqueKeysByTable,
@Nonnull SortedMap> foreignKeysByTable,
- @Nonnull Set discovered
+ @Nonnull Map> discovered
) {
// For databases that use catalogs as schemas, the catalog value represents the database name and maps to
// TABLE_SCHEMA in INFORMATION_SCHEMA views (not TABLE_CATALOG).
@@ -558,7 +575,7 @@ private static void readConstraintsFromInformationSchemaReferencing(
.add(new DbForeignKey(fkTableName, fkColumnName, pkTableName, pkColumnName));
}
}
- discovered.add(ConstraintKind.FOREIGN_KEY);
+ discovered(discovered, ConstraintKind.FOREIGN_KEY, columnsByTable.keySet());
} catch (SQLException e) {
// REFERENCED columns not available; the foreign keys stay unknown.
LOGGER.debug("Failed to read foreign keys from INFORMATION_SCHEMA.KEY_COLUMN_USAGE.", e);
@@ -575,7 +592,7 @@ private static void readConstraintsFromAllConstraints(
@Nonnull SortedMap> primaryKeysByTable,
@Nonnull SortedMap> uniqueKeysByTable,
@Nonnull SortedMap> foreignKeysByTable,
- @Nonnull Set discovered
+ @Nonnull Map> discovered
) {
// Primary keys and unique constraints.
try {
@@ -609,9 +626,10 @@ private static void readConstraintsFromAllConstraints(
}
}
}
- discovered.add(ConstraintKind.KEY);
+ discovered(discovered, ConstraintKind.PRIMARY_KEY, columnsByTable.keySet());
+ discovered(discovered, ConstraintKind.UNIQUE_KEY, columnsByTable.keySet());
} catch (SQLException e) {
- // ALL_CONSTRAINTS not available; the keys stay unknown.
+ // ALL_CONSTRAINTS not available; the primary and unique keys stay unknown.
LOGGER.debug("Failed to read primary and unique keys from ALL_CONSTRAINTS.", e);
}
// Foreign keys.
@@ -647,7 +665,7 @@ private static void readConstraintsFromAllConstraints(
.add(new DbForeignKey(fkTableName, fkColumnName, pkTableName, pkColumnName));
}
}
- discovered.add(ConstraintKind.FOREIGN_KEY);
+ discovered(discovered, ConstraintKind.FOREIGN_KEY, columnsByTable.keySet());
} catch (SQLException e) {
// ALL_CONSTRAINTS FK query not available; the foreign keys stay unknown.
LOGGER.debug("Failed to read foreign keys from ALL_CONSTRAINTS.", e);
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java
index 62775621f..1fd0979d6 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java
@@ -15,9 +15,6 @@
*/
package st.orm.core.template.impl;
-import static st.orm.core.spi.Providers.getDatabaseProductName;
-import static st.orm.core.spi.Providers.getSqlDialect;
-import static st.orm.core.spi.Providers.getSqlDialectProvider;
import static st.orm.core.template.impl.RecordReflection.isPolymorphicData;
import jakarta.annotation.Nonnull;
@@ -51,7 +48,7 @@
import st.orm.Ref;
import st.orm.StormConfig;
import st.orm.UK;
-import st.orm.core.spi.SqlDialectProvider;
+import st.orm.core.spi.Providers;
import st.orm.core.spi.TypeDiscovery;
import st.orm.core.template.Column;
import st.orm.core.template.Model;
@@ -87,13 +84,14 @@ public final class SchemaValidator {
private final DataSource dataSource;
private final ModelBuilder modelBuilder;
private final TypeCompatibility typeCompatibility;
- private final SqlDialect sqlDialect;
+ /** The dialect to validate with, or {@code null} to resolve it from the connection when validating. */
+ private final @Nullable SqlDialect sqlDialect;
private SchemaValidator(
@Nonnull DataSource dataSource,
@Nonnull ModelBuilder modelBuilder,
@Nonnull TypeCompatibility typeCompatibility,
- @Nonnull SqlDialect sqlDialect
+ @Nullable SqlDialect sqlDialect
) {
this.dataSource = dataSource;
this.modelBuilder = modelBuilder;
@@ -109,7 +107,7 @@ private SchemaValidator(
*/
public static SchemaValidator of(@Nonnull DataSource dataSource) {
return new SchemaValidator(dataSource, ModelBuilder.newInstance(), TypeCompatibility.defaultCompatibility(),
- resolveSqlDialect(dataSource));
+ null);
}
/**
@@ -120,23 +118,7 @@ public static SchemaValidator of(@Nonnull DataSource dataSource) {
* @return a new schema validator.
*/
public static SchemaValidator of(@Nonnull DataSource dataSource, @Nonnull ModelBuilder modelBuilder) {
- return new SchemaValidator(dataSource, modelBuilder, TypeCompatibility.defaultCompatibility(),
- resolveSqlDialect(dataSource));
- }
-
- /**
- * Resolves the dialect for the given data source from its database product, the same way the template factories
- * do.
- *
- * Selecting by product matters when several dialect modules are on the classpath: the strategies a dialect
- * uses to read constraints and sequences are vendor-specific, so a dialect that does not match the database
- * reads the schema with queries the database does not understand. Only when no provider claims the product does
- * this fall back to the first registered dialect.
- */
- private static SqlDialect resolveSqlDialect(@Nonnull DataSource dataSource) {
- StormConfig config = StormConfig.defaults();
- SqlDialectProvider provider = getSqlDialectProvider(getDatabaseProductName(dataSource));
- return provider != null ? provider.getSqlDialect(config) : getSqlDialect(config);
+ return new SchemaValidator(dataSource, modelBuilder, TypeCompatibility.defaultCompatibility(), null);
}
/**
@@ -172,10 +154,16 @@ public List validate(@Nonnull Iterable schemaCache = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (Class extends Data> type : types) {
- validateType(type, connection, defaultCatalog, defaultSchema, schemaCache, errors);
+ validateType(type, connection, dialect, defaultCatalog, defaultSchema, schemaCache, errors);
}
} catch (SQLException e) {
throw new st.orm.PersistenceException("Failed to read database schema for validation.", e);
@@ -356,6 +344,7 @@ static String formatErrors(@Nonnull List errors) {
*/
private DatabaseSchema resolveSchema(
@Nonnull Connection connection,
+ @Nonnull SqlDialect dialect,
@Nullable String defaultCatalog,
@Nullable String defaultSchema,
@Nonnull String entitySchema,
@@ -371,7 +360,7 @@ private DatabaseSchema resolveSchema(
}
String catalog;
String schemaPattern;
- if (!entitySchema.isEmpty() && sqlDialect.useCatalogAsSchema()) {
+ if (!entitySchema.isEmpty() && dialect.useCatalogAsSchema()) {
// Database uses catalogs as schemas (e.g., MySQL, MariaDB). The entity's schema represents a
// database name, which maps to the JDBC catalog.
catalog = entitySchema;
@@ -381,7 +370,7 @@ private DatabaseSchema resolveSchema(
schemaPattern = entitySchema.isEmpty() ? defaultSchema : entitySchema;
}
DatabaseSchema databaseSchema = DatabaseSchema.read(connection, catalog, schemaPattern,
- sqlDialect.sequenceDiscoveryStrategy(), sqlDialect.constraintDiscoveryStrategy());
+ dialect.sequenceDiscoveryStrategy(), dialect.constraintDiscoveryStrategy());
schemaCache.put(schemaKey, databaseSchema);
return databaseSchema;
}
@@ -392,6 +381,7 @@ private DatabaseSchema resolveSchema(
private void validateType(
@Nonnull Class extends Data> type,
@Nonnull Connection connection,
+ @Nonnull SqlDialect dialect,
@Nullable String defaultCatalog,
@Nullable String defaultSchema,
@Nonnull SortedMap schemaCache,
@@ -419,7 +409,7 @@ private void validateType(
String entitySchema = model.schema();
DatabaseSchema schema;
try {
- schema = resolveSchema(connection, defaultCatalog, defaultSchema, entitySchema, schemaCache);
+ schema = resolveSchema(connection, dialect, defaultCatalog, defaultSchema, entitySchema, schemaCache);
} catch (SQLException e) {
throw new st.orm.PersistenceException(
"Failed to read database schema '%s' for validation.".formatted(entitySchema), e);
@@ -476,9 +466,9 @@ private void validateType(
.formatted(columnName, qualifiedTableName)));
}
}
- // 5. Primary key match. Skipped when the keys could not be read: an empty result would otherwise read as
- // "the table has no primary key".
- if (requirePrimaryKey && schema.isDiscovered(ConstraintKind.KEY)) {
+ // 5. Primary key match. Skipped when this table's primary key could not be read: an empty result would
+ // otherwise read as "the table has no primary key".
+ if (requirePrimaryKey && schema.isDiscovered(tableName, ConstraintKind.PRIMARY_KEY)) {
Set entityPkColumns = model.declaredColumns().stream()
.filter(Column::primaryKey)
.map(column -> column.name().toUpperCase())
@@ -552,8 +542,8 @@ private void validateUniqueKeys(
@Nonnull Set ignoredComponents,
@Nonnull List errors
) {
- if (!schema.isDiscovered(ConstraintKind.KEY)) {
- // The unique keys could not be read, so nothing can be said about them.
+ if (!schema.isDiscovered(tableName, ConstraintKind.UNIQUE_KEY)) {
+ // This table's unique keys could not be read, so nothing can be said about them.
return;
}
// Build a map of unique index name -> set of column names from the database.
@@ -605,8 +595,8 @@ private void validateForeignKeys(
@Nonnull Set ignoredComponents,
@Nonnull List errors
) {
- if (!schema.isDiscovered(ConstraintKind.FOREIGN_KEY)) {
- // The foreign keys could not be read, so nothing can be said about them.
+ if (!schema.isDiscovered(tableName, ConstraintKind.FOREIGN_KEY)) {
+ // This table's foreign keys could not be read, so nothing can be said about them.
return;
}
List dbForeignKeys = schema.getForeignKeys(tableName);
diff --git a/storm-core/src/test/java/st/orm/core/template/impl/DatabaseSchemaTest.java b/storm-core/src/test/java/st/orm/core/template/impl/DatabaseSchemaTest.java
index cecae6c73..161baadb4 100644
--- a/storm-core/src/test/java/st/orm/core/template/impl/DatabaseSchemaTest.java
+++ b/storm-core/src/test/java/st/orm/core/template/impl/DatabaseSchemaTest.java
@@ -4,7 +4,9 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.lang.reflect.Proxy;
import java.sql.Connection;
+import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
@@ -55,8 +57,9 @@ void testConstraintsAreDiscoveredWithAFittingStrategy() throws SQLException {
execute("CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent (id))");
try (Connection connection = getConnection()) {
DatabaseSchema schema = DatabaseSchema.read(connection);
- assertTrue(schema.isDiscovered(ConstraintKind.KEY));
- assertTrue(schema.isDiscovered(ConstraintKind.FOREIGN_KEY));
+ assertTrue(schema.isDiscovered("child", ConstraintKind.PRIMARY_KEY));
+ assertTrue(schema.isDiscovered("child", ConstraintKind.UNIQUE_KEY));
+ assertTrue(schema.isDiscovered("child", ConstraintKind.FOREIGN_KEY));
assertEquals(1, schema.getForeignKeys("child").size());
}
}
@@ -71,11 +74,73 @@ void testForeignKeysStayUnknownWhenTheStrategyDoesNotFitTheDatabase() throws SQL
DatabaseSchema schema = DatabaseSchema.read(connection, connection.getCatalog(), connection.getSchema(),
SequenceDiscoveryStrategy.INFORMATION_SCHEMA,
ConstraintDiscoveryStrategy.INFORMATION_SCHEMA_REFERENCING);
- assertFalse(schema.isDiscovered(ConstraintKind.FOREIGN_KEY));
+ assertFalse(schema.isDiscovered("child", ConstraintKind.FOREIGN_KEY));
assertTrue(schema.getForeignKeys("child").isEmpty());
// The keys come from a query H2 does understand, so they are known and were read.
- assertTrue(schema.isDiscovered(ConstraintKind.KEY));
+ assertTrue(schema.isDiscovered("child", ConstraintKind.PRIMARY_KEY));
assertEquals(1, schema.getPrimaryKeys("child").size());
+ // A table nothing was read for is unknown rather than assumed empty.
+ assertFalse(schema.isDiscovered("no_such_table", ConstraintKind.PRIMARY_KEY));
+ }
+ }
+
+ /**
+ * Wraps the connection so one {@link DatabaseMetaData} call fails, optionally only for one table, the way a
+ * driver refuses a call it does not support for a particular relation.
+ */
+ private Connection metadataFailing(String failingMethod, String failingTable) throws SQLException {
+ Connection delegate = getConnection();
+ DatabaseMetaData realMetaData = delegate.getMetaData();
+ DatabaseMetaData metaData = (DatabaseMetaData) Proxy.newProxyInstance(
+ DatabaseMetaData.class.getClassLoader(), new Class>[]{DatabaseMetaData.class},
+ (proxy, method, args) -> {
+ if (method.getName().equals(failingMethod)
+ && (failingTable == null || failingTable.equalsIgnoreCase(String.valueOf(args[2])))) {
+ throw new SQLException("metadata call not supported");
+ }
+ return invoke(method, realMetaData, args);
+ });
+ return (Connection) Proxy.newProxyInstance(
+ Connection.class.getClassLoader(), new Class>[]{Connection.class},
+ (proxy, method, args) -> method.getName().equals("getMetaData")
+ ? metaData
+ : invoke(method, delegate, args));
+ }
+
+ private static Object invoke(java.lang.reflect.Method method, Object target, Object[] args) throws Throwable {
+ try {
+ return method.invoke(target, args);
+ } catch (java.lang.reflect.InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+
+ private DatabaseSchema readWithJdbcMetadata(Connection connection) throws SQLException {
+ return DatabaseSchema.read(connection, connection.getCatalog(), connection.getSchema(),
+ SequenceDiscoveryStrategy.INFORMATION_SCHEMA, ConstraintDiscoveryStrategy.JDBC_METADATA);
+ }
+
+ @Test
+ void testAFailedReadForOneTableLeavesTheOthersDiscovered() throws SQLException {
+ execute("CREATE TABLE parent (id INTEGER PRIMARY KEY)");
+ execute("CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent (id))");
+ try (Connection connection = metadataFailing("getImportedKeys", "CHILD")) {
+ DatabaseSchema schema = readWithJdbcMetadata(connection);
+ // Only the table whose read failed is unknown; the rest of the schema stays worth validating.
+ assertFalse(schema.isDiscovered("child", ConstraintKind.FOREIGN_KEY));
+ assertTrue(schema.isDiscovered("parent", ConstraintKind.FOREIGN_KEY));
+ }
+ }
+
+ @Test
+ void testAFailedUniqueKeyReadLeavesPrimaryKeysDiscovered() throws SQLException {
+ execute("CREATE TABLE parent (id INTEGER PRIMARY KEY)");
+ try (Connection connection = metadataFailing("getIndexInfo", null)) {
+ DatabaseSchema schema = readWithJdbcMetadata(connection);
+ // Primary keys and unique keys are separate calls, so losing one keeps the other.
+ assertFalse(schema.isDiscovered("parent", ConstraintKind.UNIQUE_KEY));
+ assertTrue(schema.isDiscovered("parent", ConstraintKind.PRIMARY_KEY));
+ assertEquals(1, schema.getPrimaryKeys("parent").size());
}
}