diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLMetaDataProvider.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLMetaDataProvider.java index 1295800d7..18fe9fc6b 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLMetaDataProvider.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLMetaDataProvider.java @@ -5,6 +5,7 @@ import static org.alfasoftware.morf.jdbc.DatabaseMetaDataProviderUtils.shouldIgnoreIndex; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -41,9 +42,19 @@ public class PostgreSQLMetaDataProvider extends DatabaseMetaDataProvider impleme private static final Log log = LogFactory.getLog(PostgreSQLMetaDataProvider.class); private static final Pattern REALNAME_COMMENT_MATCHER = Pattern.compile(".*"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":\\[([^\\]]*)\\](/TYPE:\\[([^\\]]*)\\])?.*"); + private static final String EXTENSION_RELATIONS_SQL = "SELECT c.relname" + + " FROM pg_catalog.pg_class c" + + " JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace" + + " JOIN pg_catalog.pg_depend d ON d.classid = 'pg_catalog.pg_class'::pg_catalog.regclass" + + " AND d.objid = c.oid AND d.objsubid = 0" + + " JOIN pg_catalog.pg_extension e ON d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass" + + " AND d.refobjid = e.oid" + + " WHERE d.deptype = 'e'"; + private static final String SCHEMA_EXTENSION_RELATIONS_SQL = EXTENSION_RELATIONS_SQL + " AND n.nspname = ?"; private final Supplier> allIndexNames = Suppliers.memoize(this::loadAllIndexNames); private final Supplier>> allIgnoredIndexes = Suppliers.memoize(this::loadIgnoredIndexes); + private final Supplier> extensionRelationNames = Suppliers.memoize(this::loadExtensionRelationNames); private final Set allIgnoredIndexesTables = new HashSet<>(); public PostgreSQLMetaDataProvider(Connection connection, String schemaName) { @@ -57,6 +68,48 @@ protected boolean isPrimaryKeyIndex(RealName indexName) { } + @Override + protected boolean isSystemTable(RealName tableName) { + return extensionRelationNames.get().contains(tableName); + } + + + @Override + protected boolean isSystemView(RealName viewName) { + return extensionRelationNames.get().contains(viewName); + } + + + private Set loadExtensionRelationNames() { + try { + if (StringUtils.isBlank(schemaName)) { + try (PreparedStatement statement = connection.prepareStatement(EXTENSION_RELATIONS_SQL)) { + return readExtensionRelationNames(statement); + } + } + + try (PreparedStatement statement = connection.prepareStatement(SCHEMA_EXTENSION_RELATIONS_SQL)) { + statement.setString(1, schemaName); + return readExtensionRelationNames(statement); + } + } + catch (SQLException e) { + throw new RuntimeSqlException(e); + } + } + + + private Set readExtensionRelationNames(PreparedStatement statement) throws SQLException { + Set relationNames = new HashSet<>(); + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + relationNames.add(named(resultSet.getString(1))); + } + } + return relationNames; + } + + @Override protected DataType dataTypeFromSqlType(int sqlType, String typeName, int width) { diff --git a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSqlMetaDataProvider.java b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSqlMetaDataProvider.java index b98d3598b..1b21e2139 100644 --- a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSqlMetaDataProvider.java +++ b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSqlMetaDataProvider.java @@ -55,6 +55,15 @@ public class TestPostgreSqlMetaDataProvider { private static final String TABLE_NAME = "AREALTABLE"; private static final String TEST_SCHEMA = "TestSchema"; + private static final String EXTENSION_RELATIONS_SQL = "SELECT c.relname" + + " FROM pg_catalog.pg_class c" + + " JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace" + + " JOIN pg_catalog.pg_depend d ON d.classid = 'pg_catalog.pg_class'::pg_catalog.regclass" + + " AND d.objid = c.oid AND d.objsubid = 0" + + " JOIN pg_catalog.pg_extension e ON d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass" + + " AND d.refobjid = e.oid" + + " WHERE d.deptype = 'e'"; + private static final String SCHEMA_EXTENSION_RELATIONS_SQL = EXTENSION_RELATIONS_SQL + " AND n.nspname = ?"; private final DataSource dataSource = mock(DataSource.class, RETURNS_SMART_NULLS); private final Connection connection = mock(Connection.class, RETURNS_SMART_NULLS); @@ -69,6 +78,12 @@ public void setup() { @Before public void before() throws SQLException { when(dataSource.getConnection()).thenReturn(connection); + + PreparedStatement extensionStatement = mock(PreparedStatement.class, RETURNS_SMART_NULLS); + ResultSet extensionRelations = mock(ResultSet.class, RETURNS_SMART_NULLS); + when(connection.prepareStatement(SCHEMA_EXTENSION_RELATIONS_SQL)).thenReturn(extensionStatement); + when(extensionStatement.executeQuery()).thenReturn(extensionRelations); + when(extensionRelations.next()).thenReturn(false); } @@ -95,6 +110,70 @@ public void testLoadSequences() throws SQLException { verify(statement).setString(1, TEST_SCHEMA); } + + /** + * Checks that tables and views belonging to extensions are excluded from application metadata. + * + * @throws SQLException exception + */ + @Test + public void testExtensionRelationsAreSystemRelations() throws SQLException { + // Given + PreparedStatement extensionStatement = mock(PreparedStatement.class, RETURNS_SMART_NULLS); + ResultSet extensionRelations = mock(ResultSet.class, RETURNS_SMART_NULLS); + when(connection.prepareStatement(SCHEMA_EXTENSION_RELATIONS_SQL)).thenReturn(extensionStatement); + when(extensionStatement.executeQuery()).thenReturn(extensionRelations); + when(extensionRelations.next()).thenReturn(true, true, false); + when(extensionRelations.getString(1)).thenReturn("extension_table", "extension_view"); + + DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class, RETURNS_SMART_NULLS); + ResultSet tables = mockRelations("extension_table", "application_table"); + ResultSet views = mockRelations("extension_view", "application_view"); + when(connection.getMetaData()).thenReturn(databaseMetaData); + when(databaseMetaData.getTables(null, TEST_SCHEMA, null, new String[] { "TABLE" })) + .thenReturn(tables); + when(databaseMetaData.getTables(null, TEST_SCHEMA, null, new String[] { "VIEW" })) + .thenReturn(views); + + // When + Schema postgresMetaDataProvider = postgres.openSchema(connection, "TestDatabase", TEST_SCHEMA); + + // Then + assertThat("Extension tables should be excluded", postgresMetaDataProvider.tableNames(), contains("application_table")); + assertThat("Extension views should be excluded", postgresMetaDataProvider.viewNames(), contains("application_view")); + verify(extensionStatement).setString(1, TEST_SCHEMA); + verify(extensionStatement).executeQuery(); + } + + + /** + * Checks that extension relations can be loaded when no schema filter is supplied. + * + * @throws SQLException exception + */ + @Test + public void testExtensionRelationsWithoutSchemaFilter() throws SQLException { + // Given + PreparedStatement extensionStatement = mock(PreparedStatement.class, RETURNS_SMART_NULLS); + ResultSet extensionRelations = mock(ResultSet.class, RETURNS_SMART_NULLS); + when(connection.prepareStatement(EXTENSION_RELATIONS_SQL)).thenReturn(extensionStatement); + when(extensionStatement.executeQuery()).thenReturn(extensionRelations); + when(extensionRelations.next()).thenReturn(false); + + DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class, RETURNS_SMART_NULLS); + ResultSet tables = mockRelations("application_table_one", "application_table_two"); + when(connection.getMetaData()).thenReturn(databaseMetaData); + when(databaseMetaData.getTables(null, null, null, new String[] { "TABLE" })).thenReturn(tables); + + // When + Schema postgresMetaDataProvider = new PostgreSQLMetaDataProvider(connection, null); + + // Then + assertThat("Application tables should be retained", postgresMetaDataProvider.tableNames(), + contains("application_table_one", "application_table_two")); + verify(extensionStatement).executeQuery(); + } + /** * Checks the SQL run for retrieving sequences information * @@ -181,6 +260,15 @@ public void testLoadAllIgnoredIndexes() throws SQLException { } + private ResultSet mockRelations(String firstRelation, String secondRelation) throws SQLException { + ResultSet resultSet = mock(ResultSet.class, RETURNS_SMART_NULLS); + when(resultSet.next()).thenReturn(true, true, false); + when(resultSet.getString(2)).thenReturn(TEST_SCHEMA); + when(resultSet.getString(3)).thenReturn(firstRelation, firstRelation, secondRelation, secondRelation); + return resultSet; + } + + /** * Mockito {@link Answer} that returns a mock result set with a given number of resultRows. */