From 06407c58cdfeb3a60663fc6736d38530bc401f09 Mon Sep 17 00:00:00 2001 From: iacsha <122627209+iacsha@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:47:06 +0000 Subject: [PATCH 1/3] Scope table-existence checks to the connection catalog (fixes #489) DatabaseUtil.tableExists and JdbcDao.checkAndCreateChannelTables/tableExists called DatabaseMetaData.getTables with a null catalog, so on MySQL a login with access to other databases matched their tables and a second OIE database was never migrated and missing message tables were not recreated. Pass connection.getCatalog() so each engine only sees its own database; null-catalog behaviour is unchanged on databases where getCatalog() returns null. Signed-off-by: iacsha <122627209+iacsha@users.noreply.github.com> --- .../donkey/server/data/jdbc/JdbcDao.java | 10 ++++- .../connect/server/util/DatabaseUtil.java | 8 +++- .../connect/server/util/DatabaseUtilTest.java | 43 +++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java index 1ff1034560..73bed766b6 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java @@ -1924,7 +1924,10 @@ public void checkAndCreateChannelTables() { try { DatabaseMetaData dbMetaData = connection.getMetaData(); - try (ResultSet rs = dbMetaData.getTables(null, null, "%", null)) { + // Scope to the connection's own catalog so tables in other databases visible to the + // same login are not mistaken for this database's tables. See + // OpenIntegrationEngine/engine#489. + try (ResultSet rs = dbMetaData.getTables(connection.getCatalog(), null, "%", null)) { while (rs.next()) { String tableName = rs.getString("TABLE_NAME").toLowerCase(); channelTablesMap.remove(tableName); @@ -2238,10 +2241,13 @@ public boolean initTableStructure() { private boolean tableExists(String tableName) { try { DatabaseMetaData metaData = connection.getMetaData(); + // Scope to the connection's own catalog so tables in other databases visible to the + // same login are not matched. See OpenIntegrationEngine/engine#489. + String catalog = connection.getCatalog(); // Databases disagree on identifier case folding, so try both spellings for (String name : new String[] { tableName, tableName.toUpperCase() }) { - try (ResultSet resultSet = metaData.getTables(null, null, name, null)) { + try (ResultSet resultSet = metaData.getTables(catalog, null, name, null)) { if (resultSet.next()) { return true; } diff --git a/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java b/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java index b4f815d9e3..ffe9abc7c8 100644 --- a/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java +++ b/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java @@ -211,14 +211,18 @@ public static boolean tableExists(Connection connection, String tableName) { try { DatabaseMetaData metaData = connection.getMetaData(); - resultSet = metaData.getTables(null, null, tableName.toUpperCase(), new String[] { + // Scope the lookup to the connection's own catalog so a login with access to other + // databases on the same server does not match their tables. See + // OpenIntegrationEngine/engine#489. + String catalog = connection.getCatalog(); + resultSet = metaData.getTables(catalog, null, tableName.toUpperCase(), new String[] { "TABLE" }); if (resultSet.next()) { return true; } - resultSet = metaData.getTables(null, null, tableName.toLowerCase(), new String[] { + resultSet = metaData.getTables(catalog, null, tableName.toLowerCase(), new String[] { "TABLE" }); return resultSet.next(); } catch (SQLException e) { diff --git a/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java b/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java new file mode 100644 index 0000000000..91886eecf1 --- /dev/null +++ b/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Open Integration Engine + +package com.mirth.connect.server.util; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; + +import org.junit.Test; + +public class DatabaseUtilTest { + + /** + * tableExists must scope its metadata lookup to the connection's own catalog. Passing a null + * catalog lets MySQL Connector/J match tables in any database the login can see, so a second + * OIE database on a shared login is never migrated. See OpenIntegrationEngine/engine#489. + */ + @Test + public void tableExists_scopesLookupToConnectionCatalog() throws Exception { + Connection connection = mock(Connection.class); + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + ResultSet resultSet = mock(ResultSet.class); + + when(connection.getMetaData()).thenReturn(metaData); + when(connection.getCatalog()).thenReturn("oie_b"); + when(metaData.getTables(any(), any(), any(), any())).thenReturn(resultSet); + when(resultSet.next()).thenReturn(false); + + assertFalse(DatabaseUtil.tableExists(connection, "CONFIGURATION")); + + verify(metaData).getTables(eq("oie_b"), isNull(), eq("CONFIGURATION"), any()); + verify(metaData).getTables(eq("oie_b"), isNull(), eq("configuration"), any()); + } +} From 07a31269b0702680bb2798206e7fa1e961a5499e Mon Sep 17 00:00:00 2001 From: iacsha <122627209+iacsha@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:52:54 +0000 Subject: [PATCH 2/3] Scope table lookups to the catalog only on MySQL (review feedback) Move the catalog scoping behind a JdbcDao flag that MysqlDaoFactory enables, and limit DatabaseUtil.tableExists scoping to MySQL/MariaDB, so other databases keep the existing null-catalog lookup. Signed-off-by: iacsha <122627209+iacsha@users.noreply.github.com> --- .../donkey/server/data/jdbc/JdbcDao.java | 24 ++++++++---- .../server/data/jdbc/MysqlDaoFactory.java | 1 + .../donkey/server/data/jdbc/JdbcDaoTest.java | 37 ++++++++++++++++++ .../connect/server/util/DatabaseUtil.java | 11 ++++-- .../connect/server/util/DatabaseUtilTest.java | 38 ++++++++++++++----- 5 files changed, 92 insertions(+), 19 deletions(-) diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java index 73bed766b6..b69e387174 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java @@ -83,6 +83,7 @@ public class JdbcDao implements DonkeyDao { private String statsServerId; private boolean transactionAlteredChannels = false; private char quoteChar = '"'; + private boolean scopeTablesToCatalog = false; private Logger logger = LogManager.getLogger(this.getClass()); protected JdbcDao(Donkey donkey, Connection connection, QuerySource querySource, SerializerProvider serializerProvider, boolean encryptMessageContent, boolean encryptAttachments, boolean encryptCustomMetaData, boolean decryptData, StatisticsUpdater statisticsUpdater, Statistics currentStats, Statistics totalStats, String statsServerId) { @@ -130,6 +131,20 @@ public void setQuoteChar(char quoteChar) { this.quoteChar = quoteChar; } + /** + * Limits table lookups to the connection's own catalog. MySQL Connector/J treats a null catalog + * as every database the login can see, so without this a second database on a shared login + * matches the first one's tables. Off by default; enabled by {@link MysqlDaoFactory}. See + * OpenIntegrationEngine/engine#489. + */ + public void setScopeTablesToCatalog(boolean scopeTablesToCatalog) { + this.scopeTablesToCatalog = scopeTablesToCatalog; + } + + private String getTableCatalog() throws SQLException { + return scopeTablesToCatalog ? connection.getCatalog() : null; + } + @Override public void insertMessage(Message message) { logger.debug(message.getChannelId() + "/" + message.getMessageId() + ": inserting message"); @@ -1924,10 +1939,7 @@ public void checkAndCreateChannelTables() { try { DatabaseMetaData dbMetaData = connection.getMetaData(); - // Scope to the connection's own catalog so tables in other databases visible to the - // same login are not mistaken for this database's tables. See - // OpenIntegrationEngine/engine#489. - try (ResultSet rs = dbMetaData.getTables(connection.getCatalog(), null, "%", null)) { + try (ResultSet rs = dbMetaData.getTables(getTableCatalog(), null, "%", null)) { while (rs.next()) { String tableName = rs.getString("TABLE_NAME").toLowerCase(); channelTablesMap.remove(tableName); @@ -2241,9 +2253,7 @@ public boolean initTableStructure() { private boolean tableExists(String tableName) { try { DatabaseMetaData metaData = connection.getMetaData(); - // Scope to the connection's own catalog so tables in other databases visible to the - // same login are not matched. See OpenIntegrationEngine/engine#489. - String catalog = connection.getCatalog(); + String catalog = getTableCatalog(); // Databases disagree on identifier case folding, so try both spellings for (String name : new String[] { tableName, tableName.toUpperCase() }) { diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java index ab08ebf458..7a888c8320 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java @@ -16,6 +16,7 @@ public class MysqlDaoFactory extends JdbcDaoFactory { public JdbcDao getDao(SerializerProvider serializerProvider) { JdbcDao dao = super.getDao(serializerProvider); dao.setQuoteChar('`'); + dao.setScopeTablesToCatalog(true); return dao; } } diff --git a/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoTest.java b/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoTest.java index 4cf983e55f..5ac513d43a 100644 --- a/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoTest.java +++ b/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoTest.java @@ -10,10 +10,12 @@ package com.mirth.connect.donkey.server.data.jdbc; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; @@ -476,4 +478,39 @@ private void setLocalChannelId(String channelId, long localChannelId) { channels.put(channelId, channel); when(donkey.getDeployedChannels()).thenReturn(channels); } + + /** + * Table lookups stay unscoped (null catalog) unless the factory enables catalog scoping, so + * only MySQL changes behaviour. See OpenIntegrationEngine/engine#489. + */ + @Test + public void testTableLookupUnscopedByDefault() throws SQLException { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + ResultSet resultSet = mock(ResultSet.class); + when(connection.getMetaData()).thenReturn(metaData); + when(connection.getCatalog()).thenReturn("oie_b"); + when(metaData.getTables(any(), any(), any(), any())).thenReturn(resultSet); + when(resultSet.next()).thenReturn(true); + + assertFalse(dao.initTableStructure()); + + verify(metaData).getTables(isNull(), isNull(), eq("d_channels"), isNull()); + verify(connection, never()).getCatalog(); + } + + /** With catalog scoping enabled (MySQL), lookups are limited to the connection's database. */ + @Test + public void testTableLookupScopedToCatalogWhenEnabled() throws SQLException { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + ResultSet resultSet = mock(ResultSet.class); + when(connection.getMetaData()).thenReturn(metaData); + when(connection.getCatalog()).thenReturn("oie_b"); + when(metaData.getTables(any(), any(), any(), any())).thenReturn(resultSet); + when(resultSet.next()).thenReturn(true); + + dao.setScopeTablesToCatalog(true); + assertFalse(dao.initTableStructure()); + + verify(metaData).getTables(eq("oie_b"), isNull(), eq("d_channels"), isNull()); + } } diff --git a/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java b/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java index ffe9abc7c8..b0b5855f10 100644 --- a/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java +++ b/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java @@ -211,10 +211,10 @@ public static boolean tableExists(Connection connection, String tableName) { try { DatabaseMetaData metaData = connection.getMetaData(); - // Scope the lookup to the connection's own catalog so a login with access to other - // databases on the same server does not match their tables. See + // MySQL treats a null catalog as every database the login can see, so scope the lookup + // to this connection's database there. Other databases keep the unscoped lookup. See // OpenIntegrationEngine/engine#489. - String catalog = connection.getCatalog(); + String catalog = isMySql(metaData) ? connection.getCatalog() : null; resultSet = metaData.getTables(catalog, null, tableName.toUpperCase(), new String[] { "TABLE" }); @@ -232,6 +232,11 @@ public static boolean tableExists(Connection connection, String tableName) { } } + private static boolean isMySql(DatabaseMetaData metaData) throws SQLException { + String productName = metaData.getDatabaseProductName(); + return "MySQL".equalsIgnoreCase(productName) || "MariaDB".equalsIgnoreCase(productName); + } + /** * Tell whether or not the given index exists in the database */ diff --git a/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java b/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java index 91886eecf1..3029cdd271 100644 --- a/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java +++ b/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: Open Integration Engine +// SPDX-FileCopyrightText: Open Integration Engine Contributors package com.mirth.connect.server.util; @@ -8,6 +8,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -19,25 +20,44 @@ public class DatabaseUtilTest { - /** - * tableExists must scope its metadata lookup to the connection's own catalog. Passing a null - * catalog lets MySQL Connector/J match tables in any database the login can see, so a second - * OIE database on a shared login is never migrated. See OpenIntegrationEngine/engine#489. - */ - @Test - public void tableExists_scopesLookupToConnectionCatalog() throws Exception { + private static Connection connectionFor(String productName, DatabaseMetaData metaData) throws Exception { Connection connection = mock(Connection.class); - DatabaseMetaData metaData = mock(DatabaseMetaData.class); ResultSet resultSet = mock(ResultSet.class); when(connection.getMetaData()).thenReturn(metaData); when(connection.getCatalog()).thenReturn("oie_b"); + when(metaData.getDatabaseProductName()).thenReturn(productName); when(metaData.getTables(any(), any(), any(), any())).thenReturn(resultSet); when(resultSet.next()).thenReturn(false); + return connection; + } + + /** + * On MySQL, tableExists must scope its metadata lookup to the connection's own catalog. A null + * catalog lets Connector/J match tables in any database the login can see, so a second OIE + * database on a shared login is never migrated. See OpenIntegrationEngine/engine#489. + */ + @Test + public void tableExists_onMySql_scopesLookupToConnectionCatalog() throws Exception { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + Connection connection = connectionFor("MySQL", metaData); assertFalse(DatabaseUtil.tableExists(connection, "CONFIGURATION")); verify(metaData).getTables(eq("oie_b"), isNull(), eq("CONFIGURATION"), any()); verify(metaData).getTables(eq("oie_b"), isNull(), eq("configuration"), any()); } + + /** Other databases keep the existing unscoped (null catalog) lookup. */ + @Test + public void tableExists_onOtherDatabases_keepsNullCatalog() throws Exception { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + Connection connection = connectionFor("PostgreSQL", metaData); + + assertFalse(DatabaseUtil.tableExists(connection, "CONFIGURATION")); + + verify(metaData).getTables(isNull(), isNull(), eq("CONFIGURATION"), any()); + verify(metaData).getTables(isNull(), isNull(), eq("configuration"), any()); + verify(connection, never()).getCatalog(); + } } From 39711c5fc89624a5aea228ba3345233fc4d9d9d5 Mon Sep 17 00:00:00 2001 From: iacsha <122627209+iacsha@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:19:57 +0000 Subject: [PATCH 3/3] Scope the index lookup to the catalog on MySQL too (review feedback) DatabaseUtil.indexExists still called getIndexInfo with a null catalog, so on MySQL an engine that had lost one of its own indexes found another database's index of the same name and was never offered the task that recreates it. Move the catalog choice into lookupCatalog and use it in both tableExists and indexExists. Adds two DatabaseUtilTest cases; the MySQL one fails without this change. Signed-off-by: iacsha <122627209+iacsha@users.noreply.github.com> --- .../connect/server/util/DatabaseUtil.java | 19 +++++--- .../connect/server/util/DatabaseUtilTest.java | 45 ++++++++++++++++--- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java b/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java index b0b5855f10..71dc6bbe9f 100644 --- a/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java +++ b/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java @@ -211,10 +211,7 @@ public static boolean tableExists(Connection connection, String tableName) { try { DatabaseMetaData metaData = connection.getMetaData(); - // MySQL treats a null catalog as every database the login can see, so scope the lookup - // to this connection's database there. Other databases keep the unscoped lookup. See - // OpenIntegrationEngine/engine#489. - String catalog = isMySql(metaData) ? connection.getCatalog() : null; + String catalog = lookupCatalog(connection, metaData); resultSet = metaData.getTables(catalog, null, tableName.toUpperCase(), new String[] { "TABLE" }); @@ -232,6 +229,15 @@ public static boolean tableExists(Connection connection, String tableName) { } } + /** + * The catalog to pass to metadata lookups. MySQL treats a null catalog as every database the + * login can see, so scope lookups to this connection's database there. Other databases keep + * the unscoped (null) lookup. See OpenIntegrationEngine/engine#489. + */ + private static String lookupCatalog(Connection connection, DatabaseMetaData metaData) throws SQLException { + return isMySql(metaData) ? connection.getCatalog() : null; + } + private static boolean isMySql(DatabaseMetaData metaData) throws SQLException { String productName = metaData.getDatabaseProductName(); return "MySQL".equalsIgnoreCase(productName) || "MariaDB".equalsIgnoreCase(productName); @@ -249,7 +255,8 @@ public static boolean indexExists(Connection connection, String tableName, Strin try { DatabaseMetaData metaData = connection.getMetaData(); - resultSet = metaData.getIndexInfo(null, null, tableName.toUpperCase(), false, false); + String catalog = lookupCatalog(connection, metaData); + resultSet = metaData.getIndexInfo(catalog, null, tableName.toUpperCase(), false, false); while (resultSet.next()) { if (indexName.equalsIgnoreCase(resultSet.getString("INDEX_NAME"))) { @@ -257,7 +264,7 @@ public static boolean indexExists(Connection connection, String tableName, Strin } } - resultSet = metaData.getIndexInfo(null, null, tableName.toLowerCase(), false, false); + resultSet = metaData.getIndexInfo(catalog, null, tableName.toLowerCase(), false, false); while (resultSet.next()) { if (indexName.equalsIgnoreCase(resultSet.getString("INDEX_NAME"))) { diff --git a/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java b/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java index 3029cdd271..79c975b8c9 100644 --- a/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java +++ b/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; @@ -20,15 +21,18 @@ public class DatabaseUtilTest { - private static Connection connectionFor(String productName, DatabaseMetaData metaData) throws Exception { + private static Connection connectionFor(String productName, DatabaseMetaData metaData, boolean tableFound) throws Exception { Connection connection = mock(Connection.class); - ResultSet resultSet = mock(ResultSet.class); + ResultSet tables = mock(ResultSet.class); + ResultSet indexes = mock(ResultSet.class); when(connection.getMetaData()).thenReturn(metaData); when(connection.getCatalog()).thenReturn("oie_b"); when(metaData.getDatabaseProductName()).thenReturn(productName); - when(metaData.getTables(any(), any(), any(), any())).thenReturn(resultSet); - when(resultSet.next()).thenReturn(false); + when(metaData.getTables(any(), any(), any(), any())).thenReturn(tables); + when(tables.next()).thenReturn(tableFound); + when(metaData.getIndexInfo(any(), any(), any(), anyBoolean(), anyBoolean())).thenReturn(indexes); + when(indexes.next()).thenReturn(false); return connection; } @@ -40,7 +44,7 @@ private static Connection connectionFor(String productName, DatabaseMetaData met @Test public void tableExists_onMySql_scopesLookupToConnectionCatalog() throws Exception { DatabaseMetaData metaData = mock(DatabaseMetaData.class); - Connection connection = connectionFor("MySQL", metaData); + Connection connection = connectionFor("MySQL", metaData, false); assertFalse(DatabaseUtil.tableExists(connection, "CONFIGURATION")); @@ -52,7 +56,7 @@ public void tableExists_onMySql_scopesLookupToConnectionCatalog() throws Excepti @Test public void tableExists_onOtherDatabases_keepsNullCatalog() throws Exception { DatabaseMetaData metaData = mock(DatabaseMetaData.class); - Connection connection = connectionFor("PostgreSQL", metaData); + Connection connection = connectionFor("PostgreSQL", metaData, false); assertFalse(DatabaseUtil.tableExists(connection, "CONFIGURATION")); @@ -60,4 +64,33 @@ public void tableExists_onOtherDatabases_keepsNullCatalog() throws Exception { verify(metaData).getTables(isNull(), isNull(), eq("configuration"), any()); verify(connection, never()).getCatalog(); } + + /** + * On MySQL, indexExists must scope its index lookup the same way. Otherwise an engine that has + * lost one of its own indexes finds another database's index of the same name and is never + * offered the task that recreates it. + */ + @Test + public void indexExists_onMySql_scopesLookupToConnectionCatalog() throws Exception { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + Connection connection = connectionFor("MySQL", metaData, true); + + assertFalse(DatabaseUtil.indexExists(connection, "D_MM1", "D_MM1_INDEX3")); + + verify(metaData).getIndexInfo(eq("oie_b"), isNull(), eq("D_MM1"), eq(false), eq(false)); + verify(metaData).getIndexInfo(eq("oie_b"), isNull(), eq("d_mm1"), eq(false), eq(false)); + } + + /** Other databases keep the existing unscoped (null catalog) index lookup. */ + @Test + public void indexExists_onOtherDatabases_keepsNullCatalog() throws Exception { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + Connection connection = connectionFor("PostgreSQL", metaData, true); + + assertFalse(DatabaseUtil.indexExists(connection, "D_MM1", "D_MM1_INDEX3")); + + verify(metaData).getIndexInfo(isNull(), isNull(), eq("D_MM1"), eq(false), eq(false)); + verify(metaData).getIndexInfo(isNull(), isNull(), eq("d_mm1"), eq(false), eq(false)); + verify(connection, never()).getCatalog(); + } }