Skip to content

Introducing additional technical indexes to Postgres to support nullable columns in unique indexes the same way Oracle does #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
add0b19
Introducing additional technical indexes to Postgres to support nulla…
jsimlo Jun 20, 2022
16ba335
Reading metadata for additional technical Postgres indexes, which sup…
jsimlo Jun 20, 2022
7519e21
Auto-healing additional technical Postgres indexes, which support nul…
jsimlo Jun 21, 2022
9617a75
Missing tests.
jsimlo Jun 21, 2022
37c99d9
Javadoc fixes.
jsimlo Jun 22, 2022
1df4c93
Sonar fixes.
jsimlo Jun 22, 2022
44720e9
Sonar fix.
jsimlo Jun 22, 2022
5d55def
Sonar fixes.
jsimlo Jun 22, 2022
edbb25b
Test coverage.
jsimlo Jun 23, 2022
e33c0b3
Test coverage.
jsimlo Jun 23, 2022
d56e9a1
Test coverage.
jsimlo Jun 23, 2022
5984f7b
Test coverage.
jsimlo Jun 23, 2022
8e02e1a
Test coverage.
jsimlo Jun 23, 2022
48f0684
Sonar fix.
jsimlo Jun 23, 2022
06859a0
Merge branch 'main' into nullable-columns-in-unique-indexes
jsimlo Feb 28, 2023
908ffca
Update PostgreSQLDialect.java
jsimlo Feb 28, 2023
aaea034
Updating tests post-merge.
jsimlo Feb 28, 2023
9e0c429
Merge branch 'main' into nullable-columns-in-unique-indexes
jsimlo Feb 26, 2024
55e8cad
Resolving merge conflicts.
jsimlo Feb 26, 2024
1493867
Tidy-up.
jsimlo Feb 26, 2024
fafd6de
Log whenever auto-healing strikes.
jsimlo Feb 26, 2024
0095fcf
Logging.
jsimlo Feb 27, 2024
e4292d5
Bring schemaConsistencyStatements to Upgrade.
jsimlo Feb 28, 2024
d2d0e12
Ignore failures.
jsimlo Apr 19, 2024
f5c9a8b
Added DatabaseMetaDataProvider.getDatabaseInformation()
jsimlo Apr 19, 2024
3bade63
Prep for Postgres 15.
jsimlo Apr 19, 2024
593255a
JavaDoc fix.
jsimlo Jun 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
Expand All @@ -105,6 +110,8 @@ public class DatabaseMetaDataProvider implements Schema {
private final Supplier<Map<AName, RealName>> viewNames = Suppliers.memoize(this::loadAllViewNames);
private final LoadingCache<AName, View> viewCache = CacheBuilder.newBuilder().build(CacheLoader.from(this::loadView));

private final Supplier<Map<String, String>> databaseInformation = Suppliers.memoize(this::loadDatabaseInformation);


/**
* @param connection The database connection from which meta data should be provided.
Expand All @@ -117,6 +124,27 @@ protected DatabaseMetaDataProvider(Connection connection, String schemaName) {
}


public Map<String, String> getDatabaseInformation() {
return databaseInformation.get();
}


private Map<String, String> loadDatabaseInformation() {
try {
final DatabaseMetaData databaseMetaData = connection.getMetaData();

return ImmutableMap.<String, String>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()
*/
Expand Down Expand Up @@ -693,7 +721,7 @@ protected List<Index> loadTableIndexes(RealName tableName) {
continue;
}
if (DatabaseMetaDataProviderUtils.shouldIgnoreIndex(indexName.getDbName())) {
log.info("Ignoring index: ["+indexName.getDbName()+"]");
ignoreIndexName(indexName, indexResultSet);
continue;
}

Expand All @@ -714,6 +742,9 @@ protected List<Index> 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()
Expand All @@ -740,6 +771,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 resultset on the row with further index information
* @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.
*
Expand Down Expand Up @@ -942,7 +988,7 @@ protected AName(String aName) {
this.hashCode = aName.toLowerCase().hashCode();
}

protected String getAName() {
public String getAName() {
return aName;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -72,10 +76,14 @@ public static Optional<String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Optional;

import javax.sql.DataSource;

Expand Down Expand Up @@ -78,4 +79,15 @@ public void close() {
throw new RuntimeSqlException("Closing", e);
}
}


@Override
public Optional<DatabaseMetaDataProvider> getDatabaseMetaDataProvider() {
if (delegate instanceof DatabaseMetaDataProvider) {
DatabaseMetaDataProvider metaDataProvider = (DatabaseMetaDataProvider)delegate;
return Optional.of(metaDataProvider);
}

return SchemaResource.super.getDatabaseMetaDataProvider();
}
}
12 changes: 12 additions & 0 deletions morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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;
Expand Down Expand Up @@ -4343,6 +4344,17 @@ protected String getSqlForInsertInto(@SuppressWarnings("unused") InsertStatement



/**
* Returns any statements needed to automatically heal the given schema.
*
* @param schemaResource Schema resource that can be examined.
* @return List of statements to be run.
*/
public List<String> getSchemaConsistencyStatements(@SuppressWarnings("unused") SchemaResource schemaResource) {
return ImmutableList.of();
}


/**
* Class representing the structor of an ID Table.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

package org.alfasoftware.morf.metadata;

import java.util.Optional;

import org.alfasoftware.morf.jdbc.DatabaseMetaDataProvider;

/**
* Provides database meta data.
*
Expand All @@ -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<DatabaseMetaDataProvider> getDatabaseMetaDataProvider() {
return Optional.empty();
}
}
23 changes: 17 additions & 6 deletions morf-core/src/main/java/org/alfasoftware/morf/upgrade/Upgrade.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;


/**
Expand Down Expand Up @@ -187,11 +188,21 @@ public UpgradePath findPath(Schema targetSchema, Collection<Class<? extends Upgr

// Get access to the schema we are starting from
log.info("Reading current schema");
Schema sourceSchema = UpgradeHelper.copySourceSchema(connectionResources, dataSource, exceptionRegexes);
SqlDialect dialect = connectionResources.sqlDialect();
List<String> 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());
Expand Down
Loading