detectPotentialDataLossWarnings function

List<String> detectPotentialDataLossWarnings({
  1. required SchemaDocument from,
  2. required SchemaDocument to,
})

Returns human-readable warnings for schema transitions that may lose data.

Implementation

List<String> detectPotentialDataLossWarnings({
  required SchemaDocument from,
  required SchemaDocument to,
}) {
  from = from.withoutIgnored();
  to = to.withoutIgnored();
  final warnings = <String>[];
  final matchedSourceEnumNames = <String>{};
  final matchedEnumPairs = <String>{};

  final sourceEnumsByName = {
    for (final definition in from.enums) definition.name: definition,
  };
  final sourceEnumsByDatabaseName = {
    for (final definition in from.enums) definition.databaseName: definition,
  };
  for (final targetEnum in to.enums) {
    final sourceEnum =
        sourceEnumsByName.remove(targetEnum.name) ??
        sourceEnumsByDatabaseName.remove(targetEnum.databaseName) ??
        _inferRenamedSourceEnum(
          sourceSchema: from,
          targetSchema: to,
          targetEnum: targetEnum,
          matchedSourceEnumNames: matchedSourceEnumNames,
        );
    if (sourceEnum == null) {
      continue;
    }
    matchedSourceEnumNames.add(sourceEnum.name);
    matchedEnumPairs.add(_enumPairKey(sourceEnum, targetEnum));
    sourceEnumsByName.remove(sourceEnum.name);
    sourceEnumsByDatabaseName.remove(sourceEnum.databaseName);
    final removedValues = sourceEnum.values
        .where((value) => !targetEnum.values.contains(value))
        .toList(growable: false);
    if (removedValues.isNotEmpty &&
        !_isSafeEnumTransition(sourceEnum, targetEnum)) {
      warnings.add(
        'Potential data loss: enum ${targetEnum.name} removes values ${removedValues.join(', ')}.',
      );
    }
  }
  for (final removedEnum in sourceEnumsByName.keys) {
    warnings.add(
      'Potential data loss: enum $removedEnum is removed from the target schema.',
    );
  }

  final sourceModelsByName = {
    for (final model in from.models) model.name: model,
  };
  final sourceModelsByDatabaseName = {
    for (final model in from.models) model.databaseName: model,
  };
  final targetModels = {for (final model in to.models) model.name: model};

  for (final entry in targetModels.entries) {
    final targetModel = entry.value;
    final sourceModel =
        sourceModelsByName.remove(entry.key) ??
        sourceModelsByDatabaseName.remove(targetModel.databaseName);
    if (sourceModel == null) {
      continue;
    }
    sourceModelsByName.remove(sourceModel.name);
    sourceModelsByDatabaseName.remove(sourceModel.databaseName);

    final sourceFieldsByName = {
      for (final field in sourceModel.fields) field.name: field,
    };
    final sourceFieldsByDatabaseName = {
      for (final field in sourceModel.fields) field.databaseName: field,
    };
    for (final targetField in targetModel.fields) {
      final sourceField =
          sourceFieldsByName.remove(targetField.name) ??
          sourceFieldsByDatabaseName.remove(targetField.databaseName);
      if (sourceField == null) {
        continue;
      }
      sourceFieldsByName.remove(sourceField.name);
      sourceFieldsByDatabaseName.remove(sourceField.databaseName);
      final warning = _fieldRiskWarning(
        modelName: targetModel.name,
        sourceField: sourceField,
        targetField: targetField,
        sourceSchema: from,
        targetSchema: to,
        matchedEnumPairs: matchedEnumPairs,
      );
      if (warning != null) {
        warnings.add(warning);
      }
    }

    for (final removedField in sourceModel.fields.where(
      (field) =>
          sourceFieldsByName.containsKey(field.name) ||
          sourceFieldsByDatabaseName.containsKey(field.databaseName),
    )) {
      if (_isRelationField(from, removedField)) {
        continue;
      }
      warnings.add(
        'Potential data loss: field ${sourceModel.name}.${removedField.name} is removed from the target schema.',
      );
    }
  }

  for (final removedModel in from.models.where(
    (model) =>
        sourceModelsByName.containsKey(model.name) ||
        sourceModelsByDatabaseName.containsKey(model.databaseName),
  )) {
    warnings.add(
      'Potential data loss: model ${removedModel.name} is removed from the target schema.',
    );
  }

  final sourceStorages = {
    for (final storage in collectImplicitManyToManyStorages(from))
      storage.tableName: storage,
  };
  final targetStorages = {
    for (final storage in collectImplicitManyToManyStorages(to))
      storage.tableName: storage,
  };

  for (final removedStorage in sourceStorages.keys.where(
    (name) => !targetStorages.containsKey(name),
  )) {
    warnings.add(
      'Potential data loss: implicit relation storage $removedStorage is removed from the target schema.',
    );
  }

  return List<String>.unmodifiable(_dedupe(warnings));
}