From 1cc5b4be88fb11c1f0f04f3d93d8989aceb08c7f Mon Sep 17 00:00:00 2001 From: Leon van Zantvoort Date: Sun, 2 Aug 2026 14:59:13 +0200 Subject: [PATCH] fix: address review of the dialect resolution changes Resolving the dialect while constructing a SchemaValidator opened a JDBC connection, so merely creating one could fail on pool initialisation, authentication or the network before validate() was ever called. The dialect is now resolved inside validate(), from the connection it already holds, which removes the extra connection rather than moving it. The helper that duplicated Providers.getSqlDialect(DataSource, StormConfig) goes with it. Constraint discovery was recorded for the schema as a whole, so one table that a driver could not answer for disabled primary and unique key validation for every other table, hiding mismatches unrelated to it. Discovery is now recorded per table. Primary keys and unique keys were recorded as one kind although the JDBC metadata strategy reads them with separate calls, so a driver that could not report unique indexes also cost the primary keys. They are separate kinds now, and each failure comment names the kind it belongs to. --- .../core/template/impl/DatabaseSchema.java | 104 ++++++++++-------- .../core/template/impl/SchemaValidator.java | 60 +++++----- .../template/impl/DatabaseSchemaTest.java | 73 +++++++++++- 3 files changed, 155 insertions(+), 82 deletions(-) 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 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 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()); } }