From 7843e5b2dc89257c17ced840ce89aedd40a10896 Mon Sep 17 00:00:00 2001 From: Ricardo Peres Date: Thu, 20 Nov 2014 12:00:50 +0000 Subject: [PATCH] NH-3736 - Add new StructuredType --- .../SqlTest/Custom/MsSQL/MSSQLTest.cs | 87 +++++++++++++++ src/NHibernate/Driver/Sql2008ClientDriver.cs | 11 ++ src/NHibernate/Driver/SqlClientDriver.cs | 3 +- src/NHibernate/Impl/AbstractQueryImpl.cs | 41 ++++++- src/NHibernate/NHibernateUtil.cs | 22 +++- src/NHibernate/SqlTypes/SqlTypeFactory.cs | 5 + src/NHibernate/SqlTypes/StructuredSqlType.cs | 39 +++++++ src/NHibernate/Type/StructuredType.cs | 103 ++++++++++++++++++ src/NHibernate/Type/TypeFactory.cs | 10 +- 9 files changed, 310 insertions(+), 11 deletions(-) create mode 100644 src/NHibernate/SqlTypes/StructuredSqlType.cs create mode 100644 src/NHibernate/Type/StructuredType.cs diff --git a/src/NHibernate.Test/SqlTest/Custom/MsSQL/MSSQLTest.cs b/src/NHibernate.Test/SqlTest/Custom/MsSQL/MSSQLTest.cs index 75335c73475..2e593354605 100644 --- a/src/NHibernate.Test/SqlTest/Custom/MsSQL/MSSQLTest.cs +++ b/src/NHibernate.Test/SqlTest/Custom/MsSQL/MSSQLTest.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Data; using NHibernate.Dialect; using NUnit.Framework; @@ -16,5 +17,91 @@ protected override bool AppliesTo(Dialect.Dialect dialect) { return dialect is MsSql2000Dialect; } + + [Test] + public void CanCallStoredProcedureWithTableParameterInferedAutomatically() + { + //NH-3736 + var createTypeSql = "CREATE TYPE dbo.TableType AS TABLE " + + "( " + + " a INT, " + + " b INT " + + ")"; + var createProcedureSql = "CREATE PROCEDURE dbo.TableProcedure " + + "( " + + " @t TableType READONLY " + + ") " + + "AS " + + "BEGIN " + + " SELECT * " + + " FROM @t " + + " RETURN @@ROWCOUNT " + + "END"; + + var deleteTypeSql = "DROP TYPE dbo.TableType"; + var deleteProcedureSql = "DROP PROCEDURE dbo.TableProcedure"; + + using (var session = this.OpenSession()) + { + session.CreateSQLQuery(createTypeSql).ExecuteUpdate(); + session.CreateSQLQuery(createProcedureSql).ExecuteUpdate(); + + var table = new DataTable("dbo.TableType"); + table.Columns.Add("a", typeof (int)); + table.Columns.Add("b", typeof(int)); + table.Rows.Add(1, 2); + + var result = session.CreateSQLQuery("EXEC dbo.TableProcedure :t").SetParameter("t", table).List(); + + session.CreateSQLQuery(deleteProcedureSql).ExecuteUpdate(); + session.CreateSQLQuery(deleteTypeSql).ExecuteUpdate(); + + Assert.IsNotNull(result); + Assert.IsNotEmpty(result); + } + } + + [Test] + public void CanCallStoredProcedureWithTableParameterSetSpecifically() + { + //NH-3736 + var createTypeSql = "CREATE TYPE dbo.TableType AS TABLE " + + "( " + + " a INT, " + + " b INT " + + ")"; + var createProcedureSql = "CREATE PROCEDURE dbo.TableProcedure " + + "( " + + " @t TableType READONLY " + + ") " + + "AS " + + "BEGIN " + + " SELECT * " + + " FROM @t " + + " RETURN @@ROWCOUNT " + + "END"; + + var deleteTypeSql = "DROP TYPE dbo.TableType"; + var deleteProcedureSql = "DROP PROCEDURE dbo.TableProcedure"; + + using (var session = this.OpenSession()) + { + session.CreateSQLQuery(createTypeSql).ExecuteUpdate(); + session.CreateSQLQuery(createProcedureSql).ExecuteUpdate(); + + var table = new DataTable("dbo.TableType"); + table.Columns.Add("a", typeof(int)); + table.Columns.Add("b", typeof(int)); + table.Rows.Add(1, 2); + + var result = session.CreateSQLQuery("EXEC dbo.TableProcedure :t").SetParameter("t", table, NHibernateUtil.Structured("dbo.TableType")).List(); + + session.CreateSQLQuery(deleteProcedureSql).ExecuteUpdate(); + session.CreateSQLQuery(deleteTypeSql).ExecuteUpdate(); + + Assert.IsNotNull(result); + Assert.IsNotEmpty(result); + } + } } } \ No newline at end of file diff --git a/src/NHibernate/Driver/Sql2008ClientDriver.cs b/src/NHibernate/Driver/Sql2008ClientDriver.cs index 0068832139f..b1037b59eca 100644 --- a/src/NHibernate/Driver/Sql2008ClientDriver.cs +++ b/src/NHibernate/Driver/Sql2008ClientDriver.cs @@ -1,6 +1,8 @@ using System; using System.Data; using System.Data.Common; +using System.Data.SqlClient; +using NHibernate.SqlTypes; using NHibernate.Util; namespace NHibernate.Driver @@ -11,8 +13,10 @@ public class Sql2008ClientDriver : SqlClientDriver #if NETFX private static readonly Action SetSqlDbType = (p, t) => ((System.Data.SqlClient.SqlParameter) p).SqlDbType = t; + private static readonly Action SetTypeName = (p, t) => ((System.Data.SqlClient.SqlParameter) p).TypeName = t; #else private static readonly Action SetSqlDbType = DelegateHelper.BuildPropertySetter(System.Type.GetType("System.Data.SqlClient.SqlParameter, System.Data.SqlClient", true), "SqlDbType"); + private static readonly Action SetTypeName = DelegateHelper.BuildPropertySetter(System.Type.GetType("System.Data.SqlClient.SqlParameter, System.Data.SqlClient", true), "TypeName"); #endif protected override void InitializeParameter(DbParameter dbParam, string name, SqlTypes.SqlType sqlType) @@ -28,6 +32,13 @@ protected override void InitializeParameter(DbParameter dbParam, string name, Sq SetSqlDbType(dbParam, SqlDbType.Date); break; } + + if (sqlType is StructuredSqlType type) + { + //NH-3736 + SetSqlDbType(dbParam, SqlDbType.Structured); + SetTypeName(dbParam, type.TypeName); + } } public override bool RequiresTimeSpanForTime => true; diff --git a/src/NHibernate/Driver/SqlClientDriver.cs b/src/NHibernate/Driver/SqlClientDriver.cs index 679fe397bc4..30722e49c68 100644 --- a/src/NHibernate/Driver/SqlClientDriver.cs +++ b/src/NHibernate/Driver/SqlClientDriver.cs @@ -154,13 +154,12 @@ public override bool SupportsMultipleOpenReaders protected override void InitializeParameter(DbParameter dbParam, string name, SqlType sqlType) { base.InitializeParameter(dbParam, name, sqlType); - // Defaults size/precision/scale switch (dbParam.DbType) { case DbType.AnsiString: case DbType.AnsiStringFixedLength: - dbParam.Size = IsAnsiText(dbParam, sqlType) ? MsSql2000Dialect.MaxSizeForAnsiClob : MsSql2000Dialect.MaxSizeForLengthLimitedAnsiString; + dbParam.Size = IsAnsiText(dbParam, sqlType) ? MsSql2000Dialect.MaxSizeForAnsiClob : MsSql2000Dialect.MaxSizeForLengthLimitedAnsiString; break; case DbType.Binary: dbParam.Size = IsBlob(dbParam, sqlType) ? MsSql2000Dialect.MaxSizeForBlob : MsSql2000Dialect.MaxSizeForLengthLimitedBinary; diff --git a/src/NHibernate/Impl/AbstractQueryImpl.cs b/src/NHibernate/Impl/AbstractQueryImpl.cs index 3fa74af00f2..ce9e2b3403a 100644 --- a/src/NHibernate/Impl/AbstractQueryImpl.cs +++ b/src/NHibernate/Impl/AbstractQueryImpl.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Data; using NHibernate.Engine; using NHibernate.Engine.Query; using NHibernate.Hql; @@ -312,7 +313,22 @@ public IQuery SetParameter(int position, T val) { CheckPositionalParameter(position); - return SetParameter(position, val, parameterMetadata.GetOrdinalParameterExpectedType(position + 1) ?? GuessType(typeof(T))); + var type = parameterMetadata.GetOrdinalParameterExpectedType(position + 1); + + if (type == null) + { + var table = val as DataTable; + if (table != null) + { + type = NHibernateUtil.Structured(table.TableName); + } + else + { + type = GuessType(typeof(T)); + } + } + + return SetParameter(position, val, type); } private void CheckPositionalParameter(int position) @@ -329,7 +345,22 @@ private void CheckPositionalParameter(int position) public IQuery SetParameter(string name, T val) { - return SetParameter(name, val, parameterMetadata.GetNamedParameterExpectedType(name) ?? GuessType(typeof (T))); + var type = parameterMetadata.GetNamedParameterExpectedType(name); + + if (type == null) + { + var table = val as DataTable; + if (table != null) + { + type = NHibernateUtil.Structured(table.TableName); + } + else + { + type = GuessType(typeof (T)); + } + } + + return SetParameter(name, val, type); } public IQuery SetParameter(string name, object val) @@ -345,8 +376,7 @@ public IQuery SetParameter(string name, object val) IType type = parameterMetadata.GetNamedParameterExpectedType(name); if (type == null) { - throw new ArgumentNullException("val", - "A type specific Set(name, val) should be called because the Type can not be guessed from a null value."); + throw new ArgumentNullException("val", "A type specific Set(name, val) should be called because the Type can not be guessed from a null value."); } SetParameter(name, val, type); @@ -363,8 +393,7 @@ public IQuery SetParameter(int position, object val) { if (val == null) { - throw new ArgumentNullException("val", - "A type specific Set(position, val) should be called because the Type can not be guessed from a null value."); + throw new ArgumentNullException("val", "A type specific Set(position, val) should be called because the Type can not be guessed from a null value."); } else { diff --git a/src/NHibernate/NHibernateUtil.cs b/src/NHibernate/NHibernateUtil.cs index f2af195605c..f398c0964cf 100644 --- a/src/NHibernate/NHibernateUtil.cs +++ b/src/NHibernate/NHibernateUtil.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Data; using NHibernate.Collection; using NHibernate.Impl; using NHibernate.Intercept; @@ -41,7 +42,10 @@ public static IType GuessType(System.Type type) var value = TypeFactory.GetDefaultTypeFor(type); if (value != null) return value; - + + if (type == typeof (DataTable)) + return Structured(); + if (type.IsEnum) return (IType) Activator.CreateInstance(typeof (EnumType<>).MakeGenericType(type)); @@ -54,6 +58,22 @@ public static IType GuessType(System.Type type) return Entity(type); } + /// + /// Table-valued type with type name parameter + /// + public static StructuredType Structured(string typeName) + { + return new StructuredType(typeName); + } + + /// + /// Table-valued type + /// + public static StructuredType Structured() + { + return Structured(string.Empty); + } + /// /// NHibernate Ansi String type /// diff --git a/src/NHibernate/SqlTypes/SqlTypeFactory.cs b/src/NHibernate/SqlTypes/SqlTypeFactory.cs index 3f12c5224e2..41646cc4aed 100644 --- a/src/NHibernate/SqlTypes/SqlTypeFactory.cs +++ b/src/NHibernate/SqlTypes/SqlTypeFactory.cs @@ -35,6 +35,11 @@ public static class SqlTypeFactory public static readonly SqlType UInt32 = new SqlType(DbType.UInt32); public static readonly SqlType UInt64 = new SqlType(DbType.UInt64); + public static SqlType Structured(string typeName) + { + return new StructuredSqlType(typeName); + } + public static readonly SqlType[] NoTypes = Array.Empty(); private delegate T TypeWithLenOrScaleCreateDelegate(TDim lengthOrScale); // Func diff --git a/src/NHibernate/SqlTypes/StructuredSqlType.cs b/src/NHibernate/SqlTypes/StructuredSqlType.cs new file mode 100644 index 00000000000..a1b6c1339c8 --- /dev/null +++ b/src/NHibernate/SqlTypes/StructuredSqlType.cs @@ -0,0 +1,39 @@ +using System; +using System.Data; + +namespace NHibernate.SqlTypes +{ + [Serializable] + public class StructuredSqlType : SqlType, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + public StructuredSqlType(string typeName) : base(DbType.Object) + { + TypeName = typeName ?? string.Empty; + } + + public string TypeName { get; } + + public override bool Equals(object obj) + { + return Equals(obj as StructuredSqlType); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ (TypeName != null ? TypeName.GetHashCode() : 0); + } + } + + public bool Equals(StructuredSqlType other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && string.Equals(TypeName, other.TypeName); + } + } +} diff --git a/src/NHibernate/Type/StructuredType.cs b/src/NHibernate/Type/StructuredType.cs new file mode 100644 index 00000000000..897f8cceb47 --- /dev/null +++ b/src/NHibernate/Type/StructuredType.cs @@ -0,0 +1,103 @@ +using System; +using System.Data; +using System.Data.Common; +using System.IO; +using System.Text; +using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; + +namespace NHibernate.Type +{ + [Serializable] + public class StructuredType : MutableType, IParameterizedType, IEquatable + { + public const string TypeNameParameter = "TypeName"; + + public string TypeName { get; private set; } + + public StructuredType() + : this(string.Empty) + { + } + + public StructuredType(string typeName) : base(SqlTypeFactory.Structured(typeName)) + { + TypeName = typeName ?? string.Empty; + } + + public override object DeepCopyNotNull(object value) + { + return ((DataTable) value).Clone(); + } + + public override void Set(DbCommand cmd, object value, int index, ISessionImplementor session) + { + cmd.Parameters[index].Value = value; + } + + public override object Get(DbDataReader rs, int index, ISessionImplementor session) + { + return (DataTable) rs[index]; + } + + public override object Get(DbDataReader rs, string name, ISessionImplementor session) + { + return (DataTable) rs[name]; + } + + [Obsolete("This method has no more usages and will be removed in a future version. Override ToLoggableString instead.")] + public override string ToString(object val) + { + var builder = new StringBuilder(); + using (var stream = new StringWriter(builder)) + { + ((DataTable) val).WriteXml(stream); + return builder.ToString(); + } + } + + [Obsolete("This method has no more usages and will be removed in a future version.")] + public override object FromStringValue(string xml) + { + using (var stream = new StringReader(xml)) + { + var table = new DataTable(); + table.ReadXml(stream); + return table; + } + } + + public override bool Equals(object obj) + { + return Equals(obj as StructuredType); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ (TypeName != null ? TypeName.GetHashCode() : 0); + } + } + + public override string Name => "Structured"; + + public override System.Type ReturnedClass => typeof(DataTable); + + public void SetParameterValues(System.Collections.Generic.IDictionary parameters) + { + if (parameters.TryGetValue(TypeNameParameter, out var typeName)) + { + TypeName = typeName; + } + } + + public bool Equals(StructuredType other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && string.Equals(TypeName, other.TypeName); + } + } +} diff --git a/src/NHibernate/Type/TypeFactory.cs b/src/NHibernate/Type/TypeFactory.cs index cd416deddb6..b787ce89fba 100644 --- a/src/NHibernate/Type/TypeFactory.cs +++ b/src/NHibernate/Type/TypeFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Data; using System.Globalization; using System.Reflection; using System.Xml; @@ -375,6 +376,11 @@ private static TypeClassification GetTypeClassification(string typeName) } } + public static IType Structured(string typeName) + { + return new StructuredType(typeName); + } + /// /// Given the name of a Hibernate type such as Decimal, Decimal(19,0) /// , Int32, or even NHibernate.Type.DecimalType, NHibernate.Type.DecimalType(19,0), @@ -516,7 +522,7 @@ public static IType HeuristicType(string typeName, IDictionary p if (type != null) return type; - + string[] parsedTypeName; TypeClassification typeClassification = GetTypeClassification(typeName); if (typeClassification == TypeClassification.LengthOrScale) @@ -537,7 +543,7 @@ public static IType HeuristicType(string typeName, IDictionary p if (typeClass == null) return null; - + if (typeof(IType).IsAssignableFrom(typeClass)) { try