Skip to content

InstantDBEvolution

Database schema evolution system for safe incremental updates.

Overview

The InstantDBEvolution unit provides TInstantDBEvolver, which implements safe incremental database schema evolution. Unlike TInstantDBBuilder which drops and rebuilds the entire database, the evolver compares the current database schema with the target model and generates only the necessary changes to bring the database up to date, preserving existing data.

This is the recommended approach for production environments where data preservation is critical.

Key Classes

TInstantDBEvolver

Builds a command sequence that upgrades a database schema to match the current (or specified) business object model by comparing source and target schemas and generating incremental changes.

Inheritance:

TComponent → TInstantCustomDBBuilder → TInstantCustomDBEvolver → TInstantDBEvolver

Key Features:

  • Data Preservation: Modifies schema without dropping tables
  • Incremental Updates: Generates only necessary changes
  • Schema Comparison: Automatically detects differences
  • Safe for Production: Designed for live systems with data
  • Broker Independence: Works with any InstantObjects broker

Core Algorithm

Schema Comparison Process

The evolver implements a comprehensive diff algorithm:

1. Table Evolution

For each table in the target schema (desired state):

  • If table exists in database:
    • Compare and update fields
    • Compare and update indexes
  • If table doesn't exist:
    • Generate ADD TABLE command

For each table in the source schema (current database):

  • If table not in target schema:
    • Generate DROP TABLE command

2. Field Evolution (for existing tables)

For each field in target table:

  • If field exists in database:
    • Compare metadata (type, size, required, etc.)
    • If different: Generate ALTER FIELD command
  • If field doesn't exist:
    • Generate ADD FIELD command

For each field in source table:

  • If field not in target table:
    • Generate DROP FIELD command

3. Index Evolution (for existing tables)

For each index in target table (excluding primary keys):

  • If index exists in database:
    • Compare metadata (fields, options)
    • If different: Generate ALTER INDEX command
  • If index doesn't exist:
    • Generate ADD INDEX command

For each index in source table (excluding primary keys):

  • If index not in target table:
    • Generate DROP INDEX command

Execution Order

Commands are generated in this specific order to maintain referential integrity:

  1. Add/Alter Fields - Modify table structure first
  2. Add/Alter Indexes - Create/update indexes
  3. Drop Indexes - Remove obsolete indexes before fields
  4. Drop Fields - Remove obsolete fields
  5. Add Tables - Create new tables
  6. Drop Tables - Remove obsolete tables

Properties

Inherited from TInstantCustomDBEvolver and TInstantCustomDBBuilder:

PropertyTypeDescription
ConnectorTInstantConnectorDatabase connector to evolve
TargetModelTInstantModelTarget business object model (desired state)
CommandSequenceTInstantDBBuildCommandSequenceGenerated evolution commands
SourceSchemeWarningHandlerTInstantWarningHandlerHandler for warnings when reading current schema

Methods

BuildCommandSequence

pascal
procedure BuildCommandSequence;

Generates the evolution command sequence by:

  1. Reading current database schema (via Connector.Broker.ReadDatabaseScheme)
  2. Creating target schema from model (via Connector.CreateScheme(TargetModel))
  3. Comparing schemas and generating diff commands

Example:

pascal
Evolver.Connector := InstantConnector1;
Evolver.TargetModel := InstantModel1.CurrentModel;
Evolver.BuildCommandSequence; // Generates commands

InternalBuildCommandSequence (Protected)

pascal
procedure InternalBuildCommandSequence; override;

Internal implementation that:

  1. Clears command sequence
  2. Reads source scheme from database
  3. Creates target scheme from model
  4. Calls GenerateSchemeDiff to populate command sequence

GenerateSchemeDiff (Private)

pascal
procedure GenerateSchemeDiff(const CommandSequence: TInstantDBBuildCommandSequence);

Core diff algorithm that compares source and target schemas and generates appropriate commands using:

  • AppendAddTableCommand
  • AppendDropTableCommand
  • AppendAddFieldCommand
  • AppendDropFieldCommand
  • AppendAlterFieldCommand
  • AppendAddIndexCommand
  • AppendDropIndexCommand
  • AppendAlterIndexCommand

Feature Detection

The evolver checks catalog features before generating commands:

ReadTableInfoSupported

pascal
function ReadTableInfoSupported: Boolean;

Returns True if both source and target catalogs support cfReadTableInfo feature.

ReadColumnInfoSupported

pascal
function ReadColumnInfoSupported: Boolean;

