Skip to content

Commit

Permalink
feat(options): allow column nullability to be ignored
Browse files Browse the repository at this point in the history
  • Loading branch information
jayharris committed Apr 14, 2020
1 parent 852dc63 commit f00a881
Show file tree
Hide file tree
Showing 13 changed files with 457 additions and 9 deletions.
18 changes: 16 additions & 2 deletions src/entityframeworkcore/README.md
Expand Up @@ -69,9 +69,23 @@ Default: `true`

Set to `false` to skip validation of foreign keys. Useful for platforms that do no use foreign keys.

### ValidateNullabilityForTables

## License
Type: `boolean`<br>
Default: `true`

Cobweb is copyright of Arana Software, released under the [BSD License](http://opensource.org/licenses/BSD-3-Clause).
Set to `false` to skip validation of nullability on table columns.

### ValidateNullabilityForViews

Type: `boolean`<br>
Default: `false`

Set to `false` to skip validation of nullability on view columns. By default, many database platforms enable nullability on view columns regardless of nullability on the underlying table column.

*This option is not applicable to Entity Framework Core 2.x or Aranasoft.Cobweb.EntityFrameworkCore.Validation 1.2x.x.*


## License

Cobweb is copyright of Arana Software, released under the [BSD License](http://opensource.org/licenses/BSD-3-Clause).
Expand Up @@ -12,5 +12,10 @@ public class SchemaValidationOptions {
/// Validate foreign keys configured within the model against the connected database
/// </summary>
public bool ValidateForeignKeys { get; set; } = true;

/// <summary>
/// Validate nullability on table columns
/// </summary>
public bool ValidateNullabilityForTables { get; set; } = true;
}
}
Expand Up @@ -35,7 +35,7 @@ public class SchemaValidator {
continue;
}

validationErrors.AddRange(ValidateColumns(databaseModel, persistedType));
validationErrors.AddRange(ValidateColumns(databaseModel, persistedType, validationOptions));

if (validationOptions.ValidateIndexes) {
validationErrors.AddRange(ValidateIndexes(databaseModel, persistedType));
Expand All @@ -51,7 +51,7 @@ public class SchemaValidator {
}
}

private List<string> ValidateColumns(DatabaseModel databaseModel, IEntityType persistedType) {
private List<string> ValidateColumns(DatabaseModel databaseModel, IEntityType persistedType, SchemaValidationOptions validationOptions) {
var entityTable = persistedType.Relational();
var valErrors = new List<string>();
foreach (var entityProperty in persistedType.GetProperties()) {
Expand All @@ -70,7 +70,8 @@ public class SchemaValidator {
$"Column type mismatch in {entityTable.TableName} for column {entityColumn.ColumnName}. Found: {dbColumn.StoreType.ToLowerInvariant()}, Expected {entityColumn.ColumnType.ToLowerInvariant()}");
}

if (entityProperty.IsNullable != dbColumn.IsNullable) {
var shouldValidateColumnNullability = validationOptions.ValidateNullabilityForTables;
if (shouldValidateColumnNullability && entityProperty.IsNullable != dbColumn.IsNullable) {
valErrors.Add(
$"Column nullability mismatch in {entityTable.TableName} for column {entityColumn.ColumnName}. Found: {(dbColumn.IsNullable ? "Nullable" : "NotNullable")}, Expected {(entityProperty.IsNullable ? "Nullable" : "NotNullable")}");
}
Expand Down
Expand Up @@ -14,9 +14,14 @@ public class SchemaValidationOptions {
public bool ValidateForeignKeys { get; set; } = true;

/// <summary>
/// Ignore Nullability mismatches on view columns
/// Validate nullability on table columns
/// </summary>
public bool ValidateNullabilityForTables { get; set; } = true;

/// <summary>
/// Validate nullability on view columns
/// </summary>
/// <remarks>Some database systems enable nullability on view columns regardless of nullability on the underlying table column</remarks>
public bool IgnoreNullabilityForViews { get; set; } = true;
public bool ValidateNullabilityForViews { get; set; } = false;
}
}
Expand Up @@ -67,7 +67,8 @@ public class SchemaValidator {
$"Column type mismatch in {persistedType.GetTableName()} for column {persistedColumn.GetColumnName()}. Found: {dbColumn.StoreType.ToLowerInvariant()}, Expected {persistedColumn.GetColumnType().ToLowerInvariant()}");
}

var shouldValidateColumnNullability = !validationOptions.IgnoreNullabilityForViews && persistedType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) == null;
var isViewType = persistedType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) != null;
var shouldValidateColumnNullability = (validationOptions.ValidateNullabilityForTables && !isViewType) || (validationOptions.ValidateNullabilityForViews && isViewType);
if (shouldValidateColumnNullability && persistedColumn.IsNullable != dbColumn.IsNullable) {
valErrors.Add(
$"Column nullability mismatch in {persistedType.GetTableName()} for column {persistedColumn.GetColumnName()}. Found: {(dbColumn.IsNullable ? "Nullable" : "NotNullable")}, Expected {(persistedColumn.IsNullable ? "Nullable" : "NotNullable")}");
Expand Down
@@ -0,0 +1,96 @@
using System;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.Migrations;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.SqlServer;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.XUnit;
using FluentAssertions;
using Xunit;

namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.SqlServer {
[OperatingSystemRequirement(OperatingSystems.Windows)]
public class WhenValidatingSchemaGivenIncorrectColumnNullability : IClassFixture<SqlServerMigrationsFixture<MigrationsWithIncorrectColumnNullability>> {
private readonly SqlServerMigrationsFixture<MigrationsWithIncorrectColumnNullability> _fixture;

public WhenValidatingSchemaGivenIncorrectColumnNullability( SqlServerMigrationsFixture<MigrationsWithIncorrectColumnNullability> fixture) {
_fixture = fixture;
}

[ConditionalFact]
public void ItShouldThrowValidationException() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should().ThrowExactly<SchemaValidationException>();
}

[ConditionalFact]
public void ItShouldHaveValidationErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotBeEmpty();
}

[ConditionalFact]
public void ItShouldNotHaveMissingTableErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing Table", StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveMissingViewErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing View", StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveMissingColumnErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing Column", StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveColumnTypeMismatchErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Column type mismatch", StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldOnlyHaveColumnNullabilityMismatchErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().OnlyContain(error => error.StartsWith("Column nullability mismatch", StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveMissingIndexErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing Index", StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveMissingForeignKeyErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing Foreign Key", StringComparison.InvariantCultureIgnoreCase));
}
}
}
Expand Up @@ -66,6 +66,15 @@ public class WhenValidatingSchemaGivenIncorrectColumnTypes : IClassFixture<SqlSe
.Should().OnlyContain(error => error.StartsWith("Column type mismatch", StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveColumnNullabilityMismatchErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Column nullability mismatch", StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveMissingIndexErrors() {
var applicationDbContext = _fixture.GetContext();
Expand Down
@@ -0,0 +1,24 @@
using System;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.Migrations;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.SqlServer;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.XUnit;
using FluentAssertions;
using Xunit;

namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.SqlServer {
[OperatingSystemRequirement(OperatingSystems.Windows)]
public class WhenValidatingSchemaIgnoringIndexesGivenIncorrectColumnNullability : IClassFixture<SqlServerMigrationsFixture<MigrationsWithIncorrectColumnNullability>> {
private readonly SqlServerMigrationsFixture<MigrationsWithIncorrectColumnNullability> _fixture;

public WhenValidatingSchemaIgnoringIndexesGivenIncorrectColumnNullability(SqlServerMigrationsFixture<MigrationsWithIncorrectColumnNullability> fixture) {
_fixture = fixture;
}

[ConditionalFact]
public void ItShouldNotThrowValidationExceptionWhenIgnoringIndexes() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema(new SchemaValidationOptions{ValidateNullabilityForTables = false});
validatingSchema.Should().NotThrow();
}
}
}
@@ -0,0 +1,94 @@
using System;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.Migrations;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.Sqlite;
using FluentAssertions;
using Xunit;

namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Sqlite {
public class WhenValidatingSchemaGivenIncorrectColumnNullability : IClassFixture<SqliteMigrationsFixture<MigrationsWithIncorrectColumnNullability>> {
private readonly SqliteMigrationsFixture<MigrationsWithIncorrectColumnNullability> _fixture;

public WhenValidatingSchemaGivenIncorrectColumnNullability(SqliteMigrationsFixture<MigrationsWithIncorrectColumnNullability> fixture) {
_fixture = fixture;
}

[Fact]
public void ItShouldThrowValidationException() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().ThrowExactly<SchemaValidationException>();
}

[Fact]
public void ItShouldHaveValidationErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotBeEmpty();
}

[Fact]
public void ItShouldNotHaveMissingTableErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing Table", StringComparison.InvariantCultureIgnoreCase));
}

[Fact]
public void ItShouldNotHaveMissingViewErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing View", StringComparison.InvariantCultureIgnoreCase));
}

[Fact]
public void ItShouldNotHaveMissingColumnErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing Column", StringComparison.InvariantCultureIgnoreCase));
}

[Fact]
public void ItShouldNotHaveColumnTypeMismatchErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Column type mismatch", StringComparison.InvariantCultureIgnoreCase));
}

[Fact]
public void ItShouldOnlyHaveColumnNullabilityMismatchErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().OnlyContain(error => error.StartsWith("Column nullability mismatch", StringComparison.InvariantCultureIgnoreCase));
}

[Fact]
public void ItShouldNotHaveMissingIndexErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing Index", StringComparison.InvariantCultureIgnoreCase));
}

[Fact]
public void ItShouldNotHaveMissingForeignKeyErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Missing Foreign Key", StringComparison.InvariantCultureIgnoreCase));
}
}
}
Expand Up @@ -64,6 +64,15 @@ public class WhenValidatingSchemaGivenIncorrectColumnTypes : IClassFixture<Sqlit
.Should().OnlyContain(error => error.StartsWith("Column type mismatch", StringComparison.InvariantCultureIgnoreCase));
}

[Fact]
public void ItShouldNotHaveColumnNullabilityMismatchErrors() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false});
validatingSchema.Should().Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should().NotContain(error => error.StartsWith("Column nullability mismatch", StringComparison.InvariantCultureIgnoreCase));
}

[Fact]
public void ItShouldNotHaveMissingIndexErrors() {
var context = _fixture.GetContext();
Expand Down
@@ -0,0 +1,23 @@
using System;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.Migrations;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.Sqlite;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;

namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Sqlite {
public class WhenValidatingSchemaIgnoringIndexesGivenIncorrectColumnNullability : IClassFixture<SqliteMigrationsFixture<MigrationsWithIncorrectColumnNullability>> {
private readonly SqliteMigrationsFixture<MigrationsWithIncorrectColumnNullability> _fixture;

public WhenValidatingSchemaIgnoringIndexesGivenIncorrectColumnNullability(SqliteMigrationsFixture<MigrationsWithIncorrectColumnNullability> fixture) {
_fixture = fixture;
}

[Fact]
public void ItShouldNotThrowValidationException() {
var context = _fixture.GetContext();
Action validatingSchema = () => context.ValidateSchema(new SchemaValidationOptions {ValidateForeignKeys = false, ValidateNullabilityForTables = false});
validatingSchema.Should().NotThrow();
}
}
}

0 comments on commit f00a881

Please sign in to comment.