validate method

List<ValidationIssue> validate(
  1. SchemaDocument schema, {
  2. Map<String, Map<String, NativeTypeRule>> nativeTypeRulesByProvider = const <String, Map<String, NativeTypeRule>>{},
})

Returns validation issues for schema.

Implementation

List<ValidationIssue> validate(
  SchemaDocument schema, {
  Map<String, Map<String, NativeTypeRule>> nativeTypeRulesByProvider =
      const <String, Map<String, NativeTypeRule>>{},
}) {
  final effectiveSchema = schema.withoutIgnored();
  final issues = <ValidationIssue>[];
  final seenModels = <String>{};
  final seenEnums = <String>{};
  final seenDatasources = <String>{};
  final seenGenerators = <String>{};
  // Tracks effective database table names to catch @@map conflicts.
  final seenTableNames = <String>{};

  for (final datasource in schema.datasources) {
    if (!seenDatasources.add(datasource.name)) {
      issues.add(
        ValidationIssue(
          modelName: datasource.name,
          message: 'Duplicate datasource name.',
        ),
      );
    }
    if (!_hasNonEmptyProperty(datasource.properties, 'provider')) {
      issues.add(
        ValidationIssue(
          modelName: datasource.name,
          message: 'Datasource must declare a provider.',
        ),
      );
    }
    if (!_hasNonEmptyProperty(datasource.properties, 'url')) {
      issues.add(
        ValidationIssue(
          modelName: datasource.name,
          message: 'Datasource must declare a url.',
        ),
      );
    }
  }

  for (final generator in schema.generators) {
    if (!seenGenerators.add(generator.name)) {
      issues.add(
        ValidationIssue(
          modelName: generator.name,
          message: 'Duplicate generator name.',
        ),
      );
    }
    if (!_hasNonEmptyProperty(generator.properties, 'provider')) {
      issues.add(
        ValidationIssue(
          modelName: generator.name,
          message: 'Generator must declare a provider.',
        ),
      );
    }
  }

  for (final definition in schema.enums) {
    if (!seenEnums.add(definition.name)) {
      issues.add(
        ValidationIssue(
          modelName: definition.name,
          message: 'Duplicate enum name.',
        ),
      );
    }
    if (seenModels.contains(definition.name)) {
      issues.add(
        ValidationIssue(
          modelName: definition.name,
          message: 'Schema type name is already used by a model.',
        ),
      );
    }

    final seenValues = <String>{};
    for (final value in definition.values) {
      if (!seenValues.add(value)) {
        issues.add(
          ValidationIssue(
            modelName: definition.name,
            fieldName: value,
            message: 'Duplicate enum value.',
          ),
        );
      }
    }

    for (final attribute in definition.attributes) {
      switch (attribute.name) {
        case 'map':
          final value = attribute.arguments['value'];
          if (value == null || value.isEmpty) {
            issues.add(
              ValidationIssue(
                modelName: definition.name,
                message: '@@map requires a mapped type name.',
              ),
            );
          }
        default:
          issues.add(
            ValidationIssue(
              modelName: definition.name,
              message: 'Unsupported enum attribute @@${attribute.name}.',
            ),
          );
      }
    }
  }

  for (final model in effectiveSchema.models) {
    if (!seenModels.add(model.name)) {
      issues.add(
        ValidationIssue(
          modelName: model.name,
          message: 'Duplicate model name.',
        ),
      );
    }
    if (!seenTableNames.add(model.databaseName)) {
      issues.add(
        ValidationIssue(
          modelName: model.name,
          message:
              'Database table name "${model.databaseName}" conflicts with another model.',
        ),
      );
    }
    if (seenEnums.contains(model.name)) {
      issues.add(
        ValidationIssue(
          modelName: model.name,
          message: 'Schema type name is already used by an enum.',
        ),
      );
    }

    final seenFields = <String>{};
    // Tracks effective database column names to catch @map conflicts.
    final seenDbFieldNames = <String>{};
    var idCount = 0;
    final modelId = model.attribute('id');

    if (modelId != null) {
      final idFields = _parseListArgument(
        modelId.arguments['fields'] ?? modelId.arguments['value'] ?? '',
      );
      if (idFields.isEmpty) {
        issues.add(
          ValidationIssue(
            modelName: model.name,
            message: '@@id requires at least one field.',
          ),
        );
      }
      _validateModelAttributeFields(
        model: model,
        attribute: modelId,
        issues: issues,
      );
    }

    for (final attribute in model.attributes) {
      switch (attribute.name) {
        case 'unique':
        case 'index':
          _validateModelAttributeFields(
            model: model,
            attribute: attribute,
            issues: issues,
          );
        case 'map':
          final value = attribute.arguments['value'];
          if (value == null || value.isEmpty) {
            issues.add(
              ValidationIssue(
                modelName: model.name,
                message: '@@map requires a mapped table name.',
              ),
            );
          }
        case 'ignore':
        case 'id':
          break;
        default:
          issues.add(
            ValidationIssue(
              modelName: model.name,
              message: 'Unsupported model attribute @@${attribute.name}.',
            ),
          );
      }
    }

    for (final field in model.fields) {
      if (!seenFields.add(field.name)) {
        issues.add(
          ValidationIssue(
            modelName: model.name,
            fieldName: field.name,
            message: 'Duplicate field name.',
          ),
        );
      }

      // Relation fields have no database column; skip the db-name check.
      final isColumnField =
          field.isScalar || effectiveSchema.findEnum(field.type) != null;
      if (isColumnField && !seenDbFieldNames.add(field.databaseName)) {
        issues.add(
          ValidationIssue(
            modelName: model.name,
            fieldName: field.name,
            message:
                'Field database name "${field.databaseName}" conflicts with another field in model "${model.name}".',
          ),
        );
      }

      if (field.isId) {
        idCount++;
        if (field.isNullable || field.isList) {
          issues.add(
            ValidationIssue(
              modelName: model.name,
              fieldName: field.name,
              message: 'ID field must be singular and non-nullable.',
            ),
          );
        }
      }

      if (!field.isScalar &&
          effectiveSchema.findEnum(field.type) == null &&
          effectiveSchema.findModel(field.type) == null) {
        issues.add(
          ValidationIssue(
            modelName: model.name,
            fieldName: field.name,
            message: 'Unknown relation target model "${field.type}".',
          ),
        );
      }

      final relation = field.attribute('relation');
      if (relation != null) {
        _validateRelationArguments(
          effectiveSchema,
          model,
          field,
          relation,
          issues,
        );
      }

      final updatedAt = field.attribute('updatedAt');
      if (updatedAt != null) {
        if (updatedAt.arguments.isNotEmpty) {
          issues.add(
            ValidationIssue(
              modelName: model.name,
              fieldName: field.name,
              message: '@updatedAt does not accept arguments.',
            ),
          );
        }
        if (field.type != 'DateTime' || field.isList || field.isNullable) {
          issues.add(
            ValidationIssue(
              modelName: model.name,
              fieldName: field.name,
              message:
                  '@updatedAt field must be a singular non-nullable DateTime.',
            ),
          );
        }
      }

      final nativeType = field.nativeTypeAttribute;
      if (nativeType != null) {
        final provider = _resolveNativeTypeProvider(schema);
        final issue = _validateNativeType(
          provider: provider,
          nativeTypeRulesByProvider: nativeTypeRulesByProvider,
          schema: schema,
          model: model,
          field: field,
          nativeType: nativeType,
        );
        if (issue != null) {
          issues.add(issue);
        }
      }
    }

    if (idCount == 0 && modelId == null) {
      issues.add(
        ValidationIssue(
          modelName: model.name,
          message: 'Model must have an @id field.',
        ),
      );
    }
    if (idCount > 1) {
      issues.add(
        ValidationIssue(
          modelName: model.name,
          message:
              'Use model-level @@id([fieldA, fieldB]) instead of multiple @id fields.',
        ),
      );
    }
    if (idCount > 0 && modelId != null) {
      issues.add(
        ValidationIssue(
          modelName: model.name,
          message:
              'Use either field-level @id or model-level @@id, not both.',
        ),
      );
    }
  }

  _validateRelationTopology(effectiveSchema, issues);

  return issues
      .map((issue) => _enrichIssue(schema, issue))
      .toList(growable: false);
}