From add0b19b702b4eec9d274869b8b9d5c47edcfe1d Mon Sep 17 00:00:00 2001 From: jsimlo Date: Mon, 20 Jun 2022 14:07:42 +0100 Subject: [PATCH 01/25] Introducing additional technical indexes to Postgres to support nullable columns in unique indexes the same way Oracle does. --- .../jdbc/postgresql/PostgreSQLDialect.java | 50 +- ...ueIndexAdditionalDeploymentStatements.java | 501 ++++++++++++++++++ .../postgresql/TestPostgreSQLDialect.java | 18 +- 3 files changed, 558 insertions(+), 11 deletions(-) create mode 100644 morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java index f70f7392d..31b4486c1 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java @@ -44,6 +44,7 @@ import org.apache.commons.lang3.StringUtils; import com.google.common.base.Joiner; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -243,15 +244,6 @@ public Collection renameTableStatements(Table from, Table to) { } - @Override - public Collection renameIndexStatements(Table table, String fromIndexName, String toIndexName) { - return ImmutableList.builder() - .addAll(super.renameIndexStatements(table, fromIndexName, toIndexName)) - .add(addIndexComment(toIndexName)) - .build(); - } - - private Collection renameSequenceStatements(String fromSeqName, String toSeqName) { return ImmutableList.of(String.format("ALTER SEQUENCE %s RENAME TO %s", fromSeqName, toSeqName)); } @@ -635,15 +627,55 @@ protected Collection indexDeploymentStatements(Table table, Index index) return ImmutableList.builder() .add(statement.toString()) .add(addIndexComment(index.getName())) + .addAll(additionalUniqueIndexDeploymentStatements(table, index)) .build(); } + @Override + public Collection renameIndexStatements(Table table, String fromIndexName, String toIndexName) { + return ImmutableList.builder() + .addAll(super.renameIndexStatements(table, fromIndexName, toIndexName)) + .add(addIndexComment(toIndexName)) + .addAll(additionalUniqueIndexRenameStatements(table, fromIndexName, toIndexName)) + .build(); + } + + + @Override + public Collection indexDropStatements(Table table, Index indexToBeRemoved) { + return ImmutableList.builder() + .addAll(super.indexDropStatements(table, indexToBeRemoved)) + .addAll(additionalUniqueIndexDropStatements(table, indexToBeRemoved)) + .build(); + } + + private String addIndexComment(String indexName) { return "COMMENT ON INDEX " + indexName + " IS '"+REAL_NAME_COMMENT_LABEL+":[" + indexName + "]'"; } + private Iterable additionalUniqueIndexDeploymentStatements(Table table, Index index) { + return new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index).createIndexStatements(schemaNamePrefix(table) + table.getName()); + } + + + private Iterable additionalUniqueIndexRenameStatements(Table table, String fromIndexName, String toIndexName) { + if (toIndexName.endsWith("_pk") && fromIndexName.equals(table.getName() + "_pk")) { + return ImmutableList.of(); + } + + Index index = Iterables.getOnlyElement(FluentIterable.from(table.indexes()).filter(i -> toIndexName.equalsIgnoreCase(i.getName()))); + return new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index).renameIndexStatements(schemaNamePrefix(table) + table.getName(), fromIndexName, toIndexName); + } + + + private Iterable additionalUniqueIndexDropStatements(Table table, Index index) { + return new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index).dropIndexStatements(schemaNamePrefix(table) + table.getName()); + } + + @Override public void prepareStatementParameters(NamedParameterPreparedStatement statement, DataValueLookup values, SqlParameter parameter) throws SQLException { switch (parameter.getMetadata().getType()) { diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java new file mode 100644 index 000000000..329d9a918 --- /dev/null +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -0,0 +1,501 @@ +package org.alfasoftware.morf.jdbc.postgresql; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfasoftware.morf.jdbc.SqlDialect; +import org.alfasoftware.morf.metadata.Column; +import org.alfasoftware.morf.metadata.Index; +import org.alfasoftware.morf.metadata.Table; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.google.common.base.Joiner; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; + +/** + * Helper class for producing additional supporting constraints + * for unique indexes containing nullable columns. + * + * @author Copyright (c) Alfa Financial Software Limited. 2022 + */ +class PostgreSQLUniqueIndexAdditionalDeploymentStatements { + + private static final int NULLABLE_COLUMNS_CUTOFF = 2; + + public static final String INDEX_COMMENT_LABEL = "NULLABLE"; + + private static final String INDEX_NAME_SUFFIX = "$null"; + private static final char MAGIC_GLUE = '\u00A7'; //This is the section symbol § + + private static final Pattern ADDITIONAL_INDEX_NAME_MATCHER = Pattern.compile("(.*)\\Q" + INDEX_NAME_SUFFIX + "\\E(\\d*)"); + + private static final Log log = LogFactory.getLog(PostgreSQLUniqueIndexAdditionalDeploymentStatements.class); + + private final Table table; + private final Index index; + + private final List nullableIndexColumns; + + private final Strategy strategy; + + public PostgreSQLUniqueIndexAdditionalDeploymentStatements(Table table, Index index) { + this.table = table; + this.index = index; + + this.nullableIndexColumns = extractNullableIndexColumns(table, index); + + // having too many nullable columns in a unique index would be expensive! + this.strategy = nullableIndexColumns.size() <= NULLABLE_COLUMNS_CUTOFF + ? new UsingConstellations() + : new UsingConcatenation(); + } + + + /** + * Different type of indexes use different implementations of {@link Strategy} to solve the problem. + */ + private interface Strategy { + + public Iterable createIndexStatements(String prefixedTableName); + + public Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName); + + public Iterable dropIndexStatements(String prefixedTableName); + + public boolean healIndexStatementsNeeded(Collection additionalConstraintIndexInfos); + + public Iterable healIndexStatements(Collection additionalConstraintIndexInfos, String prefixedTableName); + } + + + /** + * Strategy, which creates an additional unique index for each different constellation + * of the nullable columns from the unique index. This is the cleanest solution, which + * also allows for some interesting database optimisations, and may potentially avoid + * some pitfalls. + * + * Sample result (with no non-nullable columns available): + *
+     * CREATE UNIQUE INDEX Test_NK ON schema.Test (stringField);
+     * CREATE UNIQUE INDEX Test_NK$null0 ON schema.Test ((0)) WHERE stringField IS NULL;
+     * 
+ * + * Sample result (floatField being the only non-nullable column): + *
+     * CREATE UNIQUE INDEX Test_1 ON schema.Test (intField, floatField);
+     * CREATE UNIQUE INDEX Test_1$null0 ON schema.Test (floatField) WHERE intField IS NULL;
+     * 
+ * + * Sample result (floatField being the only non-nullable column): + *
+     * CREATE UNIQUE INDEX indexName ON testschema.Test (stringField, intField, floatField, dateField);
+     * CREATE UNIQUE INDEX indexName$null0 ON testschema.Test (intField, floatField, dateField) WHERE stringField IS NULL;
+     * CREATE UNIQUE INDEX indexName$null01 ON testschema.Test (floatField, dateField) WHERE stringField IS NULL AND intField IS NULL;
+     * CREATE UNIQUE INDEX indexName$null1 ON testschema.Test (stringField, floatField, dateField) WHERE intField IS NULL;
+     * 
+ * + * Note: This strategy can also easily handle trivial cases: non-unique indexes, non-nullable unique indexes, etc. + */ + private class UsingConstellations implements Strategy { + + private final List> constellations; + + + public UsingConstellations() { + this.constellations = ImmutableList.copyOf(enumerateNullableIndexColumns(FluentIterable.of(), Range.closed(0, nullableIndexColumns.size()))); + } + + + @Override + public Iterable createIndexStatements(String prefixedTableName) { + return FluentIterable.from(constellations) + .transformAndConcat(nullColumns -> createIndexStatement(prefixedTableName, nullColumns)); + } + + + private Iterable createIndexStatement(String prefixedTableName, Iterable nullColumns) { + final Iterator nullColumnsIterator = nullColumns.iterator(); + String nextNullColumn = nullableIndexColumns.get(nullColumnsIterator.next()); + + final List indexColumns = new ArrayList<>(); + final List whereColumns = new ArrayList<>(); + for (String column : index.columnNames()) { + if (column.equals(nextNullColumn)) { + // add to the where clause + whereColumns.add(column); + + // move to the next null column + nextNullColumn = nullColumnsIterator.hasNext() + ? nullableIndexColumns.get(nullColumnsIterator.next()) + : null; + } + else { + // add to the indexed columns + indexColumns.add(column); + } + } + + // no columns left to be indexed? + if (indexColumns.isEmpty()) { + // index needs a value to index, use a constant value + indexColumns.add("(0)"); + } + + String fullIndexName = makeIndexName(nullColumns); + String indexHash = makeIndexHash(nullColumns); + + String createStatement = + "CREATE UNIQUE INDEX " + fullIndexName + " ON " + prefixedTableName + + " (" + Joiner.on(", ").join(indexColumns) + ")" + + " WHERE " + Joiner.on(" IS NULL AND ").join(whereColumns) + " IS NULL"; + + String commentStatement = + "COMMENT ON INDEX " + fullIndexName + " IS '" + SqlDialect.REAL_NAME_COMMENT_LABEL + ":[" + indexHash + "]'"; + + return ImmutableList.of(createStatement, commentStatement); + } + + + @Override + public Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName) { + return FluentIterable.from(constellations) + .transformAndConcat(nullColumns -> renameIndexStatements(prefixedTableName, fromIndexName, toIndexName, nullColumns)); + } + + + private Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName, Iterable nullColumns) { + String indexNameSuffix = makeIndexSuffix(nullColumns); + String alterStatement = "ALTER INDEX IF EXISTS " + prefixedTableName + fromIndexName + indexNameSuffix + " RENAME TO " + toIndexName + indexNameSuffix; + return ImmutableList.of(alterStatement); + } + + + @Override + public Iterable dropIndexStatements(String prefixedTableName) { + return FluentIterable.from(constellations) + .transformAndConcat(nullColumns -> dropIndexStatements(prefixedTableName, nullColumns)); + } + + + private Iterable dropIndexStatements(@SuppressWarnings("unused") String prefixedTableName, Iterable nullColumns) { + String dropStatement = "DROP INDEX IF EXISTS " + makeIndexName(nullColumns); + return ImmutableList.of(dropStatement); + } + + + @Override + public boolean healIndexStatementsNeeded(Collection additionalConstraintIndexInfos) { + Set> existingIndexes = FluentIterable.from(additionalConstraintIndexInfos) + .transform(info -> new ImmutablePair<>(info.getIndexName(), info.getIndexHash())) + .toSet(); // removes duplicates + + Set> expectedIndexes = FluentIterable.from(constellations) + .transform(nullColumns -> new ImmutablePair<>(makeIndexName(nullColumns).toLowerCase(), makeIndexHash(nullColumns))) + .toSet(); + + return !expectedIndexes.equals(existingIndexes); + } + + + @Override + public Iterable healIndexStatements(Collection additionalConstraintIndexInfos, String prefixedTableName) { + // For simplicity, let's just drop all the existing indexes and create new ones, which will always work. + // It could be that some of the indexes could perhaps be kept, but currently the chances of that are expected to be tiny. + // To keep some of the existing indexes, we would need to work out which ones to drop, which ones to keep, and which ones to create. + + Set dropOldIndexes = FluentIterable.from(additionalConstraintIndexInfos) + .transform(info -> "DROP INDEX IF EXISTS " + info.getIndexName()) + .toSet(); // removes duplicates + + return FluentIterable.from(dropOldIndexes) + .append(createIndexStatements(prefixedTableName)); + } + + + private String makeIndexSuffix(Iterable nullColumns) { + return INDEX_NAME_SUFFIX + Joiner.on("").join(nullColumns); + } + + + private String makeIndexName(Iterable nullColumns) { + return index.getName() + makeIndexSuffix(nullColumns); + } + + + private String makeIndexHash(Iterable nullColumns) { + return calculateIndexHashVariation(index.columnNames(), nullableIndexColumns, nullColumns); + } + } + + + /** + * Strategy, suitable for many-nullable-columns unique indexes, which uses + * a single concatenated value of all columns to achieve uniqueness. This + * has potential downsides, for example, it requires {@link MAGIC_GLUE}, a + * magic value joiner character. + * + * Sample result (floatField being the only non-nullable column): + *
+     * CREATE UNIQUE INDEX indexName ON testschema.Test (stringField, intField, floatField, dateField);
+     * CREATE UNIQUE INDEX indexName$null ON testschema.Test ((stringField ||'§'|| intField ||'§'|| floatField ||'§'|| dateField)) WHERE stringField IS NULL OR intField IS NULL OR dateField IS NULL;
+     * 
+ * + * Important: This strategy does not try to handle trivial cases: non-unique indexes, non-nullable unique indexes, etc. + */ + private class UsingConcatenation implements Strategy { + + public UsingConcatenation() { + if (nullableIndexColumns.isEmpty()) { + throw new UnsupportedOperationException("This strategy does not handle trivial cases; use a different one"); + } + } + + @Override + public Iterable createIndexStatements(String prefixedTableName) { + String fullIndexName = index.getName() + INDEX_NAME_SUFFIX; + String indexHash = calculateIndexHash(index.columnNames(), nullableIndexColumns); + + String createStatement = + "CREATE UNIQUE INDEX " + fullIndexName + " ON " + prefixedTableName + + " ((" + Joiner.on(" ||'" + MAGIC_GLUE + "'|| ").join(index.columnNames()) + "))" + + " WHERE " + Joiner.on(" IS NULL OR ").join(nullableIndexColumns) + " IS NULL"; + + String commentStatement = + "COMMENT ON INDEX " + fullIndexName + " IS '" + SqlDialect.REAL_NAME_COMMENT_LABEL + ":[" + indexHash + "]'"; + + return ImmutableList.of(createStatement, commentStatement); + } + + + @Override + public Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName) { + String alterStatement = "ALTER INDEX IF EXISTS " + prefixedTableName + fromIndexName + INDEX_NAME_SUFFIX + " RENAME TO " + toIndexName + INDEX_NAME_SUFFIX; + return ImmutableList.of(alterStatement); + } + + + @Override + public Iterable dropIndexStatements(String prefixedTableName) { + String dropStatement = "DROP INDEX IF EXISTS " + index.getName() + INDEX_NAME_SUFFIX; + return ImmutableList.of(dropStatement); + } + + + @Override + public boolean healIndexStatementsNeeded(Collection additionalConstraintIndexInfos) { + String fullIndexName = (index.getName() + INDEX_NAME_SUFFIX).toLowerCase(); + String indexHash = calculateIndexHash(index.columnNames(), nullableIndexColumns); + + if (additionalConstraintIndexInfos.size() != 1) { + return true; + } + + return !additionalConstraintIndexInfos.stream() + .allMatch(info -> info.getIndexName().equals(fullIndexName) && info.getIndexHash().equals(indexHash)); + } + + + @Override + public Iterable healIndexStatements(Collection additionalConstraintIndexInfos, String prefixedTableName) { + Set dropOldIndexes = FluentIterable.from(additionalConstraintIndexInfos) + .transform(info -> "DROP INDEX IF EXISTS " + info.getIndexName()) + .toSet(); // removes duplicates + + return FluentIterable.from(dropOldIndexes) + .append(createIndexStatements(prefixedTableName)); + } + } + + + public Iterable createIndexStatements(String prefixedTableName) { + return strategy.createIndexStatements(prefixedTableName); + } + + + public Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName) { + return strategy.renameIndexStatements(prefixedTableName, fromIndexName, toIndexName); + } + + + public Iterable dropIndexStatements(String prefixedTableName) { + return strategy.dropIndexStatements(prefixedTableName); + } + + + public Iterable healIndexStatements(Collection additionalConstraintIndexInfos, String prefixedTableName) { + return healIndexStatementsNeeded(additionalConstraintIndexInfos) + ? strategy.healIndexStatements(additionalConstraintIndexInfos, prefixedTableName) + : ImmutableList.of(); + } + + + private boolean healIndexStatementsNeeded(Collection additionalConstraintIndexInfos) { + boolean healIndexStatementsNeeded = strategy.healIndexStatementsNeeded(additionalConstraintIndexInfos); + if (healIndexStatementsNeeded) { + log.info(strategy.getClass().getSimpleName() + ".healIndexStatementsNeeded(" + additionalConstraintIndexInfos + ")"); + } + return healIndexStatementsNeeded; + } + + + /** + * For a unique index with at least one nullable column, + * lists all the nullable column names of the index. + */ + private static List extractNullableIndexColumns(Table table, Index index) { + // this only concerns unique indexes + if (!index.isUnique()) { + return ImmutableList.of(); + } + + // get the list of nullable columns + final Set nullableColumnNames = table.columns().stream() + .filter(Column::isNullable) + .map(Column::getName) + .collect(toSet()); + + final List nullableIndexColumns = index.columnNames().stream() + .filter(nullableColumnNames::contains) + .collect(toList()); + + // this only concerns nullable columns in those unique indexes + if (nullableIndexColumns.isEmpty()) { + return ImmutableList.of(); + } + + // allow logging for full analysis + if (log.isDebugEnabled()) log.debug("Unique index [" + index.getName() + "] contains " + nullableIndexColumns.size() + " nullable columns: " + nullableIndexColumns); + + return nullableIndexColumns; + } + + + /** + * Produces a "power set" from the given range of integers. The empty subset is excluded. + * + * For range 0-0 produces empty iterable. + * For range 0-1 produces iterable with (1). + * For range 0-2 produces iterable with (1), (2), (1, 2). + * For range 0-3 produces iterable with (1), (2), (3), (1,2), (1,3), (2,3), (1,2,3). + * Etc. + */ + private static Iterable> enumerateNullableIndexColumns(FluentIterable nullColumns, Range nullableColumns) { + FluentIterable> constellations = FluentIterable.of(); + + for (int i = nullableColumns.lowerEndpoint(); i < nullableColumns.upperEndpoint(); i++) { + // add the next null column to the other null columns + FluentIterable newNullColumns = nullColumns.append(ImmutableList.of(i)); + + // add this constellation to the results + constellations = constellations.append(ImmutableList.of(newNullColumns)); + + // recurse from this constellation + constellations = constellations.append(enumerateNullableIndexColumns(newNullColumns, Range.closed(i + 1, nullableColumns.upperEndpoint()))); + } + + return constellations; + } + + + private static String calculateIndexHash(Iterable allColumnNames, Iterable allNullableColumnNames) { + // List all the index column names, in their appearance order. + // This ensures the hash changes whenever any columns change in the index. + // Note: This should be the same for all index constellations. + final String allColumns = Joiner.on('|').join(allColumnNames); + + // List all the nullable index column names, in their appearance order. + // This ensures the hash changes whenever any column nullability changes. + // Note: This should be the same for all index constellations. + final String allNullableColumns = Joiner.on('|').join(allNullableColumnNames); + + // Put it all together + return DigestUtils.md5Hex(allColumns + '/' + allNullableColumns); + } + + + private static String calculateIndexHashVariation(Iterable allColumnNames, Iterable allNullableColumnNames, Iterable nulledColumnNames) { + // List the columns picked to be nulled within this particular constellation. + // Note: This should tell various constellations/variations apart. + final String nulledColumns = Joiner.on('|').join(nulledColumnNames); + + // Put it all together + return calculateIndexHash(allColumnNames, allNullableColumnNames) + '/' + DigestUtils.md5Hex(nulledColumns); + } + + + /** + * Recognises a name of additional supporting constraint index. + * + * @param indexName Index name as read from the database. + * @param indexHash Index hash as read from the index comment. + * @param indexResultSet Further information available on the index. + * @return Optional.empty() if the index name is not an additional supporting constraint. + * Optional.isPresent() if the index name is an additional supporting constraint. + * This return should then be used for @TODO to validate and heal the supporting indexes. + */ + public static Optional matchAdditionalIndex(String indexName, String indexHash) throws SQLException { + Matcher matcher = ADDITIONAL_INDEX_NAME_MATCHER.matcher(indexName); + if (matcher.matches()) { + String baseName = matcher.group(1); + String suffixName = matcher.group(2); + return Optional.of(new AdditionalIndexInfo(indexName, baseName, suffixName, indexHash)); + } + return Optional.empty(); + } + + + /** + * Simple POJO for holding additional info about indexes and their columns, + * as provided by the JDBC. + */ + public static class AdditionalIndexInfo { + + private final String indexName; + private final String baseName; + private final String suffixName; + private final String indexHash; + + public AdditionalIndexInfo(String indexName, String baseName, String suffixName, String indexHash) throws SQLException { + this.indexName = indexName.toLowerCase(); + this.baseName = baseName.toLowerCase(); + this.suffixName = suffixName; + this.indexHash = indexHash; + } + + public String getIndexName() { + return indexName; + } + + public String getBaseName() { + return baseName; + } + + public String getSuffixName() { + return suffixName; + } + + public String getIndexHash() { + return indexHash; + } + + @Override + public String toString() { + return "{" + indexName + ", " + baseName + "/" + INDEX_NAME_SUFFIX + "/" + suffixName + ", " + indexHash + "}"; + } + } + } diff --git a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java index ae7914975..8ea4711ff 100644 --- a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java +++ b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java @@ -86,8 +86,12 @@ protected List expectedCreateTableStatements() { "COMMENT ON COLUMN testschema.Test.clobField IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[clobField]/TYPE:[CLOB]'", "CREATE UNIQUE INDEX Test_NK ON testschema.Test (stringField)", "COMMENT ON INDEX Test_NK IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[Test_NK]'", + "CREATE UNIQUE INDEX Test_NK$null0 ON testschema.Test ((0)) WHERE stringField IS NULL", + "COMMENT ON INDEX Test_NK$null0 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[e069928444ba25d94fe4d5f64e1423ae/cfcd208495d565ef66e7dff9f98764da]'", "CREATE UNIQUE INDEX Test_1 ON testschema.Test (intField, floatField)", "COMMENT ON INDEX Test_1 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[Test_1]'", + "CREATE UNIQUE INDEX Test_1$null0 ON testschema.Test (floatField) WHERE intField IS NULL", + "COMMENT ON INDEX Test_1$null0 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[9c2e9ac2612f55025a2656b5c2cc9316/cfcd208495d565ef66e7dff9f98764da]'", "CREATE TABLE testschema.Alternate (id NUMERIC(19) NOT NULL, version INTEGER DEFAULT 0, stringField VARCHAR(3) COLLATE \"POSIX\", CONSTRAINT Alternate_PK PRIMARY KEY(id))", "COMMENT ON TABLE testschema.Alternate IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[Alternate]'", "COMMENT ON COLUMN testschema.Alternate.id IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[id]/TYPE:[BIG_INTEGER]'", @@ -141,6 +145,8 @@ protected List expectedCreateTemporaryTableStatements() { "COMMENT ON COLUMN TempTest.clobField IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[clobField]/TYPE:[CLOB]'", "CREATE UNIQUE INDEX TempTest_NK ON TempTest (stringField)", "COMMENT ON INDEX TempTest_NK IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[TempTest_NK]'", + "CREATE UNIQUE INDEX TempTest_NK$null0 ON TempTest ((0)) WHERE stringField IS NULL", + "COMMENT ON INDEX TempTest_NK$null0 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[e069928444ba25d94fe4d5f64e1423ae/cfcd208495d565ef66e7dff9f98764da]'", "CREATE INDEX TempTest_1 ON TempTest (intField, floatField)", "COMMENT ON INDEX TempTest_1 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[TempTest_1]'", "CREATE TEMP TABLE TempAlternate (id NUMERIC(19) NOT NULL, version INTEGER DEFAULT 0, stringField VARCHAR(3) COLLATE \"POSIX\", CONSTRAINT TempAlternate_PK PRIMARY KEY(id))", @@ -182,6 +188,8 @@ protected List expectedCreateTableStatementsWithLongTableName() { "COMMENT ON COLUMN testschema.tableWithANameThatExceedsTwentySevenCharactersToMakeSureSchemaNameDoesNotGetFactoredIntoOracleNameTruncation.charField IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[charField]/TYPE:[STRING]'", "CREATE UNIQUE INDEX Test_NK ON testschema.tableWithANameThatExceedsTwentySevenCharactersToMakeSureSchemaNameDoesNotGetFactoredIntoOracleNameTruncation (stringField)", "COMMENT ON INDEX Test_NK IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[Test_NK]'", + "CREATE UNIQUE INDEX Test_NK$null0 ON testschema.tableWithANameThatExceedsTwentySevenCharactersToMakeSureSchemaNameDoesNotGetFactoredIntoOracleNameTruncation ((0)) WHERE stringField IS NULL", + "COMMENT ON INDEX Test_NK$null0 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[e069928444ba25d94fe4d5f64e1423ae/cfcd208495d565ef66e7dff9f98764da]'", "CREATE INDEX Test_1 ON testschema.tableWithANameThatExceedsTwentySevenCharactersToMakeSureSchemaNameDoesNotGetFactoredIntoOracleNameTruncation (intField, floatField)", "COMMENT ON INDEX Test_1 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[Test_1]'" ); @@ -736,6 +744,7 @@ protected List expectedAlterTableAlterColumnWithDefaultStatement() { protected List expectedChangeIndexFollowedByChangeOfAssociatedColumnStatement() { return Arrays.asList( "DROP INDEX Test_1", + "DROP INDEX IF EXISTS Test_1$null0", "CREATE INDEX Test_1 ON testschema.Test (intField)", "COMMENT ON INDEX Test_1 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[Test_1]'", "ALTER TABLE testschema.Test ALTER COLUMN intField SET NOT NULL", @@ -779,7 +788,10 @@ protected List expectedAddIndexStatementsUnique() { @Override protected List expectedAddIndexStatementsUniqueNullable() { return Arrays.asList("CREATE UNIQUE INDEX indexName ON testschema.Test (stringField, intField, floatField, dateField)", - "COMMENT ON INDEX indexName IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[indexName]'"); + "COMMENT ON INDEX indexName IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[indexName]'", + "CREATE UNIQUE INDEX indexName$null ON testschema.Test ((stringField ||'§'|| intField ||'§'|| floatField ||'§'|| dateField)) WHERE stringField IS NULL OR intField IS NULL OR dateField IS NULL", + "COMMENT ON INDEX indexName$null IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[121e97b2b38f63cc6840445595b4177c]'" + ); } @@ -1133,7 +1145,9 @@ protected List getRenamingTableWithLongNameStatements() { @Override protected List expectedRenameIndexStatements() { return ImmutableList.of("ALTER INDEX testschema.Test_1 RENAME TO Test_2", - "COMMENT ON INDEX Test_2 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[Test_2]'"); + "COMMENT ON INDEX Test_2 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[Test_2]'", + "ALTER INDEX IF EXISTS testschema.TestTest_1$null0 RENAME TO Test_2$null0" + ); } From 16ba3357e789a4743bee644498b438425a8fda99 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Mon, 20 Jun 2022 14:27:47 +0100 Subject: [PATCH 02/25] Reading metadata for additional technical Postgres indexes, which support nullable columns in unique indexes the same way Oracle does. --- .../morf/jdbc/DatabaseMetaDataProvider.java | 21 +++++++++- .../jdbc/DatabaseMetaDataProviderUtils.java | 10 ++++- .../PostgreSQLMetaDataProvider.java | 38 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java index a146d2a96..4256a8b02 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java @@ -666,6 +666,7 @@ protected List loadTableIndexes(RealName tableName) { continue; } if (DatabaseMetaDataProviderUtils.shouldIgnoreIndex(indexName.getDbName())) { + ignoreIndexName(indexName, indexResultSet); continue; } @@ -686,6 +687,9 @@ protected List loadTableIndexes(RealName tableName) { catch (SQLException e) { throw new RuntimeSqlException("Error reading metadata for index ["+indexName+"] on table ["+tableName+"]", e); } + catch (RuntimeException e) { + throw new RuntimeException("Error reading metadata for index ["+indexName+"] on table ["+tableName+"]", e); + } } return indexColumns.entrySet().stream() @@ -712,6 +716,21 @@ protected RealName readIndexName(ResultSet indexResultSet) throws SQLException { } + /** + * Receives an ignored index name. + * This can be overridden to process technical indexes, or remember performance indexes, etc. + * + * Note: This can be called multiple times for the same index! + * + * @param indexName name of the ignored index. + * @param indexResultSet + * @throws SQLException Upon errors. + */ + protected void ignoreIndexName(RealName indexName, ResultSet indexResultSet) throws SQLException { + if (log.isDebugEnabled()) log.debug("Ignored index [" + indexName.getRealName() + "] column [" + indexResultSet.getString(INDEX_COLUMN_NAME) + "]"); + } + + /** * Identify whether this is the primary key for this table. * @@ -910,7 +929,7 @@ protected AName(String aName) { this.hashCode = aName.toLowerCase().hashCode(); } - protected String getAName() { + public String getAName() { return aName; } diff --git a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProviderUtils.java b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProviderUtils.java index 16a4cd1db..637d099d2 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProviderUtils.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProviderUtils.java @@ -36,6 +36,10 @@ public class DatabaseMetaDataProviderUtils { * Regex for extracting the data type from column comments */ private static final Pattern DATA_TYPE_REGEX = Pattern.compile("TYPE:\\[(\\w+)\\]"); + /** + * Regex for matching ignored indexes + */ + private static final Pattern IGNORE_INDEX_REGEX = Pattern.compile(".*_PRF\\d+$"); /** * Get the auto increment start value (if available) from the column comments @@ -72,10 +76,14 @@ public static Optional getDataTypeFromColumnComment(String columnComment * * eg. Schedule_PRF1 * + * Also any indexes with a $ character will be ignored: + * this allows technical indexes to be added to the schema. + * * @param indexName The name of an index * @return Whether it should be ignored */ public static boolean shouldIgnoreIndex(String indexName) { - return indexName.toUpperCase().matches(".*_PRF\\d+$"); + return IGNORE_INDEX_REGEX.matcher(indexName.toUpperCase()).matches() + || indexName.indexOf('$') > -1; } } 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 9a76adba5..f85ab1fea 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 @@ -8,14 +8,17 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; +import java.util.Collection; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.alfasoftware.morf.jdbc.DatabaseMetaDataProvider; import org.alfasoftware.morf.jdbc.RuntimeSqlException; +import org.alfasoftware.morf.jdbc.postgresql.PostgreSQLUniqueIndexAdditionalDeploymentStatements.AdditionalIndexInfo; import org.alfasoftware.morf.metadata.DataType; import org.alfasoftware.morf.metadata.SchemaUtils.ColumnBuilder; import org.apache.commons.lang3.StringUtils; @@ -23,6 +26,8 @@ import org.apache.commons.logging.LogFactory; import com.google.common.base.Suppliers; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; /** @@ -38,6 +43,8 @@ public class PostgreSQLMetaDataProvider extends DatabaseMetaDataProvider { private final Supplier> allIndexNames = Suppliers.memoize(this::loadAllIndexNames); + private final HashMultimap additionalConstraintIndexes = HashMultimap.create(); + public PostgreSQLMetaDataProvider(Connection connection, String schemaName) { super(connection, schemaName); } @@ -171,4 +178,35 @@ private String matchComment(String comment) { } return null; } + + + @Override + protected void ignoreIndexName(RealName indexName, ResultSet indexResultSet) throws SQLException { + super.ignoreIndexName(indexName, indexResultSet); + + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex(indexName.getDbName(), indexName.getRealName()) + .ifPresent(additionalIndexInfo -> { + if (log.isDebugEnabled()) { + log.debug("Accepted index [" + indexName + "] as supporting constraint for index [" + additionalIndexInfo.getBaseName() + "]"); + } + additionalConstraintIndexes.put(additionalIndexInfo.getBaseName().toLowerCase(), additionalIndexInfo); + }); + } + + + /** + * For a given unique constraint index base name, returns a collection of {@link AdditionalIndexInfo}, + * a pre-parsed POJOs describing ignored technical indexes recorded for the given constraint base name. + * + * Important: The {@link AdditionalIndexInfo} are gathered when reading the metadata of the underlying + * index. Therefore, this method might return an empty collection, if called too soon, before metadata + * of the index has been read. + * + * @param baseIndexName Name of the underlying unique index. + * @return Collection of additional technical indexes belonging to the underlying unique index. + */ + public Collection getAdditionalConstraintIndexes(String baseIndexName) { + Set infos = additionalConstraintIndexes.get(baseIndexName); + return infos == null ? ImmutableList.of() : infos; + } } From 7519e2171198cb394ba4860ff6e1709b5f8407e6 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Tue, 21 Jun 2022 17:04:56 +0100 Subject: [PATCH 03/25] Auto-healing additional technical Postgres indexes, which support nullable columns in unique indexes the same way Oracle does. --- .../morf/jdbc/SchemaResourceImpl.java | 12 ++++++ .../alfasoftware/morf/jdbc/SqlDialect.java | 12 ++++++ .../morf/metadata/SchemaResource.java | 13 ++++++ .../alfasoftware/morf/upgrade/Upgrade.java | 18 ++++---- .../jdbc/postgresql/PostgreSQLDialect.java | 41 +++++++++++++++++++ ...ueIndexAdditionalDeploymentStatements.java | 4 +- 6 files changed, 88 insertions(+), 12 deletions(-) diff --git a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SchemaResourceImpl.java b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SchemaResourceImpl.java index ecf7c9fdc..34d41cf39 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SchemaResourceImpl.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SchemaResourceImpl.java @@ -17,6 +17,7 @@ import java.sql.Connection; import java.sql.SQLException; +import java.util.Optional; import javax.sql.DataSource; @@ -78,4 +79,15 @@ public void close() { throw new RuntimeSqlException("Closing", e); } } + + + @Override + public Optional getDatabaseMetaDataProvider() { + if (delegate instanceof DatabaseMetaDataProvider) { + DatabaseMetaDataProvider metaDataProvider = (DatabaseMetaDataProvider)delegate; + return Optional.of(metaDataProvider); + } + + return SchemaResource.super.getDatabaseMetaDataProvider(); + } } \ No newline at end of file diff --git a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java index f75ceeded..788b0f9df 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java @@ -43,6 +43,7 @@ import org.alfasoftware.morf.metadata.DataValueLookup; import org.alfasoftware.morf.metadata.Index; import org.alfasoftware.morf.metadata.Schema; +import org.alfasoftware.morf.metadata.SchemaResource; import org.alfasoftware.morf.metadata.SchemaUtils; import org.alfasoftware.morf.metadata.Table; import org.alfasoftware.morf.metadata.View; @@ -4081,6 +4082,17 @@ protected String getSqlForInsertInto(@SuppressWarnings("unused") InsertStatement } + /** + * Returns any statements needed to automatically heal the given schema. + * + * @param schema Schema to be examined. + * @return List of statements to be run. + */ + public List getSchemaConsistencyStatements(@SuppressWarnings("unused") SchemaResource schemaResource) { + return ImmutableList.of(); + } + + /** * Class representing the structor of an ID Table. * diff --git a/morf-core/src/main/java/org/alfasoftware/morf/metadata/SchemaResource.java b/morf-core/src/main/java/org/alfasoftware/morf/metadata/SchemaResource.java index 48e3b5ec8..64dccb6a9 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/metadata/SchemaResource.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/metadata/SchemaResource.java @@ -15,6 +15,10 @@ package org.alfasoftware.morf.metadata; +import java.util.Optional; + +import org.alfasoftware.morf.jdbc.DatabaseMetaDataProvider; + /** * Provides database meta data. * @@ -35,4 +39,13 @@ public interface SchemaResource extends Schema, AutoCloseable { @Override void close(); + + /** + * Returns the underlying {@link DatabaseMetaDataProvider}, if there is one. + * + * @return the underlying {@link DatabaseMetaDataProvider}, if there is one. + */ + default Optional getDatabaseMetaDataProvider() { + return Optional.empty(); + } } diff --git a/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java b/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java index d14cfa2a4..7b760920c 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java @@ -177,7 +177,7 @@ public UpgradePath findPath(Schema targetSchema, Collection exceptionRegexes) { - SchemaResource databaseSchemaResource = database.openSchemaResource(dataSource); - try { - return copy(databaseSchemaResource, exceptionRegexes); - } finally { - databaseSchemaResource.close(); + private static Schema readSourceDatabaseSchema(ConnectionResources database, DataSource dataSource, Collection exclusionRegExes, List upgradeStatements) { + try (SchemaResource databaseSchemaResource = database.openSchemaResource(dataSource)) { + upgradeStatements.addAll(database.sqlDialect().getSchemaConsistencyStatements(databaseSchemaResource)); + return copy(databaseSchemaResource, exclusionRegExes); } } } \ No newline at end of file diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java index 31b4486c1..d784583fa 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java @@ -15,10 +15,12 @@ import org.alfasoftware.morf.jdbc.DatabaseType; import org.alfasoftware.morf.jdbc.NamedParameterPreparedStatement; import org.alfasoftware.morf.jdbc.SqlDialect; +import org.alfasoftware.morf.jdbc.postgresql.PostgreSQLUniqueIndexAdditionalDeploymentStatements.AdditionalIndexInfo; import org.alfasoftware.morf.metadata.Column; import org.alfasoftware.morf.metadata.DataType; import org.alfasoftware.morf.metadata.DataValueLookup; import org.alfasoftware.morf.metadata.Index; +import org.alfasoftware.morf.metadata.SchemaResource; import org.alfasoftware.morf.metadata.SchemaUtils; import org.alfasoftware.morf.metadata.Table; import org.alfasoftware.morf.metadata.View; @@ -742,4 +744,43 @@ protected String getSqlFrom(DeleteStatement statement) { return sqlBuilder.toString(); } + + + @Override + public List getSchemaConsistencyStatements(SchemaResource schemaResource) { + return schemaResource.getDatabaseMetaDataProvider() + .map(PostgreSQLMetaDataProvider.class::cast) + .map(this::getSchemaConsistencyStatements) + .orElseGet(() -> super.getSchemaConsistencyStatements(schemaResource)); + } + + + private List getSchemaConsistencyStatements(PostgreSQLMetaDataProvider metaDataProvider) { + return FluentIterable.from(metaDataProvider.tables()) + .transformAndConcat(table -> healTable(metaDataProvider, table)) + .toList(); // turn all the concatenated fluent iterables into a firm immutable list + } + + + private Iterable healTable(PostgreSQLMetaDataProvider metaDataProvider, Table table) { + Iterable statements = healIndexes(metaDataProvider, table); + + if (statements.iterator().hasNext()) { + List intro = ImmutableList.of(convertCommentToSQL("Healing table: " + table.getName())); + return Iterables.concat(intro, statements); + } + return ImmutableList.of(); + } + + + private Iterable healIndexes(PostgreSQLMetaDataProvider metaDataProvider, Table table) { + return FluentIterable.from(table.indexes()) + .transformAndConcat(index -> healIndexes(metaDataProvider.getAdditionalConstraintIndexes(index.getName().toLowerCase()), table, index)); + } + + + private Iterable healIndexes(Collection additionalConstraintIndexInfo, Table table, Index index) { + return new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index) + .healIndexStatements(additionalConstraintIndexInfo, schemaNamePrefix(table) + table.getName()); + } } \ No newline at end of file diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java index 329d9a918..312c4b235 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -346,9 +346,7 @@ public Iterable healIndexStatements(Collection addi private boolean healIndexStatementsNeeded(Collection additionalConstraintIndexInfos) { boolean healIndexStatementsNeeded = strategy.healIndexStatementsNeeded(additionalConstraintIndexInfos); - if (healIndexStatementsNeeded) { - log.info(strategy.getClass().getSimpleName() + ".healIndexStatementsNeeded(" + additionalConstraintIndexInfos + ")"); - } + if (log.isDebugEnabled()) log.debug(strategy.getClass().getSimpleName() + ".healIndexStatementsNeeded: " + healIndexStatementsNeeded + ", " + additionalConstraintIndexInfos); return healIndexStatementsNeeded; } From 9617a75346172fc6363453ea01067641dfe34d5e Mon Sep 17 00:00:00 2001 From: jsimlo Date: Tue, 21 Jun 2022 17:37:15 +0100 Subject: [PATCH 04/25] Missing tests. --- .../morf/upgrade/TestUpgrade.java | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java b/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java index cf20c98fc..b6d11b3f9 100755 --- a/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java +++ b/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java @@ -169,6 +169,7 @@ public void testUpgrade() throws SQLException { SchemaResource schemaResource = mock(SchemaResource.class); when(mockConnectionResources.openSchemaResource(eq(mockConnectionResources.getDataSource()))).thenReturn(schemaResource); + when(mockConnectionResources.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of("I", "J")); when(schemaResource.tables()).thenReturn(tables); when(upgradeStatusTableService.getStatus(Optional.of(mockConnectionResources.getDataSource()))).thenReturn(NONE); @@ -176,7 +177,11 @@ public void testUpgrade() throws SQLException { .findPath(targetSchema, upgradeSteps, Lists.newArrayList("^Drivers$", "^EXCLUDE_.*$")); assertEquals("Should be two steps.", 2, results.getSteps().size()); - assertEquals("Number of SQL statements", 18, results.getSql().size()); // Includes statements to create, truncate and then drop temp table, also 2 comments + assertEquals("Number of SQL statements", 20, results.getSql().size()); // Includes statements to create, truncate and then drop temp table, also 2 comments + + assertEquals("SQL", "[I, J]", results.getSql().subList(0, 2).toString()); // schema consistency statements + assertEquals("SQL", "-- Upgrade step: org.alfasoftware.morf.upgrade.testupgrade.upgrade.v1_0_0.ChangeCar", results.getSql().get(3).toString()); // upgrade ChangeCar begins + assertEquals("SQL", "-- Upgrade step: org.alfasoftware.morf.upgrade.testupgrade.upgrade.v1_0_0.ChangeDriver", results.getSql().get(10).toString()); // upgrade ChangeDriver begins } @@ -278,6 +283,36 @@ public void testUpgradeWithNoStepsToApply() { } + /** + * Test that even if there are no upgrades to apply, + * but there are schema consistency statements to be deployed, + * that those schema consistency statements are still added to the resulting SQL. + */ + @Test + public void testUpgradeWithOnlySchemaConsistencyStatementsToDeploy() { + // Given + Table upgradeAudit = upgradeAudit(); + Schema sourceSchema = schema(upgradeAudit); + Schema targetSchema = schema(upgradeAudit); + + Collection> upgradeSteps = Collections.emptySet(); + + SchemaResource schemaResource = new StubSchemaResource(sourceSchema); + + ConnectionResources connection = mock(ConnectionResources.class, RETURNS_DEEP_STUBS); + when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(schemaResource); + when(connection.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of("I", "J")); + when(upgradeStatusTableService.getStatus(Optional.of(connection.getDataSource()))).thenReturn(NONE); + + // When + UpgradePath results = new Upgrade(connection, connection.getDataSource(), upgradePathFactory(), upgradeStatusTableService, new ViewChangesDeploymentHelper(connection.sqlDialect()), viewDeploymentValidator, graphBasedUpgradeScriptGeneratorFactory).findPath(targetSchema, upgradeSteps, new HashSet()); + + // Then + assertTrue("No steps to apply", results.getSteps().isEmpty()); + assertEquals("SQL", "[I, J]", results.getSql().toString()); + } + + /** * Test that if there are no upgrades to apply, but there is a new view, * that a pseudo-upgrade step is created and the SQL to apply the views defined. @@ -296,11 +331,14 @@ public void testUpgradeWithOnlyViewsToDeploy() { Collection> upgradeSteps = Collections.emptySet(); + SchemaResource schemaResource = new StubSchemaResource(sourceSchema); + ConnectionResources connection = mock(ConnectionResources.class, RETURNS_DEEP_STUBS); when(connection.sqlDialect().viewDeploymentStatements(same(testView))).thenReturn(ImmutableList.of("A")); when(connection.sqlDialect().viewDeploymentStatementsAsLiteral(any(View.class))).thenReturn(literal("W")); when(connection.sqlDialect().rebuildTriggers(any(Table.class))).thenReturn(Collections.emptyList()); - when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(new StubSchemaResource(sourceSchema)); + when(connection.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of()); + when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(schemaResource); when(upgradeStatusTableService.getStatus(Optional.of(connection.getDataSource()))).thenReturn(NONE); // When @@ -337,12 +375,15 @@ public void testUpgradeWithChangedViewsToDeploy() { Collection> upgradeSteps = Collections.emptySet(); + SchemaResource schemaResource = new StubSchemaResource(sourceSchema); + ConnectionResources connection = mock(ConnectionResources.class, RETURNS_DEEP_STUBS); when(connection.sqlDialect().dropStatements(any(View.class))).thenReturn(ImmutableList.of("X")); when(connection.sqlDialect().viewDeploymentStatements(same(testView))).thenReturn(ImmutableList.of("A")); when(connection.sqlDialect().viewDeploymentStatementsAsLiteral(any(View.class))).thenReturn(literal("W")); when(connection.sqlDialect().rebuildTriggers(any(Table.class))).thenReturn(Collections.emptyList()); - when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(new StubSchemaResource(sourceSchema)); + when(connection.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of()); + when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(schemaResource); when(upgradeStatusTableService.getStatus(Optional.of(connection.getDataSource()))).thenReturn(NONE); // When @@ -575,12 +616,15 @@ public void testUpgradeWithOnlyViewsToDeployWithExistingDeployedViews() { Collection> upgradeSteps = Collections.emptySet(); + SchemaResource schemaResource = new StubSchemaResource(sourceSchema); + ConnectionResources connection = mock(ConnectionResources.class, RETURNS_DEEP_STUBS); when(connection.sqlDialect().viewDeploymentStatements(same(testView))).thenReturn(ImmutableList.of("A")); when(connection.sqlDialect().viewDeploymentStatementsAsLiteral(any(View.class))).thenReturn(literal("W")); when(connection.sqlDialect().convertStatementToSQL(any(InsertStatement.class))).thenReturn(ImmutableList.of("C")); when(connection.sqlDialect().rebuildTriggers(any(Table.class))).thenReturn(Collections.emptyList()); - when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(new StubSchemaResource(sourceSchema)); + when(connection.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of()); + when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(schemaResource); when(upgradeStatusTableService.getStatus(Optional.of(connection.getDataSource()))).thenReturn(NONE); // When From 37c99d9ffeefdbfaeadd3483ed7e868942503686 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Wed, 22 Jun 2022 10:02:20 +0100 Subject: [PATCH 05/25] Javadoc fixes. --- .../org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java | 2 +- .../src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java index 4256a8b02..c3a5d4637 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java @@ -723,7 +723,7 @@ protected RealName readIndexName(ResultSet indexResultSet) throws SQLException { * Note: This can be called multiple times for the same index! * * @param indexName name of the ignored index. - * @param indexResultSet + * @param indexResultSet resultset on the row with further index information * @throws SQLException Upon errors. */ protected void ignoreIndexName(RealName indexName, ResultSet indexResultSet) throws SQLException { diff --git a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java index 788b0f9df..e7e7557f8 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java @@ -4085,7 +4085,7 @@ protected String getSqlForInsertInto(@SuppressWarnings("unused") InsertStatement /** * Returns any statements needed to automatically heal the given schema. * - * @param schema Schema to be examined. + * @param schemaResource Schema resource that can be examined. * @return List of statements to be run. */ public List getSchemaConsistencyStatements(@SuppressWarnings("unused") SchemaResource schemaResource) { From 1df4c93c52633aa81fb476e799b6510777c3d306 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Wed, 22 Jun 2022 10:14:43 +0100 Subject: [PATCH 06/25] Sonar fixes. --- ...UniqueIndexAdditionalDeploymentStatements.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java index 312c4b235..6f8785051 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -188,11 +188,11 @@ private Iterable renameIndexStatements(String prefixedTableName, String @Override public Iterable dropIndexStatements(String prefixedTableName) { return FluentIterable.from(constellations) - .transformAndConcat(nullColumns -> dropIndexStatements(prefixedTableName, nullColumns)); + .transformAndConcat(nullColumns -> dropIndexStatements(nullColumns)); } - private Iterable dropIndexStatements(@SuppressWarnings("unused") String prefixedTableName, Iterable nullColumns) { + private Iterable dropIndexStatements(Iterable nullColumns) { String dropStatement = "DROP INDEX IF EXISTS " + makeIndexName(nullColumns); return ImmutableList.of(dropStatement); } @@ -346,7 +346,7 @@ public Iterable healIndexStatements(Collection addi private boolean healIndexStatementsNeeded(Collection additionalConstraintIndexInfos) { boolean healIndexStatementsNeeded = strategy.healIndexStatementsNeeded(additionalConstraintIndexInfos); - if (log.isDebugEnabled()) log.debug(strategy.getClass().getSimpleName() + ".healIndexStatementsNeeded: " + healIndexStatementsNeeded + ", " + additionalConstraintIndexInfos); + if (log.isDebugEnabled()) log.debug(strategy.getClass().getSimpleName() + ".healIndexStatementsNeeded(" + table.getName() + ": " + index.getName() + "): " + healIndexStatementsNeeded + ", " + additionalConstraintIndexInfos); return healIndexStatementsNeeded; } @@ -377,7 +377,7 @@ private static List extractNullableIndexColumns(Table table, Index index } // allow logging for full analysis - if (log.isDebugEnabled()) log.debug("Unique index [" + index.getName() + "] contains " + nullableIndexColumns.size() + " nullable columns: " + nullableIndexColumns); + if (log.isDebugEnabled()) log.debug("Unique index [" + table.getName() + ": " + index.getName() + "] contains " + nullableIndexColumns.size() + " nullable columns: " + nullableIndexColumns); return nullableIndexColumns; } @@ -441,10 +441,9 @@ private static String calculateIndexHashVariation(Iterable allColumnName * * @param indexName Index name as read from the database. * @param indexHash Index hash as read from the index comment. - * @param indexResultSet Further information available on the index. * @return Optional.empty() if the index name is not an additional supporting constraint. - * Optional.isPresent() if the index name is an additional supporting constraint. - * This return should then be used for @TODO to validate and heal the supporting indexes. + * Optional.isPresent() if the index name is an additional supporting constraint. + * @see #healIndexStatements(Collection, String) */ public static Optional matchAdditionalIndex(String indexName, String indexHash) throws SQLException { Matcher matcher = ADDITIONAL_INDEX_NAME_MATCHER.matcher(indexName); @@ -468,7 +467,7 @@ public static class AdditionalIndexInfo { private final String suffixName; private final String indexHash; - public AdditionalIndexInfo(String indexName, String baseName, String suffixName, String indexHash) throws SQLException { + public AdditionalIndexInfo(String indexName, String baseName, String suffixName, String indexHash) { this.indexName = indexName.toLowerCase(); this.baseName = baseName.toLowerCase(); this.suffixName = suffixName; From 44720e970de6f457758b1d8ba66233aebad0d5f1 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Wed, 22 Jun 2022 10:28:42 +0100 Subject: [PATCH 07/25] Sonar fix. --- ...ueIndexAdditionalDeploymentStatements.java | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java index 6f8785051..aa21e37b2 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -160,12 +160,13 @@ private Iterable createIndexStatement(String prefixedTableName, Iterable String indexHash = makeIndexHash(nullColumns); String createStatement = - "CREATE UNIQUE INDEX " + fullIndexName + " ON " + prefixedTableName - + " (" + Joiner.on(", ").join(indexColumns) + ")" - + " WHERE " + Joiner.on(" IS NULL AND ").join(whereColumns) + " IS NULL"; + createIndexSqlStatement( + fullIndexName, prefixedTableName, + Joiner.on(", ").join(indexColumns), + Joiner.on(" IS NULL AND ").join(whereColumns) + " IS NULL" + ); - String commentStatement = - "COMMENT ON INDEX " + fullIndexName + " IS '" + SqlDialect.REAL_NAME_COMMENT_LABEL + ":[" + indexHash + "]'"; + String commentStatement = commentOnIndexSqlStatement(fullIndexName, indexHash); return ImmutableList.of(createStatement, commentStatement); } @@ -180,7 +181,7 @@ public Iterable renameIndexStatements(String prefixedTableName, String f private Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName, Iterable nullColumns) { String indexNameSuffix = makeIndexSuffix(nullColumns); - String alterStatement = "ALTER INDEX IF EXISTS " + prefixedTableName + fromIndexName + indexNameSuffix + " RENAME TO " + toIndexName + indexNameSuffix; + String alterStatement = alterIndexSqlStatement(prefixedTableName + fromIndexName + indexNameSuffix, toIndexName + indexNameSuffix); return ImmutableList.of(alterStatement); } @@ -193,7 +194,7 @@ public Iterable dropIndexStatements(String prefixedTableName) { private Iterable dropIndexStatements(Iterable nullColumns) { - String dropStatement = "DROP INDEX IF EXISTS " + makeIndexName(nullColumns); + String dropStatement = dropIndexSqlStatement(makeIndexName(nullColumns)); return ImmutableList.of(dropStatement); } @@ -219,7 +220,7 @@ public Iterable healIndexStatements(Collection addi // To keep some of the existing indexes, we would need to work out which ones to drop, which ones to keep, and which ones to create. Set dropOldIndexes = FluentIterable.from(additionalConstraintIndexInfos) - .transform(info -> "DROP INDEX IF EXISTS " + info.getIndexName()) + .transform(info -> dropIndexSqlStatement(info.getIndexName())) .toSet(); // removes duplicates return FluentIterable.from(dropOldIndexes) @@ -271,12 +272,13 @@ public Iterable createIndexStatements(String prefixedTableName) { String indexHash = calculateIndexHash(index.columnNames(), nullableIndexColumns); String createStatement = - "CREATE UNIQUE INDEX " + fullIndexName + " ON " + prefixedTableName - + " ((" + Joiner.on(" ||'" + MAGIC_GLUE + "'|| ").join(index.columnNames()) + "))" - + " WHERE " + Joiner.on(" IS NULL OR ").join(nullableIndexColumns) + " IS NULL"; + createIndexSqlStatement( + fullIndexName, prefixedTableName, + "(" + Joiner.on(" ||'" + MAGIC_GLUE + "'|| ").join(index.columnNames()) + ")", + Joiner.on(" IS NULL OR ").join(nullableIndexColumns) + " IS NULL" + ); - String commentStatement = - "COMMENT ON INDEX " + fullIndexName + " IS '" + SqlDialect.REAL_NAME_COMMENT_LABEL + ":[" + indexHash + "]'"; + String commentStatement = commentOnIndexSqlStatement(fullIndexName, indexHash); return ImmutableList.of(createStatement, commentStatement); } @@ -284,14 +286,14 @@ public Iterable createIndexStatements(String prefixedTableName) { @Override public Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName) { - String alterStatement = "ALTER INDEX IF EXISTS " + prefixedTableName + fromIndexName + INDEX_NAME_SUFFIX + " RENAME TO " + toIndexName + INDEX_NAME_SUFFIX; + String alterStatement = alterIndexSqlStatement(prefixedTableName + fromIndexName + INDEX_NAME_SUFFIX, toIndexName + INDEX_NAME_SUFFIX); return ImmutableList.of(alterStatement); } @Override public Iterable dropIndexStatements(String prefixedTableName) { - String dropStatement = "DROP INDEX IF EXISTS " + index.getName() + INDEX_NAME_SUFFIX; + String dropStatement = dropIndexSqlStatement(index.getName() + INDEX_NAME_SUFFIX); return ImmutableList.of(dropStatement); } @@ -313,7 +315,7 @@ public boolean healIndexStatementsNeeded(Collection additio @Override public Iterable healIndexStatements(Collection additionalConstraintIndexInfos, String prefixedTableName) { Set dropOldIndexes = FluentIterable.from(additionalConstraintIndexInfos) - .transform(info -> "DROP INDEX IF EXISTS " + info.getIndexName()) + .transform(info -> dropIndexSqlStatement(info.getIndexName())) .toSet(); // removes duplicates return FluentIterable.from(dropOldIndexes) @@ -436,6 +438,26 @@ private static String calculateIndexHashVariation(Iterable allColumnName } + private static String createIndexSqlStatement(String fullIndexName, String prefixedTableName, String indexColumns, String indexWhereCondition) { + return "CREATE UNIQUE INDEX " + fullIndexName + " ON " + prefixedTableName + " (" + indexColumns + ")" + " WHERE " + indexWhereCondition; + } + + + private static String commentOnIndexSqlStatement(String fullIndexName, String indexHash) { + return "COMMENT ON INDEX " + fullIndexName + " IS '" + SqlDialect.REAL_NAME_COMMENT_LABEL + ":[" + indexHash + "]'"; + } + + + private static String alterIndexSqlStatement(String oldIndexName, String newIndexName) { + return "ALTER INDEX IF EXISTS " + oldIndexName + " RENAME TO " + newIndexName; + } + + + private static String dropIndexSqlStatement(String fullIndexName) { + return "DROP INDEX IF EXISTS " + fullIndexName; + } + + /** * Recognises a name of additional supporting constraint index. * From 5d55defbf5df11f66bd4b70d2723a86d4ebc27ac Mon Sep 17 00:00:00 2001 From: jsimlo Date: Wed, 22 Jun 2022 10:37:54 +0100 Subject: [PATCH 08/25] Sonar fixes. --- .../PostgreSQLUniqueIndexAdditionalDeploymentStatements.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java index aa21e37b2..d82a6aae4 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -3,7 +3,6 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; -import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -189,7 +188,7 @@ private Iterable renameIndexStatements(String prefixedTableName, String @Override public Iterable dropIndexStatements(String prefixedTableName) { return FluentIterable.from(constellations) - .transformAndConcat(nullColumns -> dropIndexStatements(nullColumns)); + .transformAndConcat(this::dropIndexStatements); } @@ -467,7 +466,7 @@ private static String dropIndexSqlStatement(String fullIndexName) { * Optional.isPresent() if the index name is an additional supporting constraint. * @see #healIndexStatements(Collection, String) */ - public static Optional matchAdditionalIndex(String indexName, String indexHash) throws SQLException { + public static Optional matchAdditionalIndex(String indexName, String indexHash) { Matcher matcher = ADDITIONAL_INDEX_NAME_MATCHER.matcher(indexName); if (matcher.matches()) { String baseName = matcher.group(1); From edbb25b627d43e43a9e3fb789e34eefcc2fc100d Mon Sep 17 00:00:00 2001 From: jsimlo Date: Thu, 23 Jun 2022 09:39:44 +0100 Subject: [PATCH 09/25] Test coverage. --- .../jdbc/postgresql/PostgreSQLDialect.java | 2 +- ...queIndexAdditionalDeploymentStatements.java | 18 +++++++++--------- .../jdbc/postgresql/TestPostgreSQLDialect.java | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java index d784583fa..408cc171e 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java @@ -669,7 +669,7 @@ private Iterable additionalUniqueIndexRenameStatements(Table table, Stri } Index index = Iterables.getOnlyElement(FluentIterable.from(table.indexes()).filter(i -> toIndexName.equalsIgnoreCase(i.getName()))); - return new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index).renameIndexStatements(schemaNamePrefix(table) + table.getName(), fromIndexName, toIndexName); + return new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index).renameIndexStatements(schemaNamePrefix(table) + fromIndexName, toIndexName); } diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java index d82a6aae4..cae80ef64 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -72,7 +72,7 @@ private interface Strategy { public Iterable createIndexStatements(String prefixedTableName); - public Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName); + public Iterable renameIndexStatements(String prefixedFromIndexName, String toIndexName); public Iterable dropIndexStatements(String prefixedTableName); @@ -172,15 +172,15 @@ private Iterable createIndexStatement(String prefixedTableName, Iterable @Override - public Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName) { + public Iterable renameIndexStatements(String prefixedFromIndexName, String toIndexName) { return FluentIterable.from(constellations) - .transformAndConcat(nullColumns -> renameIndexStatements(prefixedTableName, fromIndexName, toIndexName, nullColumns)); + .transformAndConcat(nullColumns -> renameIndexStatements(prefixedFromIndexName, toIndexName, nullColumns)); } - private Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName, Iterable nullColumns) { + private Iterable renameIndexStatements(String prefixedFromIndexName, String toIndexName, Iterable nullColumns) { String indexNameSuffix = makeIndexSuffix(nullColumns); - String alterStatement = alterIndexSqlStatement(prefixedTableName + fromIndexName + indexNameSuffix, toIndexName + indexNameSuffix); + String alterStatement = alterIndexSqlStatement(prefixedFromIndexName + indexNameSuffix, toIndexName + indexNameSuffix); return ImmutableList.of(alterStatement); } @@ -284,8 +284,8 @@ public Iterable createIndexStatements(String prefixedTableName) { @Override - public Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName) { - String alterStatement = alterIndexSqlStatement(prefixedTableName + fromIndexName + INDEX_NAME_SUFFIX, toIndexName + INDEX_NAME_SUFFIX); + public Iterable renameIndexStatements(String prefixedFromIndexName, String toIndexName) { + String alterStatement = alterIndexSqlStatement(prefixedFromIndexName + INDEX_NAME_SUFFIX, toIndexName + INDEX_NAME_SUFFIX); return ImmutableList.of(alterStatement); } @@ -328,8 +328,8 @@ public Iterable createIndexStatements(String prefixedTableName) { } - public Iterable renameIndexStatements(String prefixedTableName, String fromIndexName, String toIndexName) { - return strategy.renameIndexStatements(prefixedTableName, fromIndexName, toIndexName); + public Iterable renameIndexStatements(String prefixedFromIndexName, String toIndexName) { + return strategy.renameIndexStatements(prefixedFromIndexName, toIndexName); } diff --git a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java index 8ea4711ff..1f013aee1 100644 --- a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java +++ b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java @@ -1146,7 +1146,7 @@ protected List getRenamingTableWithLongNameStatements() { protected List expectedRenameIndexStatements() { return ImmutableList.of("ALTER INDEX testschema.Test_1 RENAME TO Test_2", "COMMENT ON INDEX Test_2 IS '"+PostgreSQLDialect.REAL_NAME_COMMENT_LABEL+":[Test_2]'", - "ALTER INDEX IF EXISTS testschema.TestTest_1$null0 RENAME TO Test_2$null0" + "ALTER INDEX IF EXISTS testschema.Test_1$null0 RENAME TO Test_2$null0" ); } From e33c0b347cc007434ec11108c2e25231301f1267 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Thu, 23 Jun 2022 10:03:10 +0100 Subject: [PATCH 10/25] Test coverage. --- ...ueIndexAdditionalDeploymentStatements.java | 537 ++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLUniqueIndexAdditionalDeploymentStatements.java diff --git a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLUniqueIndexAdditionalDeploymentStatements.java new file mode 100644 index 000000000..b3ca9a5db --- /dev/null +++ b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -0,0 +1,537 @@ +package org.alfasoftware.morf.jdbc.postgresql; + +import static org.alfasoftware.morf.metadata.SchemaUtils.column; +import static org.alfasoftware.morf.metadata.SchemaUtils.index; +import static org.alfasoftware.morf.metadata.SchemaUtils.table; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.emptyIterable; + +import java.util.Collection; + +import org.alfasoftware.morf.jdbc.postgresql.PostgreSQLUniqueIndexAdditionalDeploymentStatements.AdditionalIndexInfo; +import org.alfasoftware.morf.metadata.DataType; +import org.alfasoftware.morf.metadata.Index; +import org.alfasoftware.morf.metadata.Table; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +/** + * + * @author Copyright (c) Alfa Financial Software Limited. 2022 + */ +public class TestPostgreSQLUniqueIndexAdditionalDeploymentStatements { + + private static final String SCHEMA_PREFIX = "public."; + private static final String TABLE_NAME = "TableName"; + + private final Index index1 = index("TableName_1").columns("u").unique(); + private final Index index2 = index("TableName_2").columns("a", "u").unique(); + private final Index index3 = index("TableName_3").columns("a", "b", "c").unique(); + private final Index index4 = index("TableName_4").columns("a", "b", "u").unique(); + private final Index index5 = index("TableName_5").columns("u", "b", "v").unique(); + private final Index index6 = index("TableName_6").columns("u", "v", "x").unique(); + + private final Table table = + table(TABLE_NAME) + .columns( + column("id", DataType.BIG_INTEGER), + column("a", DataType.BIG_INTEGER), + column("b", DataType.BIG_INTEGER), + column("c", DataType.BIG_INTEGER), + column("u", DataType.BIG_INTEGER).nullable(), + column("v", DataType.BIG_INTEGER).nullable(), + column("x", DataType.BIG_INTEGER).nullable()) + .indexes(index1, index2, index3, index4, index5, index6); + + @Test + public void testCreateIndexStatementsForIndex1() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index1).createIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_1$null0 ON public.TableName ((0)) WHERE u IS NULL", + "COMMENT ON INDEX TableName_1$null0 IS 'REALNAME:[6285d92226e5112908a10cd51e129b72/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + @Test + public void testCreateIndexStatementsForIndex2() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index2).createIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_2$null0 ON public.TableName (a) WHERE u IS NULL", + "COMMENT ON INDEX TableName_2$null0 IS 'REALNAME:[9cbb012537901e9f3520ae3fcf7dc43d/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + @Test + public void testCreateIndexStatementsForIndex3() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index3).createIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + emptyIterable()); + } + + @Test + public void testCreateIndexStatementsForIndex4() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index4).createIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_4$null0 ON public.TableName (a, b) WHERE u IS NULL", + "COMMENT ON INDEX TableName_4$null0 IS 'REALNAME:[788721e3ce858194769fdf67aab7b14c/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + @Test + public void testCreateIndexStatementsForIndex5() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index5).createIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_5$null0 ON public.TableName (b, v) WHERE u IS NULL", + "COMMENT ON INDEX TableName_5$null0 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/cfcd208495d565ef66e7dff9f98764da]'", + "CREATE UNIQUE INDEX TableName_5$null01 ON public.TableName (b) WHERE u IS NULL AND v IS NULL", + "COMMENT ON INDEX TableName_5$null01 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/ae31e892f55b0e0225acd2c08fa2c413]'", + "CREATE UNIQUE INDEX TableName_5$null1 ON public.TableName (u, b) WHERE v IS NULL", + "COMMENT ON INDEX TableName_5$null1 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/c4ca4238a0b923820dcc509a6f75849b]'" + )); + } + + @Test + public void testCreateIndexStatementsForIndex6() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index6).createIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_6$null ON public.TableName ((u ||'§'|| v ||'§'|| x)) WHERE u IS NULL OR v IS NULL OR x IS NULL", + "COMMENT ON INDEX TableName_6$null IS 'REALNAME:[47d0d3041096c2ac2e8cbb4a8d0e3fd8]'" + )); + } + + + @Test + public void renameIndexStatementsForIndex1() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index1).renameIndexStatements(SCHEMA_PREFIX + "TableName_1", "TableName_1a"), + contains( + "ALTER INDEX IF EXISTS public.TableName_1$null0 RENAME TO TableName_1a$null0" + )); + } + + @Test + public void renameIndexStatementsForIndex2() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index2).renameIndexStatements(SCHEMA_PREFIX + "TableName_2", "TableName_2a"), + contains( + "ALTER INDEX IF EXISTS public.TableName_2$null0 RENAME TO TableName_2a$null0" + )); + } + + @Test + public void renameIndexStatementsForIndex3() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index3).renameIndexStatements(SCHEMA_PREFIX + "TableName_3", "TableName_3a"), + emptyIterable()); + } + + @Test + public void renameIndexStatementsForIndex4() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index4).renameIndexStatements(SCHEMA_PREFIX + "TableName_4", "TableName_4a"), + contains( + "ALTER INDEX IF EXISTS public.TableName_4$null0 RENAME TO TableName_4a$null0" + )); + } + + @Test + public void renameIndexStatementsForIndex5() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index5).renameIndexStatements(SCHEMA_PREFIX + "TableName_5", "TableName_5a"), + contains( + "ALTER INDEX IF EXISTS public.TableName_5$null0 RENAME TO TableName_5a$null0", + "ALTER INDEX IF EXISTS public.TableName_5$null01 RENAME TO TableName_5a$null01", + "ALTER INDEX IF EXISTS public.TableName_5$null1 RENAME TO TableName_5a$null1" + )); + } + + @Test + public void renameIndexStatementsForIndex6() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index6).renameIndexStatements(SCHEMA_PREFIX + "TableName_6", "TableName_6a"), + contains( + "ALTER INDEX IF EXISTS public.TableName_6$null RENAME TO TableName_6a$null" + )); + } + + + @Test + public void testDropIndexStatementsForIndex1() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index1).dropIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS TableName_1$null0" + )); + } + + @Test + public void testDropIndexStatementsForIndex2() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index2).dropIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS TableName_2$null0" + )); + } + + @Test + public void testDropIndexStatementsForIndex3() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index3).dropIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + emptyIterable()); + } + + @Test + public void testDropIndexStatementsForIndex4() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index4).dropIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS TableName_4$null0" + )); + } + + @Test + public void testDropIndexStatementsForIndex5() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index5).dropIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS TableName_5$null0", + "DROP INDEX IF EXISTS TableName_5$null01", + "DROP INDEX IF EXISTS TableName_5$null1" + )); + } + + @Test + public void testDropIndexStatementsForIndex6() { + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index6).dropIndexStatements(SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS TableName_6$null" + )); + } + + + @Test + public void testHealIndexStatementsForIndex1FromNoIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of(); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index1).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_1$null0 ON public.TableName ((0)) WHERE u IS NULL", + "COMMENT ON INDEX TableName_1$null0 IS 'REALNAME:[6285d92226e5112908a10cd51e129b72/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + @Test + public void testHealIndexStatementsForIndex1FromGoodIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_1$null0", "6285d92226e5112908a10cd51e129b72/cfcd208495d565ef66e7dff9f98764da").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index1).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + emptyIterable()); + } + + @Test + public void testHealIndexStatementsForIndex1FromWrongHash() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_1$null0", "6285d92226e5112908a10cd51e129b72/xx").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index1).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_1$null0", + "CREATE UNIQUE INDEX TableName_1$null0 ON public.TableName ((0)) WHERE u IS NULL", + "COMMENT ON INDEX TableName_1$null0 IS 'REALNAME:[6285d92226e5112908a10cd51e129b72/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + @Test + public void testHealIndexStatementsForIndex1FromWrongName() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_1$null1", "6285d92226e5112908a10cd51e129b72/cfcd208495d565ef66e7dff9f98764da").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index1).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_1$null1", + "CREATE UNIQUE INDEX TableName_1$null0 ON public.TableName ((0)) WHERE u IS NULL", + "COMMENT ON INDEX TableName_1$null0 IS 'REALNAME:[6285d92226e5112908a10cd51e129b72/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + + @Test + public void testHealIndexStatementsForIndex2FromNoIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of(); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index2).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_2$null0 ON public.TableName (a) WHERE u IS NULL", + "COMMENT ON INDEX TableName_2$null0 IS 'REALNAME:[9cbb012537901e9f3520ae3fcf7dc43d/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + @Test + public void testHealIndexStatementsForIndex2FromGoodIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_2$null0", "9cbb012537901e9f3520ae3fcf7dc43d/cfcd208495d565ef66e7dff9f98764da").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index2).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + emptyIterable()); + } + + + @Test + public void testHealIndexStatementsForIndex2FromWrongHash() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_2$null0", "xx/cfcd208495d565ef66e7dff9f98764da").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index2).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_2$null0", + "CREATE UNIQUE INDEX TableName_2$null0 ON public.TableName (a) WHERE u IS NULL", + "COMMENT ON INDEX TableName_2$null0 IS 'REALNAME:[9cbb012537901e9f3520ae3fcf7dc43d/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + + @Test + public void testHealIndexStatementsForIndex2FromWrongName() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_2$null", "9cbb012537901e9f3520ae3fcf7dc43d/cfcd208495d565ef66e7dff9f98764da").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index2).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_2$null", + "CREATE UNIQUE INDEX TableName_2$null0 ON public.TableName (a) WHERE u IS NULL", + "COMMENT ON INDEX TableName_2$null0 IS 'REALNAME:[9cbb012537901e9f3520ae3fcf7dc43d/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + + @Test + public void testHealIndexStatementsForIndex3FromNoIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of(); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index3).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + emptyIterable()); + } + + + @Test + public void testHealIndexStatementsForIndex3FromWrongIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_3$null", "xyz/abc").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index3).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_3$null" + )); + } + + + @Test + public void testHealIndexStatementsForIndex4FromNoIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of(); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index4).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_4$null0 ON public.TableName (a, b) WHERE u IS NULL", + "COMMENT ON INDEX TableName_4$null0 IS 'REALNAME:[788721e3ce858194769fdf67aab7b14c/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + @Test + public void testHealIndexStatementsForIndex4FromGoodIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_4$null0", "788721e3ce858194769fdf67aab7b14c/cfcd208495d565ef66e7dff9f98764da").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index4).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + emptyIterable()); + } + + @Test + public void testHealIndexStatementsForIndex4FromWrongHash() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_4$null0", "788721e3ce858194769fdf67aab7b14c/xx").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index4).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_4$null0", + "CREATE UNIQUE INDEX TableName_4$null0 ON public.TableName (a, b) WHERE u IS NULL", + "COMMENT ON INDEX TableName_4$null0 IS 'REALNAME:[788721e3ce858194769fdf67aab7b14c/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + @Test + public void testHealIndexStatementsForIndex4FromWrongName() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_4$null4", "788721e3ce858194769fdf67aab7b14c/cfcd208495d565ef66e7dff9f98764da").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index4).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_4$null4", + "CREATE UNIQUE INDEX TableName_4$null0 ON public.TableName (a, b) WHERE u IS NULL", + "COMMENT ON INDEX TableName_4$null0 IS 'REALNAME:[788721e3ce858194769fdf67aab7b14c/cfcd208495d565ef66e7dff9f98764da]'" + )); + } + + + @Test + public void testHealIndexStatementsForIndex5FromNoIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of(); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index5).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_5$null0 ON public.TableName (b, v) WHERE u IS NULL", + "COMMENT ON INDEX TableName_5$null0 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/cfcd208495d565ef66e7dff9f98764da]'", + "CREATE UNIQUE INDEX TableName_5$null01 ON public.TableName (b) WHERE u IS NULL AND v IS NULL", + "COMMENT ON INDEX TableName_5$null01 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/ae31e892f55b0e0225acd2c08fa2c413]'", + "CREATE UNIQUE INDEX TableName_5$null1 ON public.TableName (u, b) WHERE v IS NULL", + "COMMENT ON INDEX TableName_5$null1 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/c4ca4238a0b923820dcc509a6f75849b]'" + )); + } + + @Test + public void testHealIndexStatementsForIndex5FromGoodIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null0", "a42e2a14d8ae857335a014ab1a1f1ad7/cfcd208495d565ef66e7dff9f98764da").get(), + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null01", "a42e2a14d8ae857335a014ab1a1f1ad7/ae31e892f55b0e0225acd2c08fa2c413").get(), + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null01", "a42e2a14d8ae857335a014ab1a1f1ad7/ae31e892f55b0e0225acd2c08fa2c413").get(), // duplicates are supported + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null1", "a42e2a14d8ae857335a014ab1a1f1ad7/c4ca4238a0b923820dcc509a6f75849b").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index5).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + emptyIterable()); + } + + @Test + public void testHealIndexStatementsForIndex5FromWrongHash() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null0", "a42e2a14d8ae857335a014ab1a1f1ad7/cfcd208495d565ef66e7dff9f98764da").get(), + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null01", "a42e2a14d8ae857335a014ab1a1f1ad7/xxxx").get(), // wrong hash + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null1", "a42e2a14d8ae857335a014ab1a1f1ad7/c4ca4238a0b923820dcc509a6f75849b").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index5).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_5$null0", + "DROP INDEX IF EXISTS tablename_5$null01", + "DROP INDEX IF EXISTS tablename_5$null1", + "CREATE UNIQUE INDEX TableName_5$null0 ON public.TableName (b, v) WHERE u IS NULL", + "COMMENT ON INDEX TableName_5$null0 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/cfcd208495d565ef66e7dff9f98764da]'", + "CREATE UNIQUE INDEX TableName_5$null01 ON public.TableName (b) WHERE u IS NULL AND v IS NULL", + "COMMENT ON INDEX TableName_5$null01 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/ae31e892f55b0e0225acd2c08fa2c413]'", + "CREATE UNIQUE INDEX TableName_5$null1 ON public.TableName (u, b) WHERE v IS NULL", + "COMMENT ON INDEX TableName_5$null1 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/c4ca4238a0b923820dcc509a6f75849b]'" + )); + } + + @Test + public void testHealIndexStatementsForIndex5FromWrongName() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null0", "a42e2a14d8ae857335a014ab1a1f1ad7/cfcd208495d565ef66e7dff9f98764da").get(), + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null", "a42e2a14d8ae857335a014ab1a1f1ad7/ae31e892f55b0e0225acd2c08fa2c413").get(), // wrong name + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_5$null1", "a42e2a14d8ae857335a014ab1a1f1ad7/c4ca4238a0b923820dcc509a6f75849b").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index5).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_5$null0", + "DROP INDEX IF EXISTS tablename_5$null", + "DROP INDEX IF EXISTS tablename_5$null1", + "CREATE UNIQUE INDEX TableName_5$null0 ON public.TableName (b, v) WHERE u IS NULL", + "COMMENT ON INDEX TableName_5$null0 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/cfcd208495d565ef66e7dff9f98764da]'", + "CREATE UNIQUE INDEX TableName_5$null01 ON public.TableName (b) WHERE u IS NULL AND v IS NULL", + "COMMENT ON INDEX TableName_5$null01 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/ae31e892f55b0e0225acd2c08fa2c413]'", + "CREATE UNIQUE INDEX TableName_5$null1 ON public.TableName (u, b) WHERE v IS NULL", + "COMMENT ON INDEX TableName_5$null1 IS 'REALNAME:[a42e2a14d8ae857335a014ab1a1f1ad7/c4ca4238a0b923820dcc509a6f75849b]'" + )); + } + + + @Test + public void testHealIndexStatementsForIndex6FromNoIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of(); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index6).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "CREATE UNIQUE INDEX TableName_6$null ON public.TableName ((u ||'§'|| v ||'§'|| x)) WHERE u IS NULL OR v IS NULL OR x IS NULL", + "COMMENT ON INDEX TableName_6$null IS 'REALNAME:[47d0d3041096c2ac2e8cbb4a8d0e3fd8]'" + )); + } + + @Test + public void testHealIndexStatementsForIndex6FromGoodIndexes() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_6$null", "47d0d3041096c2ac2e8cbb4a8d0e3fd8").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index6).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + emptyIterable()); + } + + @Test + public void testHealIndexStatementsForIndex6FromWrongHash() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_6$null", "xx").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index6).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_6$null", + "CREATE UNIQUE INDEX TableName_6$null ON public.TableName ((u ||'§'|| v ||'§'|| x)) WHERE u IS NULL OR v IS NULL OR x IS NULL", + "COMMENT ON INDEX TableName_6$null IS 'REALNAME:[47d0d3041096c2ac2e8cbb4a8d0e3fd8]'" + )); + } + + @Test + public void testHealIndexStatementsForIndex6FromWrongName() { + final Collection additionalConstraintIndexInfos = ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_6$null6", "47d0d3041096c2ac2e8cbb4a8d0e3fd8").get() + ); + + assertThat( + new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index6).healIndexStatements(additionalConstraintIndexInfos, SCHEMA_PREFIX + TABLE_NAME), + contains( + "DROP INDEX IF EXISTS tablename_6$null6", + "CREATE UNIQUE INDEX TableName_6$null ON public.TableName ((u ||'§'|| v ||'§'|| x)) WHERE u IS NULL OR v IS NULL OR x IS NULL", + "COMMENT ON INDEX TableName_6$null IS 'REALNAME:[47d0d3041096c2ac2e8cbb4a8d0e3fd8]'" + )); + } +} From d56e9a1237aea39ca3031c6b92a9468bac6f96c5 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Thu, 23 Jun 2022 12:55:00 +0100 Subject: [PATCH 11/25] Test coverage. --- .../jdbc/postgresql/PostgreSQLDialect.java | 2 + .../postgresql/TestPostgreSQLDialect.java | 56 ++++++++++++++++++ ...ueIndexAdditionalDeploymentStatements.java | 18 ++++++ .../morf/jdbc/AbstractSqlDialectTest.java | 59 +++++++++++++++++++ 4 files changed, 135 insertions(+) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java index 408cc171e..c365fe6a6 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java @@ -1,5 +1,6 @@ package org.alfasoftware.morf.jdbc.postgresql; +import static com.google.common.base.Predicates.instanceOf; import static org.alfasoftware.morf.metadata.SchemaUtils.namesOfColumns; import static org.alfasoftware.morf.metadata.SchemaUtils.primaryKeysForTable; import static org.alfasoftware.morf.sql.SelectStatement.select; @@ -749,6 +750,7 @@ protected String getSqlFrom(DeleteStatement statement) { @Override public List getSchemaConsistencyStatements(SchemaResource schemaResource) { return schemaResource.getDatabaseMetaDataProvider() + .filter(instanceOf(PostgreSQLMetaDataProvider.class)) .map(PostgreSQLMetaDataProvider.class::cast) .map(this::getSchemaConsistencyStatements) .orElseGet(() -> super.getSchemaConsistencyStatements(schemaResource)); diff --git a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java index 1f013aee1..266671fd7 100644 --- a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java +++ b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java @@ -1,7 +1,13 @@ package org.alfasoftware.morf.jdbc.postgresql; +import static org.alfasoftware.morf.metadata.SchemaUtils.column; +import static org.alfasoftware.morf.metadata.SchemaUtils.index; +import static org.alfasoftware.morf.metadata.SchemaUtils.table; +import static org.hamcrest.Matchers.contains; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.InputStream; import java.sql.SQLException; @@ -9,9 +15,13 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.alfasoftware.morf.jdbc.AbstractSqlDialectTest; import org.alfasoftware.morf.jdbc.SqlDialect; +import org.alfasoftware.morf.metadata.DataType; +import org.alfasoftware.morf.metadata.SchemaResource; +import org.alfasoftware.morf.metadata.Table; import org.alfasoftware.morf.sql.CustomHint; import org.alfasoftware.morf.sql.PostgreSQLCustomHint; import org.alfasoftware.morf.sql.SelectStatement; @@ -20,6 +30,7 @@ import org.alfasoftware.morf.sql.element.FieldReference; import org.alfasoftware.morf.sql.element.SqlParameter; import org.alfasoftware.morf.sql.element.TableReference; +import org.hamcrest.Matcher; import org.mockito.Mockito; import com.google.common.collect.ImmutableList; @@ -1347,4 +1358,49 @@ protected String expectedDeleteWithLimitWithoutWhere() { " LIMIT 1000)"; }; + + @Override + protected SchemaResource createSchemaResourceForSchemaConsistencyStatements() { + final List tables = ImmutableList.of( + table("TableName") + .columns( + column("id", DataType.BIG_INTEGER), + column("u", DataType.BIG_INTEGER).nullable(), + column("v", DataType.BIG_INTEGER).nullable(), + column("x", DataType.BIG_INTEGER).nullable()) + .indexes( + index("TableName_1").columns("u").unique(), + index("TableName_2").columns("u", "v", "x").unique(), + index("TableName_3").columns("x").unique() + ) + ); + + PostgreSQLMetaDataProvider metaDataProvider = mock(PostgreSQLMetaDataProvider.class); + when(metaDataProvider.tables()).thenReturn(tables); + when(metaDataProvider.getAdditionalConstraintIndexes("tablename_1")).thenReturn(ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_1$null0", "xx/yy").get(), + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_1$null1", "xx/yy").get() + )); + when(metaDataProvider.getAdditionalConstraintIndexes("tablename_3")).thenReturn(ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_3$null0", "76e4a5e9bce3c23b4feb0675fb0c8366/cfcd208495d565ef66e7dff9f98764da").get() + )); + + final SchemaResource schemaResource = mock(SchemaResource.class); + when(schemaResource.getDatabaseMetaDataProvider()).thenReturn(Optional.of(metaDataProvider)); + + return schemaResource; + } + + @Override + protected Matcher> expectedSchemaConsistencyStatements() { + return contains( + "-- Healing table: TableName", + "DROP INDEX IF EXISTS tablename_1$null0", + "DROP INDEX IF EXISTS tablename_1$null1", + "CREATE UNIQUE INDEX TableName_1$null0 ON testschema.TableName ((0)) WHERE u IS NULL", + "COMMENT ON INDEX TableName_1$null0 IS 'REALNAME:[6285d92226e5112908a10cd51e129b72/cfcd208495d565ef66e7dff9f98764da]'", + "CREATE UNIQUE INDEX TableName_2$null ON testschema.TableName ((u ||'§'|| v ||'§'|| x)) WHERE u IS NULL OR v IS NULL OR x IS NULL", + "COMMENT ON INDEX TableName_2$null IS 'REALNAME:[47d0d3041096c2ac2e8cbb4a8d0e3fd8]'" + ); + } } diff --git a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLUniqueIndexAdditionalDeploymentStatements.java index b3ca9a5db..1d755e1eb 100644 --- a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLUniqueIndexAdditionalDeploymentStatements.java +++ b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -6,6 +6,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.emptyIterable; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.util.Collection; @@ -534,4 +536,20 @@ public void testHealIndexStatementsForIndex6FromWrongName() { "COMMENT ON INDEX TableName_6$null IS 'REALNAME:[47d0d3041096c2ac2e8cbb4a8d0e3fd8]'" )); } + + + @Test + public void testMatchAdditionalIndexPositiveCases() { + assertTrue(PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_6$null", "abc").isPresent()); + assertTrue(PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_6$null0", "abc/def").isPresent()); + assertTrue(PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_6$null012", "abc/def").isPresent()); + } + + + @Test + public void testMatchAdditionalIndexNegativeCase() { + assertFalse(PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_6", "TableName_6").isPresent()); + assertFalse(PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_PK", "TableName_6").isPresent()); + assertFalse(PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_PRF", "TableName_6").isPresent()); + } } diff --git a/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java b/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java index fda3c4114..3ad0acf87 100755 --- a/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java +++ b/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java @@ -88,6 +88,9 @@ import static org.alfasoftware.morf.sql.element.Function.trim; import static org.alfasoftware.morf.sql.element.Function.upperCase; import static org.alfasoftware.morf.sql.element.Function.yyyymmddToDate; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyIterable; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -115,12 +118,14 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.alfasoftware.morf.dataset.Record; import org.alfasoftware.morf.metadata.Column; import org.alfasoftware.morf.metadata.DataType; import org.alfasoftware.morf.metadata.Index; import org.alfasoftware.morf.metadata.Schema; +import org.alfasoftware.morf.metadata.SchemaResource; import org.alfasoftware.morf.metadata.Table; import org.alfasoftware.morf.metadata.View; import org.alfasoftware.morf.sql.CustomHint; @@ -151,6 +156,7 @@ import org.alfasoftware.morf.upgrade.RemoveColumn; import org.alfasoftware.morf.upgrade.adapt.AlteredTable; import org.apache.commons.lang3.StringUtils; +import org.hamcrest.Matcher; import org.joda.time.LocalDate; import org.junit.Assert; import org.junit.Before; @@ -5767,6 +5773,59 @@ public void testResultSetToRecordNulls() throws SQLException { } + /** + * Checks the @link org.alfasoftware.morf.jdbc.SqlDialect.getSchemaConsistencyStatements(SchemaResource)} mechanism. + */ + @Test + public void testSchemaConsistencyStatements() throws SQLException { + final SchemaResource schemaResource = createSchemaResourceForSchemaConsistencyStatements(); + + assertThat( + testDialect.getSchemaConsistencyStatements(schemaResource), + expectedSchemaConsistencyStatements()); + } + + protected SchemaResource createSchemaResourceForSchemaConsistencyStatements() { + final SchemaResource schemaResource = mock(SchemaResource.class); + when(schemaResource.getDatabaseMetaDataProvider()).thenReturn(Optional.empty()); + return schemaResource; + } + + protected Matcher> expectedSchemaConsistencyStatements() { + return emptyIterable(); + } + + + + /** + * Checks the @link org.alfasoftware.morf.jdbc.SqlDialect.getSchemaConsistencyStatements(SchemaResource)} mechanism fallback. + */ + @Test + public void testSchemaConsistencyStatementsOnNoDatabaseMetaDataProvider() throws SQLException { + final SchemaResource schemaResource = mock(SchemaResource.class); + when(schemaResource.getDatabaseMetaDataProvider()).thenReturn(Optional.empty()); + + assertThat( + testDialect.getSchemaConsistencyStatements(schemaResource), + empty()); + } + + + + /** + * Checks the @link org.alfasoftware.morf.jdbc.SqlDialect.getSchemaConsistencyStatements(SchemaResource)} mechanism fallback. + */ + @Test + public void testSchemaConsistencyStatementsOnWrongDatabaseMetaDataProvider() throws SQLException { + final SchemaResource schemaResource = mock(SchemaResource.class); + when(schemaResource.getDatabaseMetaDataProvider()).thenReturn(Optional.of(mock(DatabaseMetaDataProvider.class))); + + assertThat( + testDialect.getSchemaConsistencyStatements(schemaResource), + empty()); + } + + /** * Matches an InputStream to a byte array. * From 5984f7bab0de63a7d466da239239298bdda1508d Mon Sep 17 00:00:00 2001 From: jsimlo Date: Thu, 23 Jun 2022 13:13:20 +0100 Subject: [PATCH 12/25] Test coverage. --- .../postgresql/TestPostgreSQLDialect.java | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java index 266671fd7..9f398d4c5 100644 --- a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java +++ b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java @@ -1362,27 +1362,37 @@ protected String expectedDeleteWithLimitWithoutWhere() { @Override protected SchemaResource createSchemaResourceForSchemaConsistencyStatements() { final List
tables = ImmutableList.of( - table("TableName") + table("TableOne") .columns( column("id", DataType.BIG_INTEGER), column("u", DataType.BIG_INTEGER).nullable(), column("v", DataType.BIG_INTEGER).nullable(), column("x", DataType.BIG_INTEGER).nullable()) .indexes( - index("TableName_1").columns("u").unique(), - index("TableName_2").columns("u", "v", "x").unique(), - index("TableName_3").columns("x").unique() + index("TableOne_1").columns("u").unique(), + index("TableOne_2").columns("u", "v", "x").unique(), + index("TableOne_3").columns("x").unique() + ), + table("TableTwo") + .columns( + column("id", DataType.BIG_INTEGER), + column("x", DataType.BIG_INTEGER).nullable()) + .indexes( + index("TableTwo_3").columns("x").unique() ) ); PostgreSQLMetaDataProvider metaDataProvider = mock(PostgreSQLMetaDataProvider.class); when(metaDataProvider.tables()).thenReturn(tables); - when(metaDataProvider.getAdditionalConstraintIndexes("tablename_1")).thenReturn(ImmutableList.of( - PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_1$null0", "xx/yy").get(), - PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_1$null1", "xx/yy").get() + when(metaDataProvider.getAdditionalConstraintIndexes("tableone_1")).thenReturn(ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableOne_1$null0", "xx/yy").get(), + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableOne_1$null1", "xx/yy").get() + )); + when(metaDataProvider.getAdditionalConstraintIndexes("tableone_3")).thenReturn(ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableOne_3$null0", "76e4a5e9bce3c23b4feb0675fb0c8366/cfcd208495d565ef66e7dff9f98764da").get() )); - when(metaDataProvider.getAdditionalConstraintIndexes("tablename_3")).thenReturn(ImmutableList.of( - PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableName_3$null0", "76e4a5e9bce3c23b4feb0675fb0c8366/cfcd208495d565ef66e7dff9f98764da").get() + when(metaDataProvider.getAdditionalConstraintIndexes("tabletwo_3")).thenReturn(ImmutableList.of( + PostgreSQLUniqueIndexAdditionalDeploymentStatements.matchAdditionalIndex("TableTwo_3$null0", "76e4a5e9bce3c23b4feb0675fb0c8366/cfcd208495d565ef66e7dff9f98764da").get() )); final SchemaResource schemaResource = mock(SchemaResource.class); @@ -1394,13 +1404,13 @@ protected SchemaResource createSchemaResourceForSchemaConsistencyStatements() { @Override protected Matcher> expectedSchemaConsistencyStatements() { return contains( - "-- Healing table: TableName", - "DROP INDEX IF EXISTS tablename_1$null0", - "DROP INDEX IF EXISTS tablename_1$null1", - "CREATE UNIQUE INDEX TableName_1$null0 ON testschema.TableName ((0)) WHERE u IS NULL", - "COMMENT ON INDEX TableName_1$null0 IS 'REALNAME:[6285d92226e5112908a10cd51e129b72/cfcd208495d565ef66e7dff9f98764da]'", - "CREATE UNIQUE INDEX TableName_2$null ON testschema.TableName ((u ||'§'|| v ||'§'|| x)) WHERE u IS NULL OR v IS NULL OR x IS NULL", - "COMMENT ON INDEX TableName_2$null IS 'REALNAME:[47d0d3041096c2ac2e8cbb4a8d0e3fd8]'" + "-- Healing table: TableOne", + "DROP INDEX IF EXISTS tableone_1$null0", + "DROP INDEX IF EXISTS tableone_1$null1", + "CREATE UNIQUE INDEX TableOne_1$null0 ON testschema.TableOne ((0)) WHERE u IS NULL", + "COMMENT ON INDEX TableOne_1$null0 IS 'REALNAME:[6285d92226e5112908a10cd51e129b72/cfcd208495d565ef66e7dff9f98764da]'", + "CREATE UNIQUE INDEX TableOne_2$null ON testschema.TableOne ((u ||'§'|| v ||'§'|| x)) WHERE u IS NULL OR v IS NULL OR x IS NULL", + "COMMENT ON INDEX TableOne_2$null IS 'REALNAME:[47d0d3041096c2ac2e8cbb4a8d0e3fd8]'" ); } } From 8e02e1a1de10da1793316c9722d9434f8a61caa4 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Thu, 23 Jun 2022 13:22:44 +0100 Subject: [PATCH 13/25] Test coverage. --- .../morf/jdbc/postgresql/TestPostgreSQLDialect.java | 3 +-- .../alfasoftware/morf/jdbc/AbstractSqlDialectTest.java | 9 ++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java index 9f398d4c5..9406e49d2 100644 --- a/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java +++ b/morf-postgresql/src/test/java/org/alfasoftware/morf/jdbc/postgresql/TestPostgreSQLDialect.java @@ -30,7 +30,6 @@ import org.alfasoftware.morf.sql.element.FieldReference; import org.alfasoftware.morf.sql.element.SqlParameter; import org.alfasoftware.morf.sql.element.TableReference; -import org.hamcrest.Matcher; import org.mockito.Mockito; import com.google.common.collect.ImmutableList; @@ -1402,7 +1401,7 @@ protected SchemaResource createSchemaResourceForSchemaConsistencyStatements() { } @Override - protected Matcher> expectedSchemaConsistencyStatements() { + protected org.hamcrest.Matcher> expectedSchemaConsistencyStatements() { return contains( "-- Healing table: TableOne", "DROP INDEX IF EXISTS tableone_1$null0", diff --git a/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java b/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java index 3ad0acf87..8a9b78b26 100755 --- a/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java +++ b/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java @@ -156,7 +156,6 @@ import org.alfasoftware.morf.upgrade.RemoveColumn; import org.alfasoftware.morf.upgrade.adapt.AlteredTable; import org.apache.commons.lang3.StringUtils; -import org.hamcrest.Matcher; import org.joda.time.LocalDate; import org.junit.Assert; import org.junit.Before; @@ -5777,7 +5776,7 @@ public void testResultSetToRecordNulls() throws SQLException { * Checks the @link org.alfasoftware.morf.jdbc.SqlDialect.getSchemaConsistencyStatements(SchemaResource)} mechanism. */ @Test - public void testSchemaConsistencyStatements() throws SQLException { + public void testSchemaConsistencyStatements() { final SchemaResource schemaResource = createSchemaResourceForSchemaConsistencyStatements(); assertThat( @@ -5791,7 +5790,7 @@ protected SchemaResource createSchemaResourceForSchemaConsistencyStatements() { return schemaResource; } - protected Matcher> expectedSchemaConsistencyStatements() { + protected org.hamcrest.Matcher> expectedSchemaConsistencyStatements() { return emptyIterable(); } @@ -5801,7 +5800,7 @@ protected Matcher> expectedSchemaConsistencyStatement * Checks the @link org.alfasoftware.morf.jdbc.SqlDialect.getSchemaConsistencyStatements(SchemaResource)} mechanism fallback. */ @Test - public void testSchemaConsistencyStatementsOnNoDatabaseMetaDataProvider() throws SQLException { + public void testSchemaConsistencyStatementsOnNoDatabaseMetaDataProvider() { final SchemaResource schemaResource = mock(SchemaResource.class); when(schemaResource.getDatabaseMetaDataProvider()).thenReturn(Optional.empty()); @@ -5816,7 +5815,7 @@ public void testSchemaConsistencyStatementsOnNoDatabaseMetaDataProvider() throws * Checks the @link org.alfasoftware.morf.jdbc.SqlDialect.getSchemaConsistencyStatements(SchemaResource)} mechanism fallback. */ @Test - public void testSchemaConsistencyStatementsOnWrongDatabaseMetaDataProvider() throws SQLException { + public void testSchemaConsistencyStatementsOnWrongDatabaseMetaDataProvider() { final SchemaResource schemaResource = mock(SchemaResource.class); when(schemaResource.getDatabaseMetaDataProvider()).thenReturn(Optional.of(mock(DatabaseMetaDataProvider.class))); From 48f0684285788077bd302a5afccb117326d03535 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Thu, 23 Jun 2022 13:34:19 +0100 Subject: [PATCH 14/25] Sonar fix. --- .../java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java b/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java index 8a9b78b26..fec8e88d1 100755 --- a/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java +++ b/morf-testsupport/src/main/java/org/alfasoftware/morf/jdbc/AbstractSqlDialectTest.java @@ -5790,7 +5790,7 @@ protected SchemaResource createSchemaResourceForSchemaConsistencyStatements() { return schemaResource; } - protected org.hamcrest.Matcher> expectedSchemaConsistencyStatements() { + protected org.hamcrest.Matcher> expectedSchemaConsistencyStatements() { //NOSONAR // Remove usage of generic wildcard type // The generic wildcard type comes from hamcrest and is therefore unavoidable return emptyIterable(); } From 908ffca69eded5c999753943d77158e79d734da7 Mon Sep 17 00:00:00 2001 From: Juraj Simlovic <33553963+jsimlo@users.noreply.github.com> Date: Tue, 28 Feb 2023 16:12:34 +0000 Subject: [PATCH 15/25] Update PostgreSQLDialect.java --- .../alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java index bfe693d33..40e4c5616 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java @@ -777,6 +777,8 @@ private Iterable healIndexes(PostgreSQLMetaDataProvider metaDataProvider private Iterable healIndexes(Collection additionalConstraintIndexInfo, Table table, Index index) { return new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index) .healIndexStatements(additionalConstraintIndexInfo, schemaNamePrefix(table) + table.getName()); + } + /** * @see org.alfasoftware.morf.jdbc.SqlDialect#tableNameWithSchemaName(org.alfasoftware.morf.sql.element.TableReference) @@ -789,4 +791,4 @@ protected String tableNameWithSchemaName(TableReference tableRef) { return tableRef.getDblink() + "." + tableRef.getName(); } } -} \ No newline at end of file +} From aaea0349b0c81fee21dff8a06c52a3a08d1ffc3c Mon Sep 17 00:00:00 2001 From: jsimlo Date: Tue, 28 Feb 2023 16:24:40 +0000 Subject: [PATCH 16/25] Updating tests post-merge. --- .../morf/upgrade/TestUpgrade.java | 95 ++++++++++--------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java b/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java index dcd158b9c..add9d21fd 100755 --- a/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java +++ b/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java @@ -16,11 +16,46 @@ package org.alfasoftware.morf.upgrade; -import com.google.common.base.Function; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; +import static org.alfasoftware.morf.metadata.SchemaUtils.column; +import static org.alfasoftware.morf.metadata.SchemaUtils.idColumn; +import static org.alfasoftware.morf.metadata.SchemaUtils.schema; +import static org.alfasoftware.morf.metadata.SchemaUtils.table; +import static org.alfasoftware.morf.metadata.SchemaUtils.versionColumn; +import static org.alfasoftware.morf.metadata.SchemaUtils.view; +import static org.alfasoftware.morf.sql.SqlUtils.field; +import static org.alfasoftware.morf.sql.SqlUtils.literal; +import static org.alfasoftware.morf.sql.SqlUtils.select; +import static org.alfasoftware.morf.sql.SqlUtils.tableRef; +import static org.alfasoftware.morf.upgrade.UpgradeStatus.NONE; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyListOf; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + +import javax.sql.DataSource; + import org.alfasoftware.morf.jdbc.ConnectionResources; import org.alfasoftware.morf.jdbc.MockDialect; import org.alfasoftware.morf.jdbc.SqlDialect; @@ -50,44 +85,11 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import javax.sql.DataSource; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; - -import static org.alfasoftware.morf.metadata.SchemaUtils.column; -import static org.alfasoftware.morf.metadata.SchemaUtils.idColumn; -import static org.alfasoftware.morf.metadata.SchemaUtils.schema; -import static org.alfasoftware.morf.metadata.SchemaUtils.table; -import static org.alfasoftware.morf.metadata.SchemaUtils.versionColumn; -import static org.alfasoftware.morf.metadata.SchemaUtils.view; -import static org.alfasoftware.morf.sql.SqlUtils.field; -import static org.alfasoftware.morf.sql.SqlUtils.literal; -import static org.alfasoftware.morf.sql.SqlUtils.select; -import static org.alfasoftware.morf.sql.SqlUtils.tableRef; -import static org.alfasoftware.morf.upgrade.UpgradeStatus.NONE; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyListOf; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; /** * Test {@link Upgrade} works correctly. @@ -226,7 +228,7 @@ public void testUpgradeWithTriggerMessage() throws SQLException { private UpgradePathFactory upgradePathFactory() { UpgradePathFactory upgradePathFactory = mock(UpgradePathFactory.class); when(upgradePathFactory.create(anyListOf(UpgradeStep.class), any(ConnectionResources.class), nullable(GraphBasedUpgradeBuilder.class))).thenAnswer(invocation -> { - return new UpgradePath(Sets.newHashSet(), (List)invocation.getArguments()[0], ((ConnectionResources)invocation.getArguments()[1]), Collections.emptyList(), Collections.emptyList()); + return new UpgradePath(Sets.newHashSet(), (List)invocation.getArguments()[0], (ConnectionResources)invocation.getArguments()[1], Collections.emptyList(), Collections.emptyList()); }); return upgradePathFactory; @@ -296,6 +298,7 @@ public void testUpgradeWithNoStepsToApply() { ConnectionResources mockConnectionResources = mock(ConnectionResources.class, RETURNS_DEEP_STUBS); SchemaResource schemaResource = mock(SchemaResource.class); + when(mockConnectionResources.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of()); when(mockConnectionResources.openSchemaResource(eq(mockConnectionResources.getDataSource()))).thenReturn(schemaResource); when(schemaResource.tables()).thenReturn(Arrays.asList(upgradeAudit)); @@ -330,7 +333,9 @@ public void testUpgradeWithOnlySchemaConsistencyStatementsToDeploy() { when(upgradeStatusTableService.getStatus(Optional.of(connection.getDataSource()))).thenReturn(NONE); // When - UpgradePath results = new Upgrade(connection, connection.getDataSource(), upgradePathFactory(), upgradeStatusTableService, new ViewChangesDeploymentHelper(connection.sqlDialect()), viewDeploymentValidator, graphBasedUpgradeScriptGeneratorFactory).findPath(targetSchema, upgradeSteps, new HashSet()); + UpgradePath results = new Upgrade.Factory(upgradePathFactory(), upgradeStatusTableServiceFactory(connection), graphBasedUpgradeScriptGeneratorFactory, viewChangesDeploymentHelperFactory(connection), viewDeploymentValidatorFactory()) + .create(connection) + .findPath(targetSchema, upgradeSteps, new HashSet<>(), connection.getDataSource()); // Then assertTrue("No steps to apply", results.getSteps().isEmpty()); From 55e8cada376cc54cec090e6728960bdbbb28dc61 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Mon, 26 Feb 2024 20:15:05 +0000 Subject: [PATCH 17/25] Resolving merge conflicts. --- .../alfasoftware/morf/upgrade/Upgrade.java | 10 +++-- .../morf/upgrade/TestUpgrade.java | 37 ++++--------------- .../jdbc/postgresql/PostgreSQLDialect.java | 2 +- 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java b/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java index de07789f5..f223431ac 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java @@ -15,16 +15,20 @@ package org.alfasoftware.morf.upgrade; +import static org.alfasoftware.morf.metadata.SchemaUtils.copy; import static org.alfasoftware.morf.sql.SelectStatement.select; import static org.alfasoftware.morf.sql.SqlUtils.tableRef; import static org.alfasoftware.morf.sql.element.Function.count; import static org.alfasoftware.morf.upgrade.UpgradeStatus.NONE; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -36,6 +40,7 @@ import org.alfasoftware.morf.jdbc.SqlScriptExecutor.ResultSetProcessor; import org.alfasoftware.morf.jdbc.SqlScriptExecutorProvider; import org.alfasoftware.morf.metadata.Schema; +import org.alfasoftware.morf.metadata.SchemaResource; import org.alfasoftware.morf.metadata.SchemaValidator; import org.alfasoftware.morf.sql.SelectStatement; import org.alfasoftware.morf.sql.element.TableReference; @@ -45,15 +50,11 @@ import org.alfasoftware.morf.upgrade.UpgradePath.UpgradePathFactoryImpl; import org.alfasoftware.morf.upgrade.UpgradePathFinder.NoUpgradePathExistsException; import org.alfasoftware.morf.upgrade.db.DatabaseUpgradeTableContribution; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; -import java.sql.SQLException; -import java.util.HashMap; -import java.util.Map; /** @@ -344,6 +345,7 @@ private static Schema readSourceDatabaseSchema(ConnectionResources database, Dat try (SchemaResource databaseSchemaResource = database.openSchemaResource(dataSource)) { upgradeStatements.addAll(database.sqlDialect().getSchemaConsistencyStatements(databaseSchemaResource)); return copy(databaseSchemaResource, exclusionRegExes); + } } diff --git a/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java b/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java index 155c216ae..ace97a9fa 100755 --- a/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java +++ b/morf-core/src/test/java/org/alfasoftware/morf/upgrade/TestUpgrade.java @@ -35,7 +35,6 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyListOf; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -178,7 +177,7 @@ public void testUpgrade() throws SQLException { SchemaResource schemaResource = mock(SchemaResource.class); when(mockConnectionResources.openSchemaResource(eq(mockConnectionResources.getDataSource()))).thenReturn(schemaResource); - when(mockConnectionResources.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of("I", "J")); + when(mockConnectionResources.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of("CONS1", "CONS2")); when(schemaResource.tables()).thenReturn(tables); UpgradePath results = new Upgrade.Factory(upgradePathFactory(), upgradeStatusTableServiceFactory(mockConnectionResources), graphBasedUpgradeScriptGeneratorFactory, viewChangesDeploymentHelperFactory(mockConnectionResources), viewDeploymentValidatorFactory(), databaseUpgradeLockServiceFactory()) @@ -186,14 +185,11 @@ public void testUpgrade() throws SQLException { .findPath(targetSchema, upgradeSteps, Lists.newArrayList("^Drivers$", "^EXCLUDE_.*$"), mockConnectionResources.getDataSource()); assertEquals("Should be two steps.", 2, results.getSteps().size()); - assertEquals("Number of SQL statements", 20, results.getSql().size()); // Includes statements to create, truncate and then drop temp table, also 2 comments + assertEquals("Number of SQL statements", 21, results.getSql().size()); // Includes statements to add optimistic locking; create, truncate and then drop temp table; also 2 comments - assertEquals("SQL", "[I, J]", results.getSql().subList(0, 2).toString()); // schema consistency statements - assertEquals("SQL", "-- Upgrade step: org.alfasoftware.morf.upgrade.testupgrade.upgrade.v1_0_0.ChangeCar", results.getSql().get(3).toString()); // upgrade ChangeCar begins - assertEquals("SQL", "-- Upgrade step: org.alfasoftware.morf.upgrade.testupgrade.upgrade.v1_0_0.ChangeDriver", results.getSql().get(10).toString()); // upgrade ChangeDriver begins - - TODO - assertEquals("Number of SQL statements", 19, results.getSql().size()); // Includes statements to add optimistic locking; create, truncate and then drop temp table; also 2 comments + assertEquals("SQL", "[INIT, CONS1, CONS2]", results.getSql().subList(0, 3).toString()); // schema consistency statements + assertEquals("SQL", "-- Upgrade step: org.alfasoftware.morf.upgrade.testupgrade.upgrade.v1_0_0.ChangeCar", results.getSql().get(4).toString()); // upgrade ChangeCar begins + assertEquals("SQL", "-- Upgrade step: org.alfasoftware.morf.upgrade.testupgrade.upgrade.v1_0_0.ChangeDriver", results.getSql().get(11).toString()); // upgrade ChangeDriver begins } @@ -255,15 +251,8 @@ public void testUpgradeWithTriggerMessage() throws SQLException { private UpgradePathFactory upgradePathFactory() { UpgradePathFactory upgradePathFactory = mock(UpgradePathFactory.class); -//<<<<<<< nullable-columns-in-unique-indexes - when(upgradePathFactory.create(anyListOf(UpgradeStep.class), any(ConnectionResources.class), nullable(GraphBasedUpgradeBuilder.class))).thenAnswer(invocation -> { - return new UpgradePath(Sets.newHashSet(), (List)invocation.getArguments()[0], (ConnectionResources)invocation.getArguments()[1], Collections.emptyList(), Collections.emptyList()); - }); -///======= when(upgradePathFactory.create(anyList(), any(ConnectionResources.class), nullable(GraphBasedUpgradeBuilder.class), anyList())) .thenAnswer(invocation -> new UpgradePath(Sets.newHashSet(), invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(3), Collections.emptyList())); -///>>>>>>> main - return upgradePathFactory; } @@ -371,17 +360,17 @@ public void testUpgradeWithOnlySchemaConsistencyStatementsToDeploy() { ConnectionResources connection = mock(ConnectionResources.class, RETURNS_DEEP_STUBS); when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(schemaResource); - when(connection.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of("I", "J")); + when(connection.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of("CONS1", "CONS2")); when(upgradeStatusTableService.getStatus(Optional.of(connection.getDataSource()))).thenReturn(NONE); // When - UpgradePath results = new Upgrade.Factory(upgradePathFactory(), upgradeStatusTableServiceFactory(connection), graphBasedUpgradeScriptGeneratorFactory, viewChangesDeploymentHelperFactory(connection), viewDeploymentValidatorFactory()) + UpgradePath results = new Upgrade.Factory(upgradePathFactory(), upgradeStatusTableServiceFactory(connection), graphBasedUpgradeScriptGeneratorFactory, viewChangesDeploymentHelperFactory(connection), viewDeploymentValidatorFactory(), databaseUpgradeLockServiceFactory()) .create(connection) .findPath(targetSchema, upgradeSteps, new HashSet<>(), connection.getDataSource()); // Then assertTrue("No steps to apply", results.getSteps().isEmpty()); - assertEquals("SQL", "[I, J]", results.getSql().toString()); + assertEquals("SQL", "[INIT, CONS1, CONS2]", results.getSql().toString()); } @@ -409,15 +398,10 @@ public void testUpgradeWithOnlyViewsToDeploy() { when(connection.sqlDialect().viewDeploymentStatements(same(testView))).thenReturn(ImmutableList.of("A")); when(connection.sqlDialect().viewDeploymentStatementsAsLiteral(any(View.class))).thenReturn(literal("W")); when(connection.sqlDialect().rebuildTriggers(any(Table.class))).thenReturn(Collections.emptyList()); -///<<<<<<< nullable-columns-in-unique-indexes when(connection.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of()); when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(schemaResource); - when(upgradeStatusTableService.getStatus(Optional.of(connection.getDataSource()))).thenReturn(NONE); -///======= - when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(new StubSchemaResource(sourceSchema)); when(connection.sqlDialect().truncateTableStatements(any(Table.class))).thenReturn(Lists.newArrayList("1")); when(connection.sqlDialect().dropStatements(any(Table.class))).thenReturn(Lists.newArrayList("2")); -///>>>>>>> main // When UpgradePath result = new Upgrade.Factory(upgradePathFactory(), upgradeStatusTableServiceFactory(connection), graphBasedUpgradeScriptGeneratorFactory, viewChangesDeploymentHelperFactory(connection), viewDeploymentValidatorFactory(), databaseUpgradeLockServiceFactory()) @@ -462,15 +446,10 @@ public void testUpgradeWithChangedViewsToDeploy() { when(connection.sqlDialect().viewDeploymentStatements(same(testView))).thenReturn(ImmutableList.of("A")); when(connection.sqlDialect().viewDeploymentStatementsAsLiteral(any(View.class))).thenReturn(literal("W")); when(connection.sqlDialect().rebuildTriggers(any(Table.class))).thenReturn(Collections.emptyList()); -///<<<<<<< nullable-columns-in-unique-indexes when(connection.sqlDialect().getSchemaConsistencyStatements(schemaResource)).thenReturn(ImmutableList.of()); when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(schemaResource); - when(upgradeStatusTableService.getStatus(Optional.of(connection.getDataSource()))).thenReturn(NONE); -///======= - when(connection.openSchemaResource(eq(connection.getDataSource()))).thenReturn(new StubSchemaResource(sourceSchema)); when(connection.sqlDialect().truncateTableStatements(any(Table.class))).thenReturn(Lists.newArrayList("1")); when(connection.sqlDialect().dropStatements(any(Table.class))).thenReturn(Lists.newArrayList("2")); -///>>>>>>> main // When UpgradePath result = new Upgrade.Factory(upgradePathFactory(), upgradeStatusTableServiceFactory(connection), graphBasedUpgradeScriptGeneratorFactory, viewChangesDeploymentHelperFactory(connection), viewDeploymentValidatorFactory(), databaseUpgradeLockServiceFactory()) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java index 440406a55..b5e252e82 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java @@ -933,4 +933,4 @@ protected String tableNameWithSchemaName(TableReference tableRef) { return tableRef.getDblink() + "." + tableRef.getName(); } } -} +} \ No newline at end of file From 1493867033209580a4e27e5ff03eb1514337de09 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Mon, 26 Feb 2024 20:23:58 +0000 Subject: [PATCH 18/25] Tidy-up. --- .../morf/jdbc/postgresql/PostgreSQLDialect.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java index b5e252e82..7d551bf53 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java @@ -13,6 +13,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.StringJoiner; import org.alfasoftware.morf.jdbc.DatabaseType; @@ -884,9 +885,7 @@ protected String getSqlFrom(DeleteStatement statement) { @Override public List getSchemaConsistencyStatements(SchemaResource schemaResource) { - return schemaResource.getDatabaseMetaDataProvider() - .filter(instanceOf(PostgreSQLMetaDataProvider.class)) - .map(PostgreSQLMetaDataProvider.class::cast) + return getPostgreSQLMetaDataProvider(schemaResource) .map(this::getSchemaConsistencyStatements) .orElseGet(() -> super.getSchemaConsistencyStatements(schemaResource)); } @@ -920,7 +919,14 @@ private Iterable healIndexes(Collection additionalC return new PostgreSQLUniqueIndexAdditionalDeploymentStatements(table, index) .healIndexStatements(additionalConstraintIndexInfo, schemaNamePrefix(table) + table.getName()); } - + + + private Optional getPostgreSQLMetaDataProvider(SchemaResource schemaResource) { + return schemaResource.getDatabaseMetaDataProvider() + .filter(instanceOf(PostgreSQLMetaDataProvider.class)) + .map(PostgreSQLMetaDataProvider.class::cast); + } + /** * @see org.alfasoftware.morf.jdbc.SqlDialect#tableNameWithSchemaName(org.alfasoftware.morf.sql.element.TableReference) From fafd6de0bec15ad255aa7aa995356d16ca39bfaf Mon Sep 17 00:00:00 2001 From: jsimlo Date: Mon, 26 Feb 2024 21:03:24 +0000 Subject: [PATCH 19/25] Log whenever auto-healing strikes. --- .../main/java/org/alfasoftware/morf/upgrade/Upgrade.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java b/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java index f223431ac..0fa6259bb 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java @@ -343,7 +343,13 @@ private UpgradePath buildUpgradePath( */ private static Schema readSourceDatabaseSchema(ConnectionResources database, DataSource dataSource, Collection exclusionRegExes, List upgradeStatements) { try (SchemaResource databaseSchemaResource = database.openSchemaResource(dataSource)) { - upgradeStatements.addAll(database.sqlDialect().getSchemaConsistencyStatements(databaseSchemaResource)); + List schemaConsistencyStatements = database.sqlDialect().getSchemaConsistencyStatements(databaseSchemaResource); + if (!schemaConsistencyStatements.isEmpty()) { + log.warn("Auto-healing statements have been generated (" + schemaConsistencyStatements.size() + " statements in total); this usually implies auto-healing being carried out." + + " It this is shown on each subsequent start-up, it can be a symptom of auto-healing failing to achieve an acceptable stable healthful state." + + " Examine the upgrade statements (the upgrade script, or the upgrade logs below) to investigate further."); + } + upgradeStatements.addAll(schemaConsistencyStatements); return copy(databaseSchemaResource, exclusionRegExes); } } From 0095fcfb58e34a5bca8989258821934094ec8255 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Tue, 27 Feb 2024 18:55:52 +0000 Subject: [PATCH 20/25] Logging. --- .../alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java index 7d551bf53..94724502f 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java @@ -902,7 +902,7 @@ private Iterable healTable(PostgreSQLMetaDataProvider metaDataProvider, Iterable statements = healIndexes(metaDataProvider, table); if (statements.iterator().hasNext()) { - List intro = ImmutableList.of(convertCommentToSQL("Healing table: " + table.getName())); + List intro = ImmutableList.of(convertCommentToSQL("Auto-Healing table: " + table.getName())); return Iterables.concat(intro, statements); } return ImmutableList.of(); From e4292d5d9941704d481151124cee6f147ded81f8 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Wed, 28 Feb 2024 19:16:48 +0000 Subject: [PATCH 21/25] Bring schemaConsistencyStatements to Upgrade. --- .../alfasoftware/morf/upgrade/Upgrade.java | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java b/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java index 0fa6259bb..edbc27697 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java @@ -188,11 +188,21 @@ public UpgradePath findPath(Schema targetSchema, Collection schemaConsistencyStatements; + Schema sourceSchema; + try (SchemaResource databaseSchemaResource = connectionResources.openSchemaResource(dataSource)) { + sourceSchema = copy(databaseSchemaResource, exceptionRegexes); + schemaConsistencyStatements = connectionResources.sqlDialect().getSchemaConsistencyStatements(databaseSchemaResource); + if (!schemaConsistencyStatements.isEmpty()) { + log.warn("Auto-healing statements have been generated (" + schemaConsistencyStatements.size() + " statements in total); this usually implies auto-healing being carried out." + + " It this is shown on each subsequent start-up, it can be a symptom of auto-healing failing to achieve an acceptable stable healthful state." + + " Examine the upgrade statements (the upgrade script, or the upgrade logs below) to investigate further."); + } + } // -- Get the current UUIDs and deployed views... log.info("Examining current views"); // + SqlDialect dialect = connectionResources.sqlDialect(); ExistingViewStateLoader existingViewState = new ExistingViewStateLoader(dialect, new ExistingViewHashLoader(dataSource, dialect), viewDeploymentValidator); Result viewChangeInfo = existingViewState.viewChanges(sourceSchema, targetSchema); ViewChanges viewChanges = new ViewChanges(targetSchema.views(), viewChangeInfo.getViewsToDrop(), viewChangeInfo.getViewsToDeploy()); @@ -331,30 +341,6 @@ private UpgradePath buildUpgradePath( } - /** - * Gets a copy of the source schema from the {@code database}. - * Also gathers up any schema consistency statements. - * - * @param database the database to connect to. - * @param dataSource the dataSource to use. - * @param exclusionRegExes collection of regular expressions describing tables/views to exclude from the schema. - * @param upgradeStatements adds schema consistency statements to this list. - * @return the schema. - */ - private static Schema readSourceDatabaseSchema(ConnectionResources database, DataSource dataSource, Collection exclusionRegExes, List upgradeStatements) { - try (SchemaResource databaseSchemaResource = database.openSchemaResource(dataSource)) { - List schemaConsistencyStatements = database.sqlDialect().getSchemaConsistencyStatements(databaseSchemaResource); - if (!schemaConsistencyStatements.isEmpty()) { - log.warn("Auto-healing statements have been generated (" + schemaConsistencyStatements.size() + " statements in total); this usually implies auto-healing being carried out." - + " It this is shown on each subsequent start-up, it can be a symptom of auto-healing failing to achieve an acceptable stable healthful state." - + " Examine the upgrade statements (the upgrade script, or the upgrade logs below) to investigate further."); - } - upgradeStatements.addAll(schemaConsistencyStatements); - return copy(databaseSchemaResource, exclusionRegExes); - } - } - - /** * Provides a number of already applied upgrade steps. * @return the number of upgrade steps from the UpgradeAudit table From d2d0e12cd5cd391041fd0fd383c468ed900e79c8 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Fri, 19 Apr 2024 14:27:33 +0100 Subject: [PATCH 22/25] Ignore failures. --- ...niqueIndexAdditionalDeploymentStatements.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java index cae80ef64..9fe8858ee 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -167,7 +167,7 @@ private Iterable createIndexStatement(String prefixedTableName, Iterable String commentStatement = commentOnIndexSqlStatement(fullIndexName, indexHash); - return ImmutableList.of(createStatement, commentStatement); + return ImmutableList.of(createIndexStatementBlock(createStatement, commentStatement)); } @@ -279,7 +279,7 @@ public Iterable createIndexStatements(String prefixedTableName) { String commentStatement = commentOnIndexSqlStatement(fullIndexName, indexHash); - return ImmutableList.of(createStatement, commentStatement); + return ImmutableList.of(createIndexStatementBlock(createStatement, commentStatement)); } @@ -457,6 +457,18 @@ private static String dropIndexSqlStatement(String fullIndexName) { } + private static String createIndexStatementBlock(String createStatement, String commentStatement) { + return "DO $IndexAutoHealing$" + "\n" + + "BEGIN" + "\n" + + " " + createStatement + ";\n" + + " " + commentStatement + ";\n" + + "EXCEPTION" + "\n" + + " WHEN UNIQUE_VIOLATION THEN" + "\n" + + " NULL; -- ignore the error" + "\n" + + "END $IndexAutoHealing$"; + } + + /** * Recognises a name of additional supporting constraint index. * From f5c9a8b8307f112223f4219f7127fdfdbbf33070 Mon Sep 17 00:00:00 2001 From: jsimlo Date: Fri, 19 Apr 2024 14:28:49 +0100 Subject: [PATCH 23/25] Added DatabaseMetaDataProvider.getDatabaseInformation() --- .../morf/jdbc/DatabaseMetaDataProvider.java | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java index cafa861a4..e0ac87aaa 100755 --- a/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java +++ b/morf-core/src/main/java/org/alfasoftware/morf/jdbc/DatabaseMetaDataProvider.java @@ -15,6 +15,8 @@ package org.alfasoftware.morf.jdbc; +import static org.alfasoftware.morf.util.SchemaValidatorUtil.validateSchemaName; + import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; @@ -54,8 +56,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; -import static org.alfasoftware.morf.util.SchemaValidatorUtil.validateSchemaName; - /** * Provides meta data based on a database connection. * @@ -92,6 +92,11 @@ public class DatabaseMetaDataProvider implements Schema { protected static final int PRIMARY_COLUMN_NAME = 4; protected static final int PRIMARY_KEY_SEQ = 5; + // Keys for Database Information map + public static final String DATABASE_PRODUCT_VERSION = "DatabaseProductVersion"; + public static final String DATABASE_MAJOR_VERSION = "DatabaseMajorVersion"; + public static final String DATABASE_MINOR_VERSION = "DatabaseMinorVersion"; + protected final Connection connection; protected final String schemaName; @@ -105,6 +110,8 @@ public class DatabaseMetaDataProvider implements Schema { private final Supplier> viewNames = Suppliers.memoize(this::loadAllViewNames); private final LoadingCache viewCache = CacheBuilder.newBuilder().build(CacheLoader.from(this::loadView)); + private final Supplier> databaseInformation = Suppliers.memoize(this::loadDatabaseInformation); + /** * @param connection The database connection from which meta data should be provided. @@ -117,6 +124,27 @@ protected DatabaseMetaDataProvider(Connection connection, String schemaName) { } + public Map getDatabaseInformation() { + return databaseInformation.get(); + } + + + private Map loadDatabaseInformation() { + try { + final DatabaseMetaData databaseMetaData = connection.getMetaData(); + + return ImmutableMap.builder() + .put(DATABASE_PRODUCT_VERSION, databaseMetaData.getDatabaseProductVersion()) + .put(DATABASE_MAJOR_VERSION, String.valueOf(databaseMetaData.getDatabaseMajorVersion())) + .put(DATABASE_MINOR_VERSION, String.valueOf(databaseMetaData.getDatabaseMinorVersion())) + .build(); + } + catch (SQLException e) { + throw new RuntimeSqlException(e); + } + } + + /** * @see org.alfasoftware.morf.metadata.Schema#isEmptyDatabase() */ From 3bade633ace60948997c012e9c0ee382f6c46b1c Mon Sep 17 00:00:00 2001 From: jsimlo Date: Fri, 19 Apr 2024 14:36:00 +0100 Subject: [PATCH 24/25] Prep for Postgres 15. --- .../morf/jdbc/postgresql/PostgreSQLDialect.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java index 94724502f..761c88249 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLDialect.java @@ -16,6 +16,7 @@ import java.util.Optional; import java.util.StringJoiner; +import org.alfasoftware.morf.jdbc.DatabaseMetaDataProvider; import org.alfasoftware.morf.jdbc.DatabaseType; import org.alfasoftware.morf.jdbc.NamedParameterPreparedStatement; import org.alfasoftware.morf.jdbc.SqlDialect; @@ -910,6 +911,15 @@ private Iterable healTable(PostgreSQLMetaDataProvider metaDataProvider, private Iterable healIndexes(PostgreSQLMetaDataProvider metaDataProvider, Table table) { + // Postgres 15 can deal with this problem on it's own + if (Integer.parseInt(metaDataProvider.getDatabaseInformation().get(DatabaseMetaDataProvider.DATABASE_MAJOR_VERSION)) >= 15) { + // TODO + // See https://www.postgresql.org/docs/current/sql-createindex.html + // Once we support Postgres 15, we should introduce CREATE INDEX ... NULLS NOT DISTINCT + // And drop any existing AdditionalConstraintIndexes already created + } + + // Older PG requires additional indexes to be added return FluentIterable.from(table.indexes()) .transformAndConcat(index -> healIndexes(metaDataProvider.getAdditionalConstraintIndexes(index.getName().toLowerCase()), table, index)); } From 593255aea6e827d518b147cb9c8813bb4d0eb44a Mon Sep 17 00:00:00 2001 From: jsimlo Date: Mon, 3 Jun 2024 12:07:32 +0100 Subject: [PATCH 25/25] JavaDoc fix. --- .../PostgreSQLUniqueIndexAdditionalDeploymentStatements.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java index 9fe8858ee..6a1477ffb 100644 --- a/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java +++ b/morf-postgresql/src/main/java/org/alfasoftware/morf/jdbc/postgresql/PostgreSQLUniqueIndexAdditionalDeploymentStatements.java @@ -100,7 +100,7 @@ private interface Strategy { * CREATE UNIQUE INDEX Test_1$null0 ON schema.Test (floatField) WHERE intField IS NULL; * * - * Sample result (floatField being the only non-nullable column): + * Sample result (stringField and intField being the two non-nullable columns): *
      * CREATE UNIQUE INDEX indexName ON testschema.Test (stringField, intField, floatField, dateField);
      * CREATE UNIQUE INDEX indexName$null0 ON testschema.Test (intField, floatField, dateField) WHERE stringField IS NULL;