Returns True if both catalogs support cfReadColumnInfo feature.

ReadIndexInfoSupported

pascal
function ReadIndexInfoSupported: Boolean;

Returns True if both catalogs support cfReadIndexInfo feature.

Usage Patterns

Basic Schema Evolution

Evolve database to match current model:

pascal
procedure EvolveDatabase(Connector: TInstantConnector; Model: TInstantModel);
var
  Evolver: TInstantDBEvolver;
begin
  Evolver := TInstantDBEvolver.Create(nil);
  try
    Evolver.Connector := Connector;
    Evolver.TargetModel := Model;

    // Generate evolution commands
    Evolver.BuildCommandSequence;

    // Execute evolution
    if Evolver.CommandSequence.Count > 0 then
    begin
      Evolver.CommandSequence.Execute;
      ShowMessage('Database evolved successfully');
    end
    else
      ShowMessage('Database schema is already up to date');
  finally
    Evolver.Free;
  end;
end;

Evolution with Preview

Preview changes before applying:

pascal
procedure EvolveWithPreview(Connector: TInstantConnector; Model: TInstantModel);
var
  Evolver: TInstantDBEvolver;
  Script: TStringList;
begin
  Evolver := TInstantDBEvolver.Create(nil);
  Script := TStringList.Create;
  try
    Evolver.Connector := Connector;
    Evolver.TargetModel := Model;
    Evolver.BuildCommandSequence;

    if Evolver.CommandSequence.Count > 0 then
    begin
      // Generate SQL script
      Evolver.CommandSequence.GenerateScript(Script);

      // Show preview
      if MessageDlg('Evolution Script:' + sLineBreak + sLineBreak +
                    Script.Text + sLineBreak +
                    'Execute these changes?',
                    mtConfirmation, [mbYes, mbNo], 0) = mrYes then
      begin
        Evolver.CommandSequence.Execute;
        ShowMessage('Database evolved successfully');
      end;
    end
    else
      ShowMessage('No changes needed');
  finally
    Script.Free;
    Evolver.Free;
  end;
end;

Evolution with Progress Monitoring

Track evolution progress with event handlers:

pascal
type
  TDatabaseEvolutionForm = class(TForm)
    ProgressBar: TProgressBar;
    StatusLabel: TLabel;
  private
    procedure CommandExecuted(Sender: TObject; Command: TInstantDBBuildCommand);
  public
    procedure EvolveDatabase(Connector: TInstantConnector; Model: TInstantModel);
  end;

procedure TDatabaseEvolutionForm.CommandExecuted(Sender: TObject;
  Command: TInstantDBBuildCommand);
var
  Sequence: TInstantDBBuildCommandSequence;
begin
  Sequence := Sender as TInstantDBBuildCommandSequence;
  ProgressBar.Position := Sequence.ExecutedCount;
  StatusLabel.Caption := Format('Executed %d of %d commands',
    [Sequence.ExecutedCount, Sequence.Count]);
  Application.ProcessMessages;
end;

procedure TDatabaseEvolutionForm.EvolveDatabase(Connector: TInstantConnector;
  Model: TInstantModel);
var
  Evolver: TInstantDBEvolver;
begin
  Evolver := TInstantDBEvolver.Create(nil);
  try
    Evolver.Connector := Connector;
    Evolver.TargetModel := Model;
    Evolver.BuildCommandSequence;

    if Evolver.CommandSequence.Count > 0 then
    begin
      ProgressBar.Max := Evolver.CommandSequence.Count;
      ProgressBar.Position := 0;

      // Attach event handler
      Evolver.CommandSequence.OnCommandExecuted := CommandExecuted;

      try
        Evolver.CommandSequence.Execute;
        ShowMessage('Database evolved successfully');
      except
        on E: Exception do
        begin
          ShowMessage('Evolution failed: ' + E.Message + sLineBreak +
                     Format('Executed %d of %d commands',
                       [Evolver.CommandSequence.ExecutedCount,
                        Evolver.CommandSequence.Count]));
          raise;
        end;
      end;
    end;
  finally
    Evolver.Free;
  end;
end;

Evolution with Error Handling

Robust error handling with rollback support:

pascal
procedure SafeEvolveDatabase(Connector: TInstantConnector; Model: TInstantModel);
var
  Evolver: TInstantDBEvolver;
  Script: TStringList;
begin
  // Verify connection
  if not Connector.Connected then
    raise Exception.Create('Connector must be connected');

  Evolver := TInstantDBEvolver.Create(nil);
  Script := TStringList.Create;
  try
    Evolver.Connector := Connector;
    Evolver.TargetModel := Model;

    // Build command sequence
    Evolver.BuildCommandSequence;

    if Evolver.CommandSequence.Count = 0 then
    begin
      ShowMessage('Database schema is already up to date');
      Exit;
    end;

    // Save script for audit trail
    Evolver.CommandSequence.GenerateScript(Script);
    Script.SaveToFile('evolution_' + FormatDateTime('yyyymmdd_hhnnss', Now) + '.sql');

    // Start transaction
    Connector.StartTransaction;
    try
      // Execute evolution
      Evolver.CommandSequence.Execute;

      // Commit if successful
      Connector.CommitTransaction;
      ShowMessage('Database evolved successfully');
    except
      on E: Exception do
      begin
        // Rollback on error
        Connector.RollbackTransaction;

        ShowMessage(Format('Evolution failed at command %d of %d: %s',
          [Evolver.CommandSequence.ExecutedCount + 1,
           Evolver.CommandSequence.Count,
           E.Message]));
        raise;
      end;
    end;
  finally
    Script.Free;
    Evolver.Free;
  end;
end;

Model-Based Evolution

Evolve database when model changes:

pascal
procedure EvolveFromModelChange(Connector: TInstantConnector;
  ModelFileName: string);
var
  Evolver: TInstantDBEvolver;
  Model: TInstantModel;
begin
  Model := TInstantModel.Create(nil);
  try
    // Load new model
    Model.LoadFromFile(ModelFileName);

    Evolver := TInstantDBEvolver.Create(nil);
    try
      Evolver.Connector := Connector;
      Evolver.TargetModel := Model;
      Evolver.BuildCommandSequence;

      if Evolver.CommandSequence.Count > 0 then
      begin
        WriteLn(Format('Evolution requires %d commands:',
          [Evolver.CommandSequence.Count]));
        WriteLn('- Add Tables: ', Evolver.CommandSequence.AddTableCommandCount);
        WriteLn('- Drop Tables: ', Evolver.CommandSequence.DropTableCommandCount);
        WriteLn('- Add Fields: ', Evolver.CommandSequence.AddFieldCommandCount);
        WriteLn('- Drop Fields: ', Evolver.CommandSequence.DropFieldCommandCount);
        WriteLn('- Alter Fields: ', Evolver.CommandSequence.AlterFieldCommandCount);
        WriteLn('- Add Indexes: ', Evolver.CommandSequence.AddIndexCommandCount);
        WriteLn('- Drop Indexes: ', Evolver.CommandSequence.DropIndexCommandCount);

        Write('Proceed? (Y/N): ');
        if UpCase(ReadKey) = 'Y' then
        begin
          Evolver.CommandSequence.Execute;
          WriteLn('Evolution completed successfully');
        end;
      end;
    finally
      Evolver.Free;
    end;
  finally
    Model.Free;
  end;
end;

Evolution with Backup Verification

pascal
procedure EvolveWithBackupCheck(Connector: TInstantConnector;
  Model: TInstantModel; const BackupPath: string);
begin
  // Verify backup exists and is recent
  if not FileExists(BackupPath) then
    raise Exception.Create('Backup file not found: ' + BackupPath);

  if Now - FileDateToDateTime(FileAge(BackupPath)) > 1.0 then  // 1 day
    if MessageDlg('Backup is more than 1 day old. Continue?',
                  mtWarning, [mbYes, mbNo], 0) <> mrYes then
      Exit;

  // Proceed with evolution
  EvolveDatabase(Connector, Model);
end;

Metadata Comparison

Field Metadata Comparison

The evolver uses TInstantFieldMetadata.Equals() to detect changes:

pascal
// Compared properties:
- Name: Field name
- DataType: Data type (dtString, dtInteger, etc.)
- Size: Field size
- Required: Whether field is NOT NULL
- AlternateDataTypes: Alternative data types for compatibility

Field changes that trigger ALTER FIELD:

  • Data type change (e.g., String → Memo)
  • Size change (e.g., VARCHAR(50) → VARCHAR(100))
  • Required constraint change (NULL → NOT NULL or vice versa)

Index Metadata Comparison

The evolver uses TInstantIndexMetadata.Equals() to detect changes:

pascal
// Compared properties:
- Name: Index name
- Fields: Indexed field names (semicolon-separated)
- Options: Index options (ixUnique, ixDescending, etc.)

Index changes that trigger ALTER INDEX:

  • Field list change
  • Uniqueness change
  • Sort order change

Note: Primary key indexes (ixPrimary) are skipped during evolution.

Catalog Features

Required catalog features for schema evolution:

FeatureDescriptionRequired For
cfReadTableInfoRead table metadataTable comparison
cfReadColumnInfoRead column metadataField comparison
cfReadIndexInfoRead index metadataIndex comparison

Check feature support:

pascal
if cfReadTableInfo in Connector.Broker.Catalog.Features then
  // Table evolution supported

Limitations and Considerations

Case Sensitivity

The current implementation uses AnsiUpperCase() for object name comparison, which only works for case-insensitive object names:

pascal
{ TODO : This only works for case-insensitive object names! }
SourceTableMetadata := CommandSequence.SourceScheme.FindTableMetadata(
  AnsiUpperCase(TargetTableMetadata.Name));

Workaround for case-sensitive databases:

  • Use consistent casing in all metadata
  • Verify catalog implementation handles case correctly

Primary Key Modifications

Primary key indexes are not modified during evolution:

pascal
if not (ixPrimary in TargetIndexMetadata.Options) then
  // Only process non-primary indexes

Reason: Modifying primary keys requires special handling due to referential integrity constraints.

Foreign Key Constraints

The current algorithm does not explicitly handle foreign key dependencies.

Planned enhancement (from source comments):

pascal
// When we have foreign keys, we'll have to sort the generated list
// according to references, or we could use a strategy of
// disabling/dropping all the constraints at the beginning and
// recreate them later.

Current workaround:

  • Drop and recreate foreign keys manually if needed
  • Use database-specific constraint management

Data Type Compatibility

Some data type changes may not be supported by all databases:

pascal
// Safe conversions:
VARCHAR(50) → VARCHAR(100)  // Increasing size
INTEGER → BIGINT           // Widening type

// Potentially unsafe conversions:
VARCHAR(100) → VARCHAR(50)  // Decreasing size (may truncate data)
VARCHAR → INTEGER          // Type change (may fail if data not numeric)
NOT NULL → NULL            // Safe
NULL → NOT NULL            // May fail if existing NULLs present

Best practice: Test evolution on a copy of the database first.

Comparison: Evolver vs Builder

AspectTInstantDBEvolverTInstantDBBuilder
Data PreservationPreserves all dataDestroys all data
OperationIncremental changesDrop and rebuild
SpeedSlower (comparison + execution)Faster (no comparison)
Use CaseProduction databasesDevelopment, initial deployment
RiskLow (data preserved)High (data loss)
Schema ReadingRequiredNot required
ComplexityHigher (diff algorithm)Lower (straightforward rebuild)
TransactionsRecommendedRecommended
Backup RequiredYes (best practice)Mandatory

Best Practices

1. Always Backup Before Evolution

pascal
// Backup database before evolution
BackupDatabase(Connector, 'backup_' + FormatDateTime('yyyymmdd_hhnnss', Now) + '.bak');

// Then evolve
EvolveDatabase(Connector, Model);

2. Preview Changes Before Execution

pascal
Evolver.BuildCommandSequence;
Evolver.CommandSequence.GenerateScript(PreviewScript);
// Review PreviewScript before executing
if UserApproves(PreviewScript.Text) then
  Evolver.CommandSequence.Execute;

3. Use Transactions for Atomicity

pascal
Connector.StartTransaction;
try
  Evolver.CommandSequence.Execute;
  Connector.CommitTransaction;
except
  Connector.RollbackTransaction;
  raise;
end;

4. Test on Development Database First

pascal
// 1. Test on dev database
EvolveDatabase(DevConnector, Model);
VerifyDataIntegrity(DevConnector);

// 2. If successful, apply to production
EvolveDatabase(ProdConnector, Model);

5. Save Evolution Scripts for Audit Trail

pascal
Evolver.BuildCommandSequence;
Evolver.CommandSequence.GenerateScript(Script);
Script.SaveToFile('evolution_' + DateTimeToStr(Now) + '.sql');
Evolver.CommandSequence.Execute;

6. Monitor Evolution Progress

pascal
// Attach event handlers for long-running evolutions
Evolver.CommandSequence.OnCommandExecuted := HandleCommandExecuted;
Evolver.CommandSequence.OnCommandError := HandleCommandError;

7. Verify Catalog Feature Support

pascal
if not (cfReadTableInfo in Connector.Broker.Catalog.Features) then
  raise Exception.Create('Broker does not support schema reading');

8. Handle Data Type Conversions Carefully

pascal
// For risky conversions, handle in separate steps:
// 1. Add new field with new type
// 2. Copy/convert data
// 3. Drop old field
// 4. Rename new field

Common Scenarios

Adding a New Field

Model change:

pascal
// In TCustomer class:
property Email: string;  // New field added

Generated evolution:

sql
ALTER TABLE Customer ADD Email VARCHAR(100)

Modifying Field Size

Model change:

pascal
// Changed from:
property Name: string index 50;
// To:
property Name: string index 100;

Generated evolution:

sql
ALTER TABLE Customer ALTER COLUMN Name VARCHAR(100)

Adding an Index

Model change:

pascal
// In model:
TCustomer.Email marked with ixUnique option

Generated evolution:

sql
CREATE UNIQUE INDEX IX_Customer_Email ON Customer (Email)

Removing a Field

Model change:

pascal
// Removed from TCustomer class:
// property OldField: string;

Generated evolution:

sql
ALTER TABLE Customer DROP COLUMN OldField

Adding a New Class (Table)

Model change:

pascal
// New class added:
TProduct = class(TInstantObject)
  property Name: string index 100;
  property Price: Currency;
end;

Generated evolution:

sql
CREATE TABLE Product (
  Id VARCHAR(32) NOT NULL PRIMARY KEY,
  Class VARCHAR(32),
  UpdateCount INTEGER,
  Name VARCHAR(100),
  Price DECIMAL(19,4)
)

Removing a Class (Table)

Model change:

pascal
// TObsoleteClass removed from model

Generated evolution:

sql
DROP TABLE ObsoleteClass

Troubleshooting

"Broker does not support schema reading"

Problem: Broker's catalog lacks required features.

Solution:

pascal
// Check catalog features:
with Connector.Broker.Catalog do
  if not (cfReadTableInfo in Features) then
    ShowMessage('Catalog does not support reading table metadata');

Ensure broker implementation supports catalog features.

Evolution Generates No Commands

Problem: Database already matches model, but you expected changes.

Diagnosis:

pascal
Evolver.BuildCommandSequence;
if Evolver.CommandSequence.Count = 0 then
begin
  // Compare manually
  SourceScheme := Connector.Broker.ReadDatabaseScheme(nil);
  TargetScheme := Connector.CreateScheme(Model);
  // Inspect differences
end;

Possible causes:

  • Model not loaded correctly
  • Metadata already synchronized
  • Catalog not reading schema correctly

Case-Sensitivity Issues

Problem: Tables/fields not found due to case differences.

Solution: Ensure consistent casing in:

  • Model metadata
  • Database object names
  • Catalog implementation

Data Loss During Field Type Change

Problem: Converting VARCHAR to INTEGER fails.

Solution:

pascal
// Manual migration:
// 1. Add new field
ALTER TABLE Customer ADD EmailNew VARCHAR(100)
// 2. Copy/convert data
UPDATE Customer SET EmailNew = Email WHERE Email IS NOT NULL
// 3. Drop old field
ALTER TABLE Customer DROP COLUMN Email
// 4. Rename (if needed via SQL or manually)

Transaction Isolation Issues

Problem: Evolution fails due to locked tables.

Solution:

pascal
// Ensure exclusive access during evolution:
// 1. Disconnect all users
// 2. Run evolution in maintenance window
// 3. Use appropriate transaction isolation level

Primary Key Changes Not Applied

Problem: Primary key modifications ignored.

Explanation: Primary keys are skipped during evolution to avoid referential integrity issues.

Solution: Modify primary keys manually with careful constraint management.

Events

Inherited from TInstantDBBuildCommandSequence:

EventDescription
OnCommandExecutedFired after each command executes
OnCommandErrorFired when a command fails

See InstantDBBuild for event details.

See Also

Source Code

File: InstantDBEvolution.pasLocation: Source/Core/

Summary

TInstantDBEvolver provides safe, incremental database schema evolution by:

  • Comparing current database schema with target business model
  • Generating only necessary changes
  • Preserving all existing data
  • Supporting rollback via transactions

When to use:

  • ✓ Production databases with valuable data
  • ✓ Incremental model updates
  • ✓ Safe schema migrations

When NOT to use:

  • ✗ Initial database creation (use TInstantDBBuilder)
  • ✗ Complete schema redesign (backup, drop, rebuild)
  • ✗ Development databases without data (TInstantDBBuilder faster)

Remember: Always backup before evolution, preview changes, test on development first, and use transactions for atomicity.

Released under Mozilla License, Version 2.0.