Skip to content

Generate stable sql objects names #1802

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

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 9 additions & 2 deletions src/NHibernate.Test/MappingTest/TableFixture.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Linq;
using System.Threading;
using NHibernate.Dialect;
using NHibernate.Mapping;
using NUnit.Framework;
Expand Down Expand Up @@ -61,5 +59,14 @@ public void SchemaNameQuoted()

Assert.AreEqual("[schema].name", tbl.GetQualifiedName(dialect));
}

[Test]
public void NameIsStable()
{
var tbl = new Table { Name = "name" };
Assert.That(
Constraint.GenerateName("FK_", tbl, null, new[] {new Column("col1"), new Column("col2")}),
Is.EqualTo("FK_3B355A0C"));
}
}
}
5 changes: 3 additions & 2 deletions src/NHibernate.Test/NHSpecificTest/NH1399/Fixture.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System;
using NHibernate.Mapping;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.NH1399
{
[TestFixture]
[TestFixture, Obsolete]
public class Fixture
Copy link
Member Author

Choose a reason for hiding this comment

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

This fixture is testing Table.UniqueColumnString with a non-null referencedTable parameter, case which was no more having any usage in NHibernate. So indeed these tests are obsolete since some times.

{
[Test]
Expand Down Expand Up @@ -42,4 +43,4 @@ public void UsingTwoInstancesWithSameValuesTheFkNameIsTheSame()
Assert.That(t2Fk_, Is.EqualTo(t2Fk));
}
}
}
}
8 changes: 7 additions & 1 deletion src/NHibernate/Cfg/Configuration.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -1191,6 +1190,13 @@ private void SecondPassCompileForeignKeys(Table table, ISet<ForeignKey> done)
try
{
fk.AddReferencedTable(referencedClass);

if (string.IsNullOrEmpty(fk.Name))
{
fk.Name = Constraint.GenerateName(
fk.GeneratedConstraintNamePrefix, table, fk.ReferencedTable, fk.Columns);
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a heavily simplified port of the Hibernate code, which instead uses name policy generation and bothers with quoting/unquoting (like the things removed in #1703).

}

fk.AlignColumns();
}
catch (MappingException me)
Expand Down
51 changes: 51 additions & 0 deletions src/NHibernate/Mapping/Constraint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,57 @@ public IEnumerable<Column> ColumnIterator
get { return columns; }
}

/// <summary>
/// Generate a name hopefully unique using the table and column names.
/// Static so the name can be generated prior to creating the Constraint.
/// They're cached, keyed by name, in multiple locations.
/// </summary>
/// <param name="prefix">A name prefix for the generated name.</param>
/// <param name="table">The table for which the name is generated.</param>
/// <param name="referencedTable">The referenced table, if any.</param>
/// <param name="columns">The columns for which the name is generated.</param>
/// <returns>The generated name.</returns>
/// <remarks>Hybrid of Hibernate <c>Constraint.generateName</c> and
/// <c>NamingHelper.generateHashedFkName</c>.</remarks>
public static string GenerateName(
string prefix, Table table, Table referencedTable, IEnumerable<Column> columns)
{
// Use a concatenation that guarantees uniqueness, even if identical names
// exist between all table and column identifiers.
var sb = new StringBuilder("table`").Append(table.Name).Append("`");
if (referencedTable != null)
sb.Append("references`").Append(referencedTable.Name).Append("`");

// Ensure a consistent ordering of columns, regardless of the order
// they were bound.
// Clone the list, as sometimes a set of order-dependent Column
// bindings are given.
var alphabeticalColumns = new List<Column>(columns);
alphabeticalColumns.Sort(ColumnComparator.Instance);
foreach (var column in alphabeticalColumns)
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 just use foreach (var column in columns.OrderBy(c=>c.Name))?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because I have ported the Java code without thinking about it.

{
var columnName = column == null ? "" : column.Name;
sb.Append("column`").Append(columnName).Append("`");
Copy link
Member

Choose a reason for hiding this comment

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

Again, here should be CanonicalName prob.

}
// Hash the generated name for avoiding collisions with user choosen names.
// This is not 100% reliable, as hashing may still have a chance of generating
// collisions.
// Hibernate uses MD5 here, which .Net standrad implementation is rejected by
// FIPS enabled machine. Better use a non-cryptographic hash.
var name = prefix + Hasher.HashToString(sb.ToString());

return name;
}

private class ColumnComparator : IComparer<Column>
{
public static readonly ColumnComparator Instance = new ColumnComparator();

public int Compare(Column col1, Column col2) {
return StringComparer.Ordinal.Compare(col1?.Name, col2?.Name);
Copy link
Member

Choose a reason for hiding this comment

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

This should compare on Column.CanonicalName. The Name of a Column class is case-sensitive when quoted and case-insensitive when not quoted.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it depends on database. SQL Server 2017 keeps respecting the collation of the database (in fact, of its metadata tables), quoted or not.
It may also be case sensitive when unquoted, not only when quoted. And this varies from databases to databases.

Quoted identifier being case sensitive is an Oracle feature. So I think it is better taking the name "as is", relying on the user to map its objects in a consistent way.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not talking about databases here, but about rules of NHibernate itself. Column has overridden Equals and GetHashCode operators which follow this rule. So we need to be consistent.

}
}

/// <summary>
/// Adds the <see cref="Column"/> to the <see cref="ICollection"/> of
/// Columns that are part of the constraint.
Expand Down
22 changes: 10 additions & 12 deletions src/NHibernate/Mapping/DenormalizedTable.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Collections;
using NHibernate.Util;
using System.Collections.Generic;
using System.Linq;

namespace NHibernate.Mapping
{
Expand Down Expand Up @@ -56,21 +56,19 @@ public override IEnumerable<Index> IndexIterator
public override void CreateForeignKeys()
{
includedTable.CreateForeignKeys();
IEnumerable includedFks = includedTable.ForeignKeyIterator;
foreach (ForeignKey fk in includedFks)
var includedFks = includedTable.ForeignKeyIterator;
foreach (var fk in includedFks)
{
// NH Different behaviour (fk name)
CreateForeignKey(GetForeignKeyName(fk), fk.Columns, fk.ReferencedEntityName);
CreateForeignKey(
Constraint.GenerateName(
fk.GeneratedConstraintNamePrefix,
this,
null,
fk.Columns),
fk.Columns, fk.ReferencedEntityName);
}
}

private string GetForeignKeyName(ForeignKey fk)
{
// (the FKName length, of H3.2 implementation, may be too long for some RDBMS so we implement something different)
int hash = fk.Name.GetHashCode() ^ Name.GetHashCode();
return string.Format("KF{0}", hash.ToString("X"));
}

public override Column GetColumn(Column column)
{
Column superColumn = base.GetColumn(column);
Expand Down
3 changes: 2 additions & 1 deletion src/NHibernate/Mapping/ForeignKey.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Collections;
using System.Collections.Generic;
using System.Text;
using NHibernate.Util;
Expand Down Expand Up @@ -233,5 +232,7 @@ public bool IsReferenceToPrimaryKey
{
get { return referencedColumns.Count == 0; }
}

public string GeneratedConstraintNamePrefix => "FK_";
}
}
20 changes: 9 additions & 11 deletions src/NHibernate/Mapping/Table.cs
Original file line number Diff line number Diff line change
Expand Up @@ -808,15 +808,9 @@ public virtual ForeignKey CreateForeignKey(string keyName, IEnumerable<Column> k
if (fk == null)
{
fk = new ForeignKey();
if (!string.IsNullOrEmpty(keyName))
{
fk.Name = keyName;
}
else
{
fk.Name = "FK" + UniqueColumnString(kCols, referencedEntityName);
//TODO: add referencedClass to disambiguate to FKs on the same columns, pointing to different tables
}
// NOTE : if the name is null, we will generate an implicit name during second pass processing
// after we know the referenced table name (which might not be resolved yet).
fk.Name = keyName;
fk.Table = this;
foreignKeys.Add(key, fk);
fk.ReferencedEntityName = referencedEntityName;
Expand All @@ -837,8 +831,8 @@ public virtual ForeignKey CreateForeignKey(string keyName, IEnumerable<Column> k

public virtual UniqueKey CreateUniqueKey(IList<Column> keyColumns)
{
string keyName = "UK" + UniqueColumnString(keyColumns);
UniqueKey uk = GetOrCreateUniqueKey(keyName);
var keyName = Constraint.GenerateName( "UK_", this, null, keyColumns);
var uk = GetOrCreateUniqueKey(keyName);
uk.AddColumns(keyColumns);
return uk;
}
Expand All @@ -851,11 +845,15 @@ public virtual UniqueKey CreateUniqueKey(IList<Column> keyColumns)
/// <returns>
/// An unique string for the <see cref="Column"/> objects.
/// </returns>
// Since v5.2
[Obsolete("Use Constraint.GenerateName instead.")]
public string UniqueColumnString(IEnumerable uniqueColumns)
{
return UniqueColumnString(uniqueColumns, null);
}

// Since v5.2
[Obsolete("Use Constraint.GenerateName instead.")]
public string UniqueColumnString(IEnumerable iterator, string referencedEntityName)
{
// NH Different implementation (NH-1399)
Expand Down
117 changes: 117 additions & 0 deletions src/NHibernate/Util/Hasher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Derived from MurmurHash2Simple,
* http://landman-code.blogspot.com/2009/02/c-superfasthash-and-murmurhash2.html
*/

/***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is HashTableHashing.MurmurHash2.
*
* The Initial Developer of the Original Code is
* Davy Landman.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */

using System;
using System.Text;

namespace NHibernate.Util
{
/// <summary>A stable hasher using MurmurHash2 algorithm.</summary>
internal static class Hasher
{
internal static string HashToString(string input)
{
var hash = Hash(input);
return hash.ToString("X");
}

internal static uint Hash(string input)
{
return Hash(Encoding.UTF8.GetBytes(input));
}

internal static uint Hash(byte[] data)
{
return Hash(data, 0xc58f1a7b);
}

private const uint _m = 0x5bd1e995;
private const int _r = 24;

internal static uint Hash(byte[] data, uint seed)
{
var length = data.Length;
if (length == 0)
return 0;
var h = seed ^ (uint) length;
var currentIndex = 0;
while (length >= 4)
{
var k = BitConverter.ToUInt32(data, currentIndex);
k *= _m;
k ^= k >> _r;
k *= _m;

h *= _m;
h ^= k;
currentIndex += 4;
length -= 4;
}

switch (length)
{
case 3:
h ^= BitConverter.ToUInt16(data, currentIndex);
h ^= (uint) data[currentIndex + 2] << 16;
h *= _m;
break;
case 2:
h ^= BitConverter.ToUInt16(data, currentIndex);
h *= _m;
break;
case 1:
h ^= data[currentIndex];
h *= _m;
break;
}

// Do a few final mixes of the hash to ensure the last few
// bytes are well-incorporated.

h ^= h >> 13;
h *= _m;
h ^= h >> 15;

return h;
}
}
}