From 86fdbcc2d0bfe8c14a2ec616b14a0a7b83139a5f Mon Sep 17 00:00:00 2001 From: Jon Knight Date: Thu, 16 Jul 2026 16:34:49 +0100 Subject: [PATCH 1/2] Make extension views in postgres be recognised as system views --- .../PostgreSQLMetaDataProvider.java | 78 +++++++++++++++---- .../TestPostgreSqlMetaDataProvider.java | 58 ++++++++++++++ 2 files changed, 120 insertions(+), 16 deletions(-) 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..e799dec16 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; @@ -44,6 +45,7 @@ public class PostgreSQLMetaDataProvider extends DatabaseMetaDataProvider impleme 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 +59,50 @@ 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() { + Set relationNames = new HashSet<>(); + String schemaFilter = StringUtils.isNotBlank(schemaName) ? " AND n.nspname = ?" : ""; + String 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'" + + schemaFilter; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + if (StringUtils.isNotBlank(schemaName)) { + statement.setString(1, schemaName); + } + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + relationNames.add(named(resultSet.getString(1))); + } + } + } + catch (SQLException e) { + throw new RuntimeSqlException(e); + } + + return relationNames; + } + + @Override protected DataType dataTypeFromSqlType(int sqlType, String typeName, int width) { @@ -97,8 +143,8 @@ protected RealName readColumnName(ResultSet columnResultSet) throws SQLException String comment = columnResultSet.getString(COLUMN_REMARKS); String realName = matchComment(comment); return StringUtils.isNotBlank(realName) - ? createRealName(columnName, realName) - : super.readColumnName(columnResultSet); + ? createRealName(columnName, realName) + : super.readColumnName(columnResultSet); } @@ -108,8 +154,8 @@ protected RealName readTableName(ResultSet tableResultSet) throws SQLException { String comment = tableResultSet.getString(TABLE_REMARKS); String realName = matchComment(comment); return StringUtils.isNotBlank(realName) - ? createRealName(tableName, realName) - : super.readTableName(tableResultSet); + ? createRealName(tableName, realName) + : super.readTableName(tableResultSet); } @@ -119,8 +165,8 @@ protected RealName readViewName(ResultSet viewResultSet) throws SQLException { String comment = viewResultSet.getString(TABLE_REMARKS); String realName = matchComment(comment); return StringUtils.isNotBlank(realName) - ? createRealName(viewName, realName) - : super.readViewName(viewResultSet); + ? createRealName(viewName, realName) + : super.readViewName(viewResultSet); } @Override @@ -153,16 +199,16 @@ protected Map loadAllIndexNames() { final ImmutableMap.Builder indexNames = ImmutableMap.builder(); String schema = StringUtils.isNotBlank(schemaName) - ? " JOIN pg_catalog.pg_namespace n ON n.oid = ci.relnamespace AND n.nspname = '" + schemaName + "'" - : ""; + ? " JOIN pg_catalog.pg_namespace n ON n.oid = ci.relnamespace AND n.nspname = '" + schemaName + "'" + : ""; String sql = "SELECT ci.relname AS indexName, d.description AS indexRemark, t.relname as tableName, td.description as tableRemark" - + " FROM pg_catalog.pg_index i" - + " JOIN pg_catalog.pg_class ci ON ci.oid = i.indexrelid" - + " JOIN pg_catalog.pg_class t ON t.oid = i.indrelid" - + schema - + " JOIN pg_description d ON d.objoid = ci.oid" - + " JOIN pg_description td ON td.objoid = t.oid and td.objsubid=0"; + + " FROM pg_catalog.pg_index i" + + " JOIN pg_catalog.pg_class ci ON ci.oid = i.indexrelid" + + " JOIN pg_catalog.pg_class t ON t.oid = i.indrelid" + + schema + + " JOIN pg_description d ON d.objoid = ci.oid" + + " JOIN pg_description td ON td.objoid = t.oid and td.objsubid=0"; allIgnoredIndexesTables.clear(); @@ -224,8 +270,8 @@ private String matchComment(String comment) { @Override protected String buildSequenceSql(String schemaName) { StringBuilder sequenceSqlBuilder = new StringBuilder("SELECT S.relname FROM pg_class S LEFT JOIN pg_depend D ON " + - "(S.oid = D.objid AND D.deptype = 'a') LEFT JOIN pg_namespace N on (N.oid = S.relnamespace) WHERE S.relkind = " + - "'S' AND D.objid IS NULL"); + "(S.oid = D.objid AND D.deptype = 'a') LEFT JOIN pg_namespace N on (N.oid = S.relnamespace) WHERE S.relkind = " + + "'S' AND D.objid IS NULL"); if (schemaName != null && !schemaName.isBlank()) { sequenceSqlBuilder.append(" AND N.nspname=?"); 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..5cf9f6ff4 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,14 @@ 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' 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 +77,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(EXTENSION_RELATIONS_SQL)).thenReturn(extensionStatement); + when(extensionStatement.executeQuery()).thenReturn(extensionRelations); + when(extensionRelations.next()).thenReturn(false); } @@ -95,6 +109,41 @@ 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(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 the SQL run for retrieving sequences information * @@ -181,6 +230,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. */ From f0f7e8846f0a9a34051ddd3bfd60a3e6375cec09 Mon Sep 17 00:00:00 2001 From: Jon Knight Date: Thu, 16 Jul 2026 16:59:55 +0100 Subject: [PATCH 2/2] Separate the the parameterised and unparameterised statements in isSystemView --- .../PostgreSQLMetaDataProvider.java | 77 ++++++++++--------- .../TestPostgreSqlMetaDataProvider.java | 36 ++++++++- 2 files changed, 75 insertions(+), 38 deletions(-) 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 e799dec16..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 @@ -42,6 +42,15 @@ 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); @@ -72,33 +81,31 @@ protected boolean isSystemView(RealName viewName) { private Set loadExtensionRelationNames() { - Set relationNames = new HashSet<>(); - String schemaFilter = StringUtils.isNotBlank(schemaName) ? " AND n.nspname = ?" : ""; - String 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'" - + schemaFilter; - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - if (StringUtils.isNotBlank(schemaName)) { - statement.setString(1, schemaName); + try { + if (StringUtils.isBlank(schemaName)) { + try (PreparedStatement statement = connection.prepareStatement(EXTENSION_RELATIONS_SQL)) { + return readExtensionRelationNames(statement); + } } - try (ResultSet resultSet = statement.executeQuery()) { - while (resultSet.next()) { - relationNames.add(named(resultSet.getString(1))); - } + 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; } @@ -143,8 +150,8 @@ protected RealName readColumnName(ResultSet columnResultSet) throws SQLException String comment = columnResultSet.getString(COLUMN_REMARKS); String realName = matchComment(comment); return StringUtils.isNotBlank(realName) - ? createRealName(columnName, realName) - : super.readColumnName(columnResultSet); + ? createRealName(columnName, realName) + : super.readColumnName(columnResultSet); } @@ -154,8 +161,8 @@ protected RealName readTableName(ResultSet tableResultSet) throws SQLException { String comment = tableResultSet.getString(TABLE_REMARKS); String realName = matchComment(comment); return StringUtils.isNotBlank(realName) - ? createRealName(tableName, realName) - : super.readTableName(tableResultSet); + ? createRealName(tableName, realName) + : super.readTableName(tableResultSet); } @@ -165,8 +172,8 @@ protected RealName readViewName(ResultSet viewResultSet) throws SQLException { String comment = viewResultSet.getString(TABLE_REMARKS); String realName = matchComment(comment); return StringUtils.isNotBlank(realName) - ? createRealName(viewName, realName) - : super.readViewName(viewResultSet); + ? createRealName(viewName, realName) + : super.readViewName(viewResultSet); } @Override @@ -199,16 +206,16 @@ protected Map loadAllIndexNames() { final ImmutableMap.Builder indexNames = ImmutableMap.builder(); String schema = StringUtils.isNotBlank(schemaName) - ? " JOIN pg_catalog.pg_namespace n ON n.oid = ci.relnamespace AND n.nspname = '" + schemaName + "'" - : ""; + ? " JOIN pg_catalog.pg_namespace n ON n.oid = ci.relnamespace AND n.nspname = '" + schemaName + "'" + : ""; String sql = "SELECT ci.relname AS indexName, d.description AS indexRemark, t.relname as tableName, td.description as tableRemark" - + " FROM pg_catalog.pg_index i" - + " JOIN pg_catalog.pg_class ci ON ci.oid = i.indexrelid" - + " JOIN pg_catalog.pg_class t ON t.oid = i.indrelid" - + schema - + " JOIN pg_description d ON d.objoid = ci.oid" - + " JOIN pg_description td ON td.objoid = t.oid and td.objsubid=0"; + + " FROM pg_catalog.pg_index i" + + " JOIN pg_catalog.pg_class ci ON ci.oid = i.indexrelid" + + " JOIN pg_catalog.pg_class t ON t.oid = i.indrelid" + + schema + + " JOIN pg_description d ON d.objoid = ci.oid" + + " JOIN pg_description td ON td.objoid = t.oid and td.objsubid=0"; allIgnoredIndexesTables.clear(); @@ -270,8 +277,8 @@ private String matchComment(String comment) { @Override protected String buildSequenceSql(String schemaName) { StringBuilder sequenceSqlBuilder = new StringBuilder("SELECT S.relname FROM pg_class S LEFT JOIN pg_depend D ON " + - "(S.oid = D.objid AND D.deptype = 'a') LEFT JOIN pg_namespace N on (N.oid = S.relnamespace) WHERE S.relkind = " + - "'S' AND D.objid IS NULL"); + "(S.oid = D.objid AND D.deptype = 'a') LEFT JOIN pg_namespace N on (N.oid = S.relnamespace) WHERE S.relkind = " + + "'S' AND D.objid IS NULL"); if (schemaName != null && !schemaName.isBlank()) { sequenceSqlBuilder.append(" AND N.nspname=?"); 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 5cf9f6ff4..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 @@ -62,7 +62,8 @@ public class TestPostgreSqlMetaDataProvider { + " 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' AND n.nspname = ?"; + + " 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); @@ -80,7 +81,7 @@ public void before() throws SQLException { 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(connection.prepareStatement(SCHEMA_EXTENSION_RELATIONS_SQL)).thenReturn(extensionStatement); when(extensionStatement.executeQuery()).thenReturn(extensionRelations); when(extensionRelations.next()).thenReturn(false); } @@ -120,7 +121,7 @@ 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(EXTENSION_RELATIONS_SQL)).thenReturn(extensionStatement); + 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"); @@ -144,6 +145,35 @@ public void testExtensionRelationsAreSystemRelations() throws SQLException { 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 *