plan method

PostgresqlMigrationPlan plan({
  1. required SchemaDocument from,
  2. required SchemaDocument to,
})

Builds a plan from two schema snapshots.

Implementation

PostgresqlMigrationPlan plan({
  required SchemaDocument from,
  required SchemaDocument to,
}) {
  from = from.withoutIgnored();
  to = to.withoutIgnored();
  final statements = <String>[];
  final warnings = <String>[];
  var requiresRebuild = false;
  final matchedSourceEnumNames = <String>{};
  final matchedEnumPairs = <String>{};

  final fromEnumsByName = {
    for (final definition in from.enums) definition.name: definition,
  };
  final fromEnumsByDatabaseName = {
    for (final definition in from.enums) definition.databaseName: definition,
  };
  for (final targetEnum in to.enums) {
    final sourceEnum =
        fromEnumsByName.remove(targetEnum.name) ??
        fromEnumsByDatabaseName.remove(targetEnum.databaseName) ??
        _inferRenamedSourceEnum(
          sourceSchema: from,
          targetSchema: to,
          targetEnum: targetEnum,
          matchedSourceEnumNames: matchedSourceEnumNames,
        );
    if (sourceEnum == null) {
      statements.add(schemaApplier.createEnumStatement(targetEnum));
      continue;
    }

    matchedSourceEnumNames.add(sourceEnum.name);
    matchedEnumPairs.add(_enumPairKey(sourceEnum, targetEnum));
    fromEnumsByName.remove(sourceEnum.name);
    fromEnumsByDatabaseName.remove(sourceEnum.databaseName);

    // Handle DB-level type rename when @@map changes.
    if (sourceEnum.databaseName != targetEnum.databaseName) {
      final escapedOld = sourceEnum.databaseName.replaceAll('"', '""');
      final escapedNew = targetEnum.databaseName.replaceAll('"', '""');
      statements.add('ALTER TYPE "$escapedOld" RENAME TO "$escapedNew"');
    }

    if (_hasCompatibleEnumDefinition(sourceEnum, targetEnum)) {
      continue;
    }

    final renamedValues = _renamedEnumValues(sourceEnum, targetEnum);
    if (renamedValues != null) {
      for (final rename in renamedValues) {
        statements.add(
          _createRenameEnumValueStatement(
            targetEnum.databaseName,
            rename.$1,
            rename.$2,
          ),
        );
      }
      continue;
    }

    final appendedValues = _appendedEnumValues(sourceEnum, targetEnum);
    if (appendedValues != null) {
      for (final statement in appendedValues) {
        statements.add(statement);
      }
      continue;
    }

    final combinedValues = _combinedRenameInsertStatements(
      sourceEnum,
      targetEnum,
    );
    if (combinedValues != null) {
      for (final statement in combinedValues) {
        statements.add(statement);
      }
      continue;
    }

    requiresRebuild = true;
    warnings.add(
      'Altering enum ${targetEnum.databaseName} requires schema rebuild and data compatibility review.',
    );
  }

  for (final removedEnum in fromEnumsByName.values) {
    requiresRebuild = true;
    warnings.add(
      'Dropping enum ${removedEnum.databaseName} requires schema rebuild and data compatibility review.',
    );
  }

  final fromModelsByName = {
    for (final model in from.models) model.name: model,
  };
  final fromModelsByDatabaseName = {
    for (final model in from.models) model.databaseName: model,
  };

  for (final targetModel in to.models) {
    final sourceModel =
        fromModelsByName.remove(targetModel.name) ??
        fromModelsByDatabaseName.remove(targetModel.databaseName);
    if (sourceModel == null) {
      statements.add(
        schemaApplier.createTableStatementForModel(targetModel, schema: to),
      );
      continue;
    }
    fromModelsByName.remove(sourceModel.name);
    fromModelsByDatabaseName.remove(sourceModel.databaseName);

    _planModelDiff(
      sourceSchema: from,
      targetSchema: to,
      sourceModel: sourceModel,
      targetModel: targetModel,
      matchedEnumPairs: matchedEnumPairs,
      statements: statements,
      warnings: warnings,
    );
  }

  for (final removedModel in from.models.where(
    (model) =>
        fromModelsByName.containsKey(model.name) ||
        fromModelsByDatabaseName.containsKey(model.databaseName),
  )) {
    warnings.add(
      'Dropping model ${removedModel.name} requires manual migration.',
    );
  }

  _planImplicitManyToManyDiffs(
    from: from,
    to: to,
    statements: statements,
    warnings: warnings,
  );

  return PostgresqlMigrationPlan(
    statements: List<String>.unmodifiable(statements),
    warnings: List<String>.unmodifiable(warnings),
    requiresRebuild: requiresRebuild,
  );
}