Skip to content

WIP Fix validating similar but not equal column types #2997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public void SchemaExport_Validate_CausesValidateExceptionAsync()
Assert.That(
() => validator.ValidateAsync(),
Throws.TypeOf<SchemaValidationException>()
.And.Message.EqualTo("Schema validation failed: see list of validation errors")
.And.Message.StartsWith("Schema validation failed: see list of validation errors")
.And.Property("ValidationErrors").Contains("Missing table: Home_Validate"));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by AsyncGenerator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------


using System;
using NHibernate.Cfg;
using NHibernate.Dialect;
using NHibernate.Mapping.ByCode;
using NHibernate.Tool.hbm2ddl;
using NUnit.Framework;

namespace NHibernate.Test.Tools.hbm2ddl.SchemaValidator
{
using System.Threading.Tasks;
[TestFixture]
public class PostgresSchemaValidateFixtureAsync
{
[Test]
public async Task ShouldBeInvalidIfTimestampTzIsExpectedAndGotTimestampAsync()
{
var actual = BuildConfiguration("timestamp");
Assume.That(Dialect.Dialect.GetDialect(actual.Properties) is PostgreSQLDialect);

var expected = BuildConfiguration("timestamptz");

var export = new SchemaExport(actual);
await (export.CreateAsync(true, true));

try
{
var validator = new Tool.hbm2ddl.SchemaValidator(expected);

var error = Assert.ThrowsAsync<SchemaValidationException>(() => validator.ValidateAsync());
Assert.That(error, Has.Message.StartsWith("Schema validation failed: see list of validation errors"));
Assert.That(
error,
Has.Property("ValidationErrors").Some.Contains("Wrong column type").IgnoreCase
.And.Contains("for column CreatedAt. Found: timestamp, Expected timestamptz").IgnoreCase);
}
finally
{
await (export.DropAsync(true, true));
}
}

[Test]
public async Task ShouldBeInvalidIfTimestampIsExpectedAndGotTimestampTzAsync()
{
var actual = BuildConfiguration("timestamptz");
Assume.That(Dialect.Dialect.GetDialect(actual.Properties) is PostgreSQLDialect);

var expected = BuildConfiguration("timestamp");

var export = new SchemaExport(actual);
await (export.CreateAsync(true, true));

try
{
var validator = new Tool.hbm2ddl.SchemaValidator(expected);

var error = Assert.ThrowsAsync<SchemaValidationException>(() => validator.ValidateAsync());
Assert.That(error, Has.Message.StartsWith("Schema validation failed: see list of validation errors"));
Assert.That(
error,
Has.Property("ValidationErrors").Some.Contains("Wrong column type").IgnoreCase
.And.Contains("for column CreatedAt. Found: timestamptz, Expected timestamp").IgnoreCase);
}
finally
{
await (export.DropAsync(true, true));
}
}

private static Configuration BuildConfiguration(string type)
{
var mapper = new ModelMapper();
mapper.Class<Entity>(c =>
{
c.Table("Entity");
c.Id(x => x.Id);
c.Property(x => x.CreatedAt, p => p.Column(cm => cm.SqlType(type)));
});

var cfg = TestConfigurationHelper.GetDefaultConfiguration();
cfg.AddDeserializedMapping(mapper.CompileMappingForAllExplicitlyAddedEntities(), "Entity");
return cfg;
}

public class Entity
{
public virtual int Id { get; set; }
public virtual DateTime CreatedAt { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public async Task ShouldVerifySameTableAsync()
await (validator.ValidateAsync());
}

[Test, SetCulture("tr-TR"), SetUICulture("tr-TR"), NetFxOnly]
[Test, SetCulture("tr-TR"), SetUICulture("tr-TR")]
public async Task ShouldVerifySameTableTurkishAsync()
{
//NH-3063
Expand Down Expand Up @@ -90,7 +90,7 @@ public void ShouldNotVerifyModifiedTableAsync()
Assert.That(
() => validatorV2.ValidateAsync(),
Throws.TypeOf<SchemaValidationException>()
.And.Message.EqualTo("Schema validation failed: see list of validation errors")
.And.Message.StartsWith("Schema validation failed: see list of validation errors")
.And.Property("ValidationErrors").Some.Contains("Missing column: Name in ").IgnoreCase.And.Contains("Version").IgnoreCase);
}

Expand All @@ -103,7 +103,7 @@ public void ShouldNotVerifyMultiModifiedTableAsync()

var error = Assert.ThrowsAsync<SchemaValidationException>(() => validator.ValidateAsync());
Assert.That(error,
Has.Message.EqualTo("Schema validation failed: see list of validation errors")
Has.Message.StartsWith("Schema validation failed: see list of validation errors")
.And.Property("ValidationErrors").Some.Contains("Missing column: Name in ").IgnoreCase.And.Contains("Version").IgnoreCase);
Assert.That(error,
Has.Property("ValidationErrors").Some.Contains("Missing column: Title in ").IgnoreCase.And.Contains("Version").IgnoreCase);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void SchemaExport_Validate_CausesValidateException()
Assert.That(
() => validator.Validate(),
Throws.TypeOf<SchemaValidationException>()
.And.Message.EqualTo("Schema validation failed: see list of validation errors")
.And.Message.StartsWith("Schema validation failed: see list of validation errors")
.And.Property("ValidationErrors").Contains("Missing table: Home_Validate"));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using NHibernate.Cfg;
using NHibernate.Dialect;
using NHibernate.Mapping.ByCode;
using NHibernate.Tool.hbm2ddl;
using NUnit.Framework;

namespace NHibernate.Test.Tools.hbm2ddl.SchemaValidator
{
[TestFixture]
public class PostgresSchemaValidateFixture
{
[Test]
public void ShouldBeInvalidIfTimestampTzIsExpectedAndGotTimestamp()
{
var actual = BuildConfiguration("timestamp");
Assume.That(Dialect.Dialect.GetDialect(actual.Properties) is PostgreSQLDialect);

var expected = BuildConfiguration("timestamptz");

var export = new SchemaExport(actual);
export.Create(true, true);

try
{
var validator = new Tool.hbm2ddl.SchemaValidator(expected);

var error = Assert.Throws<SchemaValidationException>(() => validator.Validate());
Assert.That(error, Has.Message.StartsWith("Schema validation failed: see list of validation errors"));
Assert.That(
error,
Has.Property("ValidationErrors").Some.Contains("Wrong column type").IgnoreCase
.And.Contains("for column CreatedAt. Found: timestamp, Expected timestamptz").IgnoreCase);
}
finally
{
export.Drop(true, true);
}
}

[Test]
public void ShouldBeInvalidIfTimestampIsExpectedAndGotTimestampTz()
{
var actual = BuildConfiguration("timestamptz");
Assume.That(Dialect.Dialect.GetDialect(actual.Properties) is PostgreSQLDialect);

var expected = BuildConfiguration("timestamp");

var export = new SchemaExport(actual);
export.Create(true, true);

try
{
var validator = new Tool.hbm2ddl.SchemaValidator(expected);

var error = Assert.Throws<SchemaValidationException>(() => validator.Validate());
Assert.That(error, Has.Message.StartsWith("Schema validation failed: see list of validation errors"));
Assert.That(
error,
Has.Property("ValidationErrors").Some.Contains("Wrong column type").IgnoreCase
.And.Contains("for column CreatedAt. Found: timestamptz, Expected timestamp").IgnoreCase);
}
finally
{
export.Drop(true, true);
}
}

private static Configuration BuildConfiguration(string type)
{
var mapper = new ModelMapper();
mapper.Class<Entity>(c =>
{
c.Table("Entity");
c.Id(x => x.Id);
c.Property(x => x.CreatedAt, p => p.Column(cm => cm.SqlType(type)));
});

var cfg = TestConfigurationHelper.GetDefaultConfiguration();
cfg.AddDeserializedMapping(mapper.CompileMappingForAllExplicitlyAddedEntities(), "Entity");
return cfg;
}

public class Entity
{
public virtual int Id { get; set; }
public virtual DateTime CreatedAt { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void ShouldVerifySameTable()
validator.Validate();
}

[Test, SetCulture("tr-TR"), SetUICulture("tr-TR"), NetFxOnly]
[Test, SetCulture("tr-TR"), SetUICulture("tr-TR")]
public void ShouldVerifySameTableTurkish()
{
//NH-3063
Expand Down Expand Up @@ -79,7 +79,7 @@ public void ShouldNotVerifyModifiedTable()
Assert.That(
() => validatorV2.Validate(),
Throws.TypeOf<SchemaValidationException>()
.And.Message.EqualTo("Schema validation failed: see list of validation errors")
.And.Message.StartsWith("Schema validation failed: see list of validation errors")
.And.Property("ValidationErrors").Some.Contains("Missing column: Name in ").IgnoreCase.And.Contains("Version").IgnoreCase);
}

Expand All @@ -92,7 +92,7 @@ public void ShouldNotVerifyMultiModifiedTable()

var error = Assert.Throws<SchemaValidationException>(() => validator.Validate());
Assert.That(error,
Has.Message.EqualTo("Schema validation failed: see list of validation errors")
Has.Message.StartsWith("Schema validation failed: see list of validation errors")
.And.Property("ValidationErrors").Some.Contains("Missing column: Name in ").IgnoreCase.And.Contains("Version").IgnoreCase);
Assert.That(error,
Has.Property("ValidationErrors").Some.Contains("Missing column: Title in ").IgnoreCase.And.Contains("Version").IgnoreCase);
Expand Down
11 changes: 9 additions & 2 deletions src/NHibernate/Mapping/Table.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NHibernate.Dialect.Schema;
using NHibernate.Engine;
using NHibernate.Util;
Expand Down Expand Up @@ -1035,7 +1036,13 @@ public IEnumerable<string> ValidateColumns(Dialect.Dialect dialect, IMapping map
}

//TODO: Add new method to ColumnMetadata :getTypeCode
bool typesMatch = column.GetSqlType(dialect, mapping).StartsWith(columnInfo.TypeName, StringComparison.OrdinalIgnoreCase);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not simply check that next char is not a letter (char.IsLetter)?

Copy link
Member Author

@hazzik hazzik Jan 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was/am thinking about that. Even have code about to push. But it is not that simple.

is not a letter

Oracle is pouncing with (n)varchar/(n)varchar2...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still see no reasons to use regex - check additionally for not a digit (char.IsDigit). Alphanumeric character check should suffice

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I hate the regex myself. But that is not that easy really. For example, Db2's CHAR(16) FOR BIT DATA would match to CHAR(16) even in my implementation with regex.

var columnType = column.GetSqlType(dialect, mapping);
var typesMatch =
string.Equals(columnType, columnInfo.TypeName, StringComparison.OrdinalIgnoreCase) ||
Regex.IsMatch(
columnType,
$@"^{Regex.Escape(columnInfo.TypeName)}\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
//|| columnInfo.get() == column.GetSqlTypeCode(mapping);
if (!typesMatch)
{
Expand All @@ -1045,7 +1052,7 @@ public IEnumerable<string> ValidateColumns(Dialect.Dialect dialect, IMapping map
dialect.Qualify(tableInfo.Catalog, tableInfo.Schema, tableInfo.Name),
column.Name,
columnInfo.TypeName.ToLowerInvariant(),
column.GetSqlType(dialect, mapping)));
columnType));
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/NHibernate/SchemaValidationException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
using System.Security;
using System.Text;

namespace NHibernate
{
Expand All @@ -28,5 +29,21 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont
base.GetObjectData(info, context);
info.AddValue("ValidationErrors", ValidationErrors);
}

public override string Message
{
get
{
var message = base.Message;
if (ValidationErrors == null || ValidationErrors.Count == 0)
return message;

var sb = new StringBuilder(message).AppendLine().AppendLine("Validation errors:");
foreach (var error in ValidationErrors)
sb.Append("\t- ").AppendLine(error);

return sb.ToString();
}
}
}
}