diff --git a/src/NHibernate.Test/NHSpecificTest/NH3634/CachedPerson.cs b/src/NHibernate.Test/NHSpecificTest/NH3634/CachedPerson.cs new file mode 100644 index 00000000000..8a1ad99e168 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3634/CachedPerson.cs @@ -0,0 +1,9 @@ +namespace NHibernate.Test.NHSpecificTest.NH3634 +{ + class CachedPerson + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual Connection Connection { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3634/CachedPersonMapper.cs b/src/NHibernate.Test/NHSpecificTest/NH3634/CachedPersonMapper.cs new file mode 100644 index 00000000000..69700be3726 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3634/CachedPersonMapper.cs @@ -0,0 +1,26 @@ +using NHibernate.Mapping.ByCode; +using NHibernate.Mapping.ByCode.Conformist; + +namespace NHibernate.Test.NHSpecificTest.NH3634 +{ + class CachedPersonMapper : ClassMapping + { + public CachedPersonMapper() + { + Id(p => p.Id, m => m.Generator(Generators.Identity)); + Lazy(false); + Cache(m => m.Usage(CacheUsage.ReadWrite)); + Table("cachedpeople"); + Property(p => p.Name); + Component( + p => p.Connection, + m => + { + m.Class(); + m.Property(c => c.ConnectionType, mapper => mapper.NotNullable(true)); + m.Property(c => c.Address, mapper => mapper.NotNullable(false)); + m.Property(c => c.PortName, mapper => mapper.NotNullable(false)); + }); + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3634/Connection.cs b/src/NHibernate.Test/NHSpecificTest/NH3634/Connection.cs new file mode 100644 index 00000000000..93ed1522daf --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3634/Connection.cs @@ -0,0 +1,9 @@ +namespace NHibernate.Test.NHSpecificTest.NH3634 +{ + class Connection + { + public virtual string ConnectionType { get; set; } + public virtual string Address { get; set; } + public virtual string PortName { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3634/FixtureByCode.cs b/src/NHibernate.Test/NHSpecificTest/NH3634/FixtureByCode.cs new file mode 100644 index 00000000000..d6d038e5ce6 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3634/FixtureByCode.cs @@ -0,0 +1,345 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Criterion; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH3634 +{ + public class ByCodeFixture : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.AddMapping(); + mapper.AddMapping(); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + var bobsConnection = new Connection + { + Address = "test.com", + ConnectionType = "http", + PortName = "80" + }; + var e1 = new Person + { + Name = "Bob", + Connection = bobsConnection + }; + session.Save(e1); + + var sallysConnection = new Connection + { + Address = "test.com", + ConnectionType = "http", + }; + var e2 = new Person + { + Name = "Sally", + Connection = sallysConnection + }; + session.Save(e2); + + var cachedNullConnection = new Connection + { + Address = "test.com", + ConnectionType = "http", + }; + var cachedNullConnectionPerson = new CachedPerson + { + Name = "CachedNull", + Connection = cachedNullConnection + }; + var cachedNotNullConnection = new Connection + { + Address = "test.com", + ConnectionType = "http", + PortName = "port" + }; + var cachedNotNullConnectionPerson = new CachedPerson + { + Name = "CachedNotNull", + Connection = cachedNotNullConnection + }; + session.Save(cachedNullConnectionPerson); + session.Save(cachedNotNullConnectionPerson); + + session.Flush(); + transaction.Commit(); + session.Evict(typeof(CachedPerson)); + } + } + + protected override void OnTearDown() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + session.Flush(); + transaction.Commit(); + } + } + + [Test] + public void QueryOverComponentWithANullProperty() + { +// Broken at the time NH3634 was reported +// Generates the following Rpc(exec sp_executesql) +// SELECT this_.Id as Id0_0_, +// this_.Name as Name0_0_, +// this_.ConnectionType as Connecti3_0_0_, +// this_.Address as Address0_0_, +// this_.PortName as PortName0_0_ +// FROM people this_ +// WHERE this_.ConnectionType = @p0 +// and this_.Address = @p1 +// and this_.PortName = @p2 +// +// @p0=N'http',@p1=N'test.com',@p2=NULL + + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var componentToCompare = new Connection + { + ConnectionType = "http", + Address = "test.com", + PortName = null + }; + var sally = session.QueryOver() + .Where(p => p.Connection == componentToCompare) + .SingleOrDefault(); + + Assert.That(sally.Name, Is.EqualTo("Sally")); + Assert.That(sally.Connection.PortName, Is.Null); + } + } + + [Test] + public void QueryAgainstComponentWithANullPropertyUsingCriteria() + { +// Broken at the time NH3634 was reported +// Generates the following Rpc(exec sp_executesql) +// SELECT this_.Id as Id0_0_, +// this_.Name as Name0_0_, +// this_.ConnectionType as Connecti3_0_0_, +// this_.Address as Address0_0_, +// this_.PortName as PortName0_0_ +// FROM people this_ +// WHERE this_.ConnectionType = @p0 +// and this_.Address = @p1 +// and this_.PortName = @p2 +// +// @p0=N'http',@p1=N'test.com',@p2=NULL + + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var componentToCompare = new Connection + { + ConnectionType = "http", + Address = "test.com", + PortName = null + }; + var sally = session.CreateCriteria() + .Add(Restrictions.Eq("Connection", componentToCompare)) + .UniqueResult(); + + Assert.That(sally.Name, Is.EqualTo("Sally")); + Assert.That(sally.Connection.PortName, Is.Null); + } + } + + [Test] + public void CachedQueryMissesWithDifferentNotNullComponent() + { + var componentToCompare = new Connection + { + ConnectionType = "http", + Address = "test.com", + PortName = null + }; + + using (ISession session = OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + var cached = session.CreateCriteria() + .Add(Restrictions.Eq("Connection", componentToCompare)) + .SetCacheable(true) + .UniqueResult(); + + Assert.That(cached.Name, Is.EqualTo("CachedNull")); + Assert.That(cached.Connection.PortName, Is.Null); + + using (var dbCommand = session.Connection.CreateCommand()) + { + dbCommand.CommandText = "DELETE FROM cachedpeople"; + tx.Enlist(dbCommand); + dbCommand.ExecuteNonQuery(); + } + + tx.Commit(); + } + + componentToCompare.PortName = "port"; + using (ISession session = OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + //Cache should not return cached entity, because it no longer matches criteria + var cachedPeople = session.CreateCriteria() + .Add(Restrictions.Eq("Connection", componentToCompare)) + .SetCacheable(true) + .List(); + + Assert.That(cachedPeople, Is.Empty); + + tx.Commit(); + } + } + + [Test] + public void CachedQueryMissesWithDifferentNullComponent() + { + var componentToCompare = new Connection + { + ConnectionType = "http", + Address = "test.com", + PortName = "port" + }; + + using (ISession session = OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + var cached = session.CreateCriteria() + .Add(Restrictions.Eq("Connection", componentToCompare)) + .SetCacheable(true) + .UniqueResult(); + + Assert.That(cached.Name, Is.EqualTo("CachedNotNull")); + Assert.That(cached.Connection.PortName, Is.Not.Null); + + using (var dbCommand = session.Connection.CreateCommand()) + { + dbCommand.CommandText = "DELETE FROM cachedpeople"; + tx.Enlist(dbCommand); + dbCommand.ExecuteNonQuery(); + } + + tx.Commit(); + } + + componentToCompare.PortName = null; + using (ISession session = OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + //Cache should not return cached entity, because it no longer matches criteria + var cachedPeople = session.CreateCriteria() + .Add(Restrictions.Eq("Connection", componentToCompare)) + .SetCacheable(true) + .List(); + + Assert.That(cachedPeople, Is.Empty); + + tx.Commit(); + } + } + + [Test] + public void CachedQueryAgainstComponentWithANullPropertyUsingCriteria() + { + var componentToCompare = new Connection + { + ConnectionType = "http", + Address = "test.com", + PortName = null + }; + + using (ISession session = OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + var cached = session.CreateCriteria() + .Add(Restrictions.Eq("Connection", componentToCompare)) + .SetCacheable(true) + .UniqueResult(); + + Assert.That(cached.Name, Is.EqualTo("CachedNull")); + Assert.That(cached.Connection.PortName, Is.Null); + + using (var dbCommand = session.Connection.CreateCommand()) + { + dbCommand.CommandText = "DELETE FROM cachedpeople"; + tx.Enlist(dbCommand); + dbCommand.ExecuteNonQuery(); + } + + tx.Commit(); + } + + using (ISession session = OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + //Should retreive from cache since we deleted directly from database. + var cached = session.CreateCriteria() + .Add(Restrictions.Eq("Connection", componentToCompare)) + .SetCacheable(true) + .UniqueResult(); + + Assert.That(cached.Name, Is.EqualTo("CachedNull")); + Assert.That(cached.Connection.PortName, Is.Null); + + tx.Commit(); + } + } + + [Test] + public void QueryOverANullComponentProperty() + { +// Works at the time NH3634 was reported +// Generates the following SqlBatch: +// SELECT this_.Id as Id0_0_, +// this_.Name as Name0_0_, +// this_.ConnectionType as Connecti3_0_0_, +// this_.Address as Address0_0_, +// this_.PortName as PortName0_0_ +// FROM people this_ +// WHERE this_.PortName is null + + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var sally = session.QueryOver() + .Where(p => p.Connection.PortName == null) + .And(p => p.Connection.Address == "test.com") + .And(p => p.Connection.ConnectionType == "http") + .SingleOrDefault(); + + Assert.That(sally.Name, Is.EqualTo("Sally")); + Assert.That(sally.Connection.PortName, Is.Null); + } + } + + [Test] + public void QueryAgainstANullComponentPropertyUsingCriteriaApi() + { + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var sally = session.CreateCriteria() + .Add(Restrictions.Eq("Connection.PortName", null)) + .Add(Restrictions.Eq("Connection.Address", "test.com")) + .Add(Restrictions.Eq("Connection.ConnectionType", "http")) + .UniqueResult(); + + Assert.That(sally.Name, Is.EqualTo("Sally")); + Assert.That(sally.Connection.PortName, Is.Null); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3634/Person.cs b/src/NHibernate.Test/NHSpecificTest/NH3634/Person.cs new file mode 100644 index 00000000000..009954bf1bb --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3634/Person.cs @@ -0,0 +1,9 @@ +namespace NHibernate.Test.NHSpecificTest.NH3634 +{ + class Person + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual Connection Connection { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3634/PersonMapper.cs b/src/NHibernate.Test/NHSpecificTest/NH3634/PersonMapper.cs new file mode 100644 index 00000000000..6e7844db955 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3634/PersonMapper.cs @@ -0,0 +1,25 @@ +using NHibernate.Mapping.ByCode; +using NHibernate.Mapping.ByCode.Conformist; + +namespace NHibernate.Test.NHSpecificTest.NH3634 +{ + class PersonMapper : ClassMapping + { + public PersonMapper() + { + Id(p => p.Id, m => m.Generator(Generators.Identity)); + Lazy(false); + Table("people"); + Property(p => p.Name); + Component( + p => p.Connection, + m => + { + m.Class(); + m.Property(c => c.ConnectionType, mapper => mapper.NotNullable(true)); + m.Property(c => c.Address, mapper => mapper.NotNullable(false)); + m.Property(c => c.PortName, mapper => mapper.NotNullable(false)); + }); + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index 7689ef6bd5e..45d796ec55f 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -728,6 +728,8 @@ + + @@ -863,6 +865,10 @@ + + + + diff --git a/src/NHibernate/Criterion/SimpleExpression.cs b/src/NHibernate/Criterion/SimpleExpression.cs index d08c1411c6a..bd82e7c71cf 100644 --- a/src/NHibernate/Criterion/SimpleExpression.cs +++ b/src/NHibernate/Criterion/SimpleExpression.cs @@ -88,8 +88,9 @@ public override SqlString ToSqlString(ICriteria criteria, ICriteriaQuery criteri this, value); - Parameter[] parameters = criteriaQuery.NewQueryParameter(GetParameterTypedValue(criteria, criteriaQuery)).ToArray(); - + TypedValue typedValue = GetParameterTypedValue(criteria, criteriaQuery); + Parameter[] parameters = criteriaQuery.NewQueryParameter(typedValue).ToArray(); + if (ignoreCase) { if (columnNames.Length != 1) @@ -98,7 +99,7 @@ public override SqlString ToSqlString(ICriteria criteria, ICriteriaQuery criteri "case insensitive expression may only be applied to single-column properties: " + propertyName); } - + return new SqlString( criteriaQuery.Factory.Dialect.LowercaseFunction, StringHelper.OpenParen, @@ -110,17 +111,31 @@ public override SqlString ToSqlString(ICriteria criteria, ICriteriaQuery criteri else { SqlStringBuilder sqlBuilder = new SqlStringBuilder(4 * columnNames.Length); + var columnNullness = typedValue.Type.ToColumnNullness(typedValue.Value, criteriaQuery.Factory); + if (columnNullness.Length != columnNames.Length) + { + throw new AssertionFailure("Column nullness length doesn't match number of columns."); + } + for (int i = 0; i < columnNames.Length; i++) { if (i > 0) { sqlBuilder.Add(" and "); } - - sqlBuilder.Add(columnNames[i]) - .Add(Op) - .Add(parameters[i]); + + if (columnNullness[i]) + { + sqlBuilder.Add(columnNames[i]) + .Add(Op) + .Add(parameters[i]); + } + else + { + sqlBuilder.Add(columnNames[i]) + .Add(" is null "); + } } return sqlBuilder.ToSqlString(); } diff --git a/src/NHibernate/SqlCommand/SqlCommandImpl.cs b/src/NHibernate/SqlCommand/SqlCommandImpl.cs index 74cfab3a8b9..8ad7f7fe2ff 100644 --- a/src/NHibernate/SqlCommand/SqlCommandImpl.cs +++ b/src/NHibernate/SqlCommand/SqlCommandImpl.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -105,7 +106,7 @@ public void ResetParametersIndexesForTheCommand(int singleSqlParametersOffset) } // due to IType.NullSafeSet(System.Data.IDbCommand , object, int, ISessionImplementor) the SqlType[] is supposed to be in a certain sequence. - // this mean that found the first location of a parameter for the IType span, the others are in secuence + // this mean that found the first location of a parameter for the IType span, the others are in sequence foreach (IParameterSpecification specification in Specifications) { string firstParameterId = specification.GetIdsForBackTrack(factory).First(); @@ -115,7 +116,7 @@ public void ResetParametersIndexesForTheCommand(int singleSqlParametersOffset) int firstParamNameIndex = effectiveParameterLocations[0] + singleSqlParametersOffset; foreach (int location in effectiveParameterLocations) { - int parameterSpan = specification.ExpectedType.GetColumnSpan(factory); + int parameterSpan = Math.Min(specification.ExpectedType.GetColumnSpan(factory), SqlQueryParametersList.Count); for (int j = 0; j < parameterSpan; j++) { sqlQueryParametersList[location + j].ParameterPosition = firstParamNameIndex + j; diff --git a/src/NHibernate/Type/MetaType.cs b/src/NHibernate/Type/MetaType.cs index 55fbf2c9198..e8dc7037870 100644 --- a/src/NHibernate/Type/MetaType.cs +++ b/src/NHibernate/Type/MetaType.cs @@ -109,7 +109,7 @@ public override void SetToXMLNode(XmlNode node, object value, ISessionFactoryImp public override bool[] ToColumnNullness(object value, IMapping mapping) { - throw new NotSupportedException(); + return baseType.ToColumnNullness(value, mapping); } public string ToXMLString(object value, ISessionFactoryImplementor factory)