Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -1924,7 +1939,7 @@ public void checkAndCreateChannelTables() {
try {
DatabaseMetaData dbMetaData = connection.getMetaData();

try (ResultSet rs = dbMetaData.getTables(null, null, "%", null)) {
try (ResultSet rs = dbMetaData.getTables(getTableCatalog(), null, "%", null)) {
while (rs.next()) {
String tableName = rs.getString("TABLE_NAME").toLowerCase();
channelTablesMap.remove(tableName);
Expand Down Expand Up @@ -2238,10 +2253,11 @@ public boolean initTableStructure() {
private boolean tableExists(String tableName) {
try {
DatabaseMetaData metaData = connection.getMetaData();
String catalog = getTableCatalog();

// 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
// 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;
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) {
Expand All @@ -228,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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: Open Integration Engine Contributors

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.never;
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 {

private static Connection connectionFor(String productName, DatabaseMetaData metaData) throws Exception {
Connection connection = mock(Connection.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();
}
}