Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
08598df
Add comprehensive test suite for UpgradeGraph class
Feb 4, 2026
031546d
Add DeferredIndexConfig with defaults for deferred index execution
Feb 20, 2026
2ce4961
Add DeferredIndexOperation system tables and bootstrap upgrade step
Feb 20, 2026
c0ca5d9
Add DeferredIndexOperation domain class, enums, and DAO
Feb 21, 2026
ac910c3
Add DeferredAddIndex SchemaChange with visitor wiring and DAO interfa…
Feb 21, 2026
92257b1
Add SchemaEditor.addIndexDeferred() and visitor wiring for Stage 5
Feb 22, 2026
b92d7dc
Add auto-cancel and dependency tracking for deferred index operations
Feb 28, 2026
045d336
Add DeferredIndexExecutor, RecoveryService, and Validator (Stages 7-10)
Mar 1, 2026
5b87bbf
Add cross-platform deferred index dialect support (Stage 11)
Mar 2, 2026
67625fd
Add end-to-end integration tests for deferred index lifecycle (Stage 12)
Mar 2, 2026
a6c1e4d
Fix review findings: timestamp format, boolean column, and ChangeInde…
Mar 2, 2026
9738338
Cap retry backoff delay and fail upgrade on unresolved deferred indexes
Mar 2, 2026
6342d68
Fix review findings #4, #5, #7: backoff cap, validator throw, in-memo…
Mar 2, 2026
a50f492
Use BIG_INTEGER primary keys for both deferred index tables
Mar 3, 2026
fcb61a9
Replace N+1 queries with JOIN in DeferredIndexOperationDAO
Mar 3, 2026
7a691a9
Fix case-sensitivity inconsistencies in deferred index handling
Mar 3, 2026
286d7dc
Widen deferred index table name columns to SchemaValidator.MAX_LENGTH
Mar 3, 2026
47b00bc
Remove dead code and fix stale comment
Mar 3, 2026
66a3c52
Remove @ImplementedBy and @Inject from DAO since it is always constru…
Mar 3, 2026
f2c4248
Distinguish deferred index in human-readable upgrade output
Mar 3, 2026
6405207
Add config validation to deferred index services
Mar 3, 2026
d06bcbb
Add DeferredIndexService facade and make internal classes package-pri…
Mar 3, 2026
b68ffbe
Fix stale assertions in TestUpgradeSteps for deferred index tables
Mar 3, 2026
c801e00
Add unit tests to fill coverage gaps in deferred index feature
Mar 3, 2026
805bd6c
Improve test coverage for deferred index feature
Mar 3, 2026
1b7df21
Refactor deferred index services to use Guice constructor injection
Mar 3, 2026
594f2e3
Add force-immediate config to bypass deferred index creation
Mar 3, 2026
0ec04b8
Add force-deferred config to override immediate index creation
Mar 3, 2026
47591b2
Add coverage for forceImmediateIndexes and forceDeferredIndexes getters
Mar 3, 2026
84941aa
Merge origin/main into experimental/deferred-index-creation
Mar 3, 2026
253301f
Fix review findings: stale rename, negative IDs, SKIPPED status, javadoc
Mar 3, 2026
2b4fa36
Add DEBUG logging to deferred index services
Mar 3, 2026
0723c35
Code review fixes: remove dead code, simplify timestamps, decouple DAO
Mar 3, 2026
babf682
Refactor DeferredIndexChangeServiceImpl: extract SQL builders, add ja…
Mar 3, 2026
3f42844
Rename operationTimeoutSeconds to executionTimeoutSeconds, default 8h
Mar 3, 2026
d5984b1
Extract interfaces for DeferredIndexExecutor, DeferredIndexRecoverySe…
Mar 4, 2026
aa12945
Extract ExecutionResult/ExecutionStatus from DeferredIndexExecutor, r…
Mar 4, 2026
b885f8c
Replace polling with CompletableFuture, validate config in execute()
Mar 4, 2026
8533866
Ensure deferred index tables exist before parallel upgrade steps
Mar 4, 2026
4aa85ea
Rename DeferredIndexValidator to DeferredIndexReadinessCheck, wire in…
Mar 4, 2026
b792b48
Add DeferredIndexExecutorServiceFactory for pluggable thread pool cre…
Mar 4, 2026
cbf4147
Remove test-only constructor from DeferredIndexExecutorImpl
Mar 4, 2026
bfbcd84
Replace ScheduledExecutorService with per-operation progress logging
Mar 5, 2026
b15f6ea
Add getProgress() to DeferredIndexService facade
Mar 5, 2026
b607831
Executor cleanup: INFO/ERROR logging with elapsed time, autocommit re…
Mar 5, 2026
564bb99
Add Javadoc to all non-public methods across deferred index package
Mar 5, 2026
5ae9af7
Code review fixes: remove dead DAO methods, drop operationType column…
Mar 5, 2026
85056d9
Remove shutdown() from DeferredIndexExecutor interface
Mar 5, 2026
874c8b0
Add TDD integration tests for deferred index lifecycle (expect compil…
Mar 5, 2026
a5e7d41
Rename DeferredIndexConfig to DeferredIndexExecutionConfig, add force…
Mar 5, 2026
3ff3a90
Fix Mode 1: move readiness check before sourceSchema capture, add sch…
Mar 5, 2026
106f086
Executor crash recovery + remove recovery service from DeferredIndexS…
Mar 5, 2026
3894fe8
Remove recovery service, dead DAO method, fix lifecycle test index va…
Mar 5, 2026
4764e56
Code review fixes: dedup reconstructIndex, remove dead DAO methods, a…
Mar 5, 2026
ebb04e7
Remove redundant fields from executor, remove dead DAO insertOperatio…
Mar 6, 2026
b198c46
Simplify resetAllInProgressToPending, remove noOp(), fix javadoc wording
Mar 6, 2026
998e11c
Rename run/augment methods, extract awaitCompletion, add stale-index log
Mar 6, 2026
e3c3722
Fix DeferredIndexReadinessCheck Javadoc to describe both modes
Mar 9, 2026
824ae21
Code review fixes: inline upgrade tables, re-defer on ChangeIndex, re…
Mar 9, 2026
d50fd08
Remove unnecessary volatile from executionFuture field
Mar 9, 2026
af1dd91
Code review fixes: harden executor, DAO, config validation, fix flaky…
Mar 18, 2026
7ee6dda
Remove Mode 1/Mode 2, simplify to unified deferred index behavior
Mar 19, 2026
1624541
Add @see Javadoc to all @Override methods, split POJO test into per-f…
Mar 19, 2026
03bbf9a
Remove plan files from repo, add PLAN-*.md to .gitignore
Mar 19, 2026
b1c2148
Add CLAUDE.md to .gitignore
Mar 19, 2026
76b2d7f
Fix stray characters in readiness check Javadoc
Mar 19, 2026
e44d503
Add dialect-level deferred index support, fall back to immediate on u…
Mar 19, 2026
28a7eb1
Add supportsDeferredIndexCreation() override to H2v2 dialect, fix cha…
Mar 20, 2026
66c3f37
Remove DeferredIndexOperationColumn table, store columns as comma-sep…
Mar 24, 2026
0eb027f
Move deferred index config into UpgradeConfigAndContext, validate at …
Mar 24, 2026
3cb8049
Add deferredIndexCreationEnabled kill switch, disabled by default
Mar 24, 2026
31d43cb
Fix SonarCloud code smells: extract constants, clean up imports, refa…
Mar 24, 2026
3161ecf
Code review fixes: constants, Javadoc, private visibility, redundant …
Mar 24, 2026
b7a93fc
Backport code review fixes from comments-based branch
Apr 4, 2026
7d0bb7f
Add given/when/then structure to backported integration tests
Apr 5, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ target
*.iml
.idea
**/ivy-ide-settings.properties
PLAN-*.md
CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ public Upgrade provideUpgrade(ConnectionResources connectionResources,
ViewDeploymentValidator viewDeploymentValidator,
DatabaseUpgradePathValidationService databaseUpgradePathValidationService,
GraphBasedUpgradeBuilderFactory graphBasedUpgradeBuilderFactory,
UpgradeConfigAndContext upgradeConfigAndContext) {
UpgradeConfigAndContext upgradeConfigAndContext,
org.alfasoftware.morf.upgrade.deferred.DeferredIndexReadinessCheck deferredIndexReadinessCheck) {
return new Upgrade(connectionResources, factory, upgradeStatusTableService, viewChangesDeploymentHelper,
viewDeploymentValidator, databaseUpgradePathValidationService, graphBasedUpgradeBuilderFactory,
upgradeConfigAndContext);
upgradeConfigAndContext, deferredIndexReadinessCheck);
}
}

56 changes: 54 additions & 2 deletions morf-core/src/main/java/org/alfasoftware/morf/jdbc/SqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -4047,6 +4047,40 @@ public Collection<String> addIndexStatements(Table table, Index index) {
}


/**
* Whether this dialect supports deferred index creation. When {@code true},
* {@link org.alfasoftware.morf.upgrade.SchemaEditor#addIndexDeferred} queues
* the index for background creation. When {@code false}, deferred requests
* are silently converted to immediate index creation, because the platform's
* {@code CREATE INDEX} blocks DML and deferring would move the lock from the
* upgrade window (when no traffic is flowing) to post-startup (when it is).
*
* <p>The default returns {@code false}. Dialects that support non-blocking
* DDL (e.g. PostgreSQL {@code CONCURRENTLY}, Oracle {@code ONLINE}) should
* override this to return {@code true}.</p>
*
* @return {@code true} if deferred index creation is beneficial on this platform.
*/
public boolean supportsDeferredIndexCreation() {
return false;
}


/**
* Generates the SQL to build a deferred index on an existing table. By default this
* delegates to {@link #addIndexStatements(Table, Index)}, which issues a standard
* {@code CREATE INDEX} statement. Platform-specific dialects may override this method
* to emit non-blocking variants (e.g. {@code CREATE INDEX CONCURRENTLY} on PostgreSQL).
*
* @param table The existing table.
* @param index The new index to build in the background.
* @return A collection of SQL statements.
*/
public Collection<String> deferredIndexDeploymentStatements(Table table, Index index) {
return addIndexStatements(table, index);
}


/**
* Helper method to create all index statements defined for a table
*
Expand All @@ -4070,13 +4104,31 @@ protected List<String> createAllIndexStatements(Table table) {
* @return The SQL to deploy the index on the table.
*/
protected Collection<String> indexDeploymentStatements(Table table, Index index) {
return ImmutableList.of(buildCreateIndexStatement(table, index, ""));
}


/**
* Builds a {@code CREATE [UNIQUE] INDEX} statement with an optional keyword
* inserted between {@code INDEX} and the index name (e.g. {@code "CONCURRENTLY"}).
*
* @param table The table to create the index on.
* @param index The index to create.
* @param afterIndexKeyword keyword to insert after {@code INDEX}, or empty string for none.
* @return the complete CREATE INDEX SQL string.
*/
protected String buildCreateIndexStatement(Table table, Index index, String afterIndexKeyword) {
StringBuilder statement = new StringBuilder();

statement.append("CREATE ");
if (index.isUnique()) {
statement.append("UNIQUE ");
}
statement.append("INDEX ")
statement.append("INDEX ");
if (!afterIndexKeyword.isEmpty()) {
statement.append(afterIndexKeyword).append(' ');
}
statement
.append(schemaNamePrefix(table))
.append(index.getName())
.append(" ON ")
Expand All @@ -4086,7 +4138,7 @@ protected Collection<String> indexDeploymentStatements(Table table, Index index)
.append(Joiner.on(", ").join(index.columnNames()))
.append(')');

return ImmutableList.of(statement.toString());
return statement.toString();
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@

package org.alfasoftware.morf.upgrade;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

import org.alfasoftware.morf.jdbc.SqlDialect;
import org.alfasoftware.morf.metadata.Index;
import org.alfasoftware.morf.metadata.Schema;
import org.alfasoftware.morf.metadata.Table;
import org.alfasoftware.morf.sql.Statement;
import org.alfasoftware.morf.upgrade.deferred.DeferredAddIndex;
import org.alfasoftware.morf.upgrade.deferred.DeferredIndexChangeService;
import org.alfasoftware.morf.upgrade.deferred.DeferredIndexChangeServiceImpl;

/**
* Common code between SchemaChangeVisitor implementors
Expand All @@ -20,6 +25,7 @@ public abstract class AbstractSchemaChangeVisitor implements SchemaChangeVisitor
protected final Table idTable;
protected final TableNameResolver tracker;

private final DeferredIndexChangeService deferredIndexChangeService = new DeferredIndexChangeServiceImpl();

public AbstractSchemaChangeVisitor(Schema currentSchema, UpgradeConfigAndContext upgradeConfigAndContext, SqlDialect sqlDialect,
Table idTable) {
Expand Down Expand Up @@ -66,6 +72,7 @@ public void visit(AddTable addTable) {
@Override
public void visit(RemoveTable removeTable) {
currentSchema = removeTable.apply(currentSchema);
deferredIndexChangeService.cancelAllPendingForTable(removeTable.getTable().getName()).forEach(this::visitStatement);
writeStatements(sqlDialect.dropStatements(removeTable.getTable()));
}

Expand All @@ -80,37 +87,57 @@ public void visit(AddColumn addColumn) {
@Override
public void visit(ChangeColumn changeColumn) {
currentSchema = changeColumn.apply(currentSchema);
deferredIndexChangeService.updatePendingColumnName(changeColumn.getTableName(), changeColumn.getFromColumn().getName(), changeColumn.getToColumn().getName()).forEach(this::visitStatement);
writeStatements(sqlDialect.alterTableChangeColumnStatements(currentSchema.getTable(changeColumn.getTableName()), changeColumn.getFromColumn(), changeColumn.getToColumn()));
}


@Override
public void visit(RemoveColumn removeColumn) {
currentSchema = removeColumn.apply(currentSchema);
deferredIndexChangeService.cancelPendingReferencingColumn(removeColumn.getTableName(), removeColumn.getColumnDefinition().getName()).forEach(this::visitStatement);
writeStatements(sqlDialect.alterTableDropColumnStatements(currentSchema.getTable(removeColumn.getTableName()), removeColumn.getColumnDefinition()));
}


@Override
public void visit(RemoveIndex removeIndex) {
currentSchema = removeIndex.apply(currentSchema);
writeStatements(sqlDialect.indexDropStatements(currentSchema.getTable(removeIndex.getTableName()), removeIndex.getIndexToBeRemoved()));
String tableName = removeIndex.getTableName();
String indexName = removeIndex.getIndexToBeRemoved().getName();
if (deferredIndexChangeService.hasPendingDeferred(tableName, indexName)) {
deferredIndexChangeService.cancelPending(tableName, indexName).forEach(this::visitStatement);
} else {
writeStatements(sqlDialect.indexDropStatements(currentSchema.getTable(tableName), removeIndex.getIndexToBeRemoved()));
}
}


@Override
public void visit(ChangeIndex changeIndex) {
currentSchema = changeIndex.apply(currentSchema);
writeStatements(sqlDialect.indexDropStatements(currentSchema.getTable(changeIndex.getTableName()), changeIndex.getFromIndex()));
writeStatements(sqlDialect.addIndexStatements(currentSchema.getTable(changeIndex.getTableName()), changeIndex.getToIndex()));
String tableName = changeIndex.getTableName();
Optional<DeferredAddIndex> existing = deferredIndexChangeService.getPendingDeferred(tableName, changeIndex.getFromIndex().getName());
if (existing.isPresent()) {
deferredIndexChangeService.cancelPending(tableName, changeIndex.getFromIndex().getName()).forEach(this::visitStatement);
deferredIndexChangeService.trackPending(new DeferredAddIndex(existing.get().getTableName(), changeIndex.getToIndex(), existing.get().getUpgradeUUID())).forEach(this::visitStatement);
} else {
writeStatements(sqlDialect.indexDropStatements(currentSchema.getTable(tableName), changeIndex.getFromIndex()));
writeStatements(sqlDialect.addIndexStatements(currentSchema.getTable(tableName), changeIndex.getToIndex()));
}
}


@Override
public void visit(final RenameIndex renameIndex) {
currentSchema = renameIndex.apply(currentSchema);
writeStatements(sqlDialect.renameIndexStatements(currentSchema.getTable(renameIndex.getTableName()),
renameIndex.getFromIndexName(), renameIndex.getToIndexName()));
String tableName = renameIndex.getTableName();
if (deferredIndexChangeService.hasPendingDeferred(tableName, renameIndex.getFromIndexName())) {
deferredIndexChangeService.updatePendingIndexName(tableName, renameIndex.getFromIndexName(), renameIndex.getToIndexName()).forEach(this::visitStatement);
} else {
writeStatements(sqlDialect.renameIndexStatements(currentSchema.getTable(tableName),
renameIndex.getFromIndexName(), renameIndex.getToIndexName()));
}
}


Expand All @@ -120,6 +147,7 @@ public void visit(RenameTable renameTable) {
currentSchema = renameTable.apply(currentSchema);
Table newTable = currentSchema.getTable(renameTable.getNewTableName());

deferredIndexChangeService.updatePendingTableName(renameTable.getOldTableName(), renameTable.getNewTableName()).forEach(this::visitStatement);
writeStatements(sqlDialect.renameTableStatements(oldTable, newTable));
}

Expand Down Expand Up @@ -196,6 +224,22 @@ private void visitPortableSqlStatement(PortableSqlStatement sql) {
}


/**
* @see org.alfasoftware.morf.upgrade.SchemaChangeVisitor#visit(org.alfasoftware.morf.upgrade.deferred.DeferredAddIndex)
*/
@Override
public void visit(DeferredAddIndex deferredAddIndex) {
if (!sqlDialect.supportsDeferredIndexCreation()) {
// Dialect does not support deferred index creation — fall back to
// building the index immediately during the upgrade.
visit(new AddIndex(deferredAddIndex.getTableName(), deferredAddIndex.getNewIndex()));
return;
}
currentSchema = deferredAddIndex.apply(currentSchema);
deferredIndexChangeService.trackPending(deferredAddIndex).forEach(this::visitStatement);
}


/**
* @see org.alfasoftware.morf.upgrade.SchemaChangeVisitor#visit(org.alfasoftware.morf.upgrade.AddIndex)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ public void addIndex(String tableName, Index index) {
consumer.schemaChange(HumanReadableStatementHelper.generateAddIndexString(tableName, index));
}

/** @see org.alfasoftware.morf.upgrade.SchemaEditor#addIndexDeferred(java.lang.String, org.alfasoftware.morf.metadata.Index) **/
@Override
public void addIndexDeferred(String tableName, Index index) {
consumer.schemaChange("Add index (deferred if supported): " + HumanReadableStatementHelper.generateAddIndexString(tableName, index));
}

/** @see org.alfasoftware.morf.upgrade.SchemaEditor#addTable(org.alfasoftware.morf.metadata.Table) **/
@Override
public void addTable(Table definition) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.alfasoftware.morf.upgrade;

import org.alfasoftware.morf.upgrade.deferred.DeferredAddIndex;

/**
* Interface for adapting schema changes, i.e. {@link SchemaChange} implementations.
*
Expand Down Expand Up @@ -169,6 +171,16 @@ public default RemoveSequence adapt(RemoveSequence removeSequence) {
}


/**
* Perform adapt operation on a {@link DeferredAddIndex} instance.
*
* @param deferredAddIndex instance of {@link DeferredAddIndex} to adapt.
*/
public default DeferredAddIndex adapt(DeferredAddIndex deferredAddIndex) {
return deferredAddIndex;
}


/**
* Simply uses the default implementation, which is already no-op.
* By no-op, we mean non-changing: the input is passed through as output.
Expand All @@ -190,22 +202,22 @@ public Combining(SchemaChangeAdaptor first, SchemaChangeAdaptor second) {
this.second = second;
}

@Override
@Override
public AddColumn adapt(AddColumn addColumn) {
return second.adapt(first.adapt(addColumn));
}

@Override
@Override
public AddTable adapt(AddTable addTable) {
return second.adapt(first.adapt(addTable));
}

@Override
@Override
public RemoveTable adapt(RemoveTable removeTable) {
return second.adapt(first.adapt(removeTable));
}

@Override
@Override
public AddIndex adapt(AddIndex addIndex) {
return second.adapt(first.adapt(addIndex));
}
Expand Down Expand Up @@ -269,5 +281,13 @@ public AddSequence adapt(AddSequence addSequence) {
public RemoveSequence adapt(RemoveSequence removeSequence) {
return second.adapt(first.adapt(removeSequence));
}

/**
* @see org.alfasoftware.morf.upgrade.SchemaChangeAdaptor#adapt(org.alfasoftware.morf.upgrade.deferred.DeferredAddIndex)
*/
@Override
public DeferredAddIndex adapt(DeferredAddIndex deferredAddIndex) {
return second.adapt(first.adapt(deferredAddIndex));
}
}
}
Loading