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()); } }