diff --git a/doc/reference/modules/configuration.xml b/doc/reference/modules/configuration.xml index 15656f556c1..ee6644cadf7 100644 --- a/doc/reference/modules/configuration.xml +++ b/doc/reference/modules/configuration.xml @@ -871,6 +871,20 @@ var session = sessions.OpenSession(conn); + + + escape_backslash_in_strings + + + Indicates if the database needs to have backslash escaped in string literals. + The default value is dialect dependant. That is false for + most dialects. + + eg. + true | false + + + show_sql @@ -1515,12 +1529,6 @@ in the parameter binding. NHibernate.Dialect.PostgreSQLDialect - - PostgreSQL - NHibernate.Dialect.PostgreSQLDialect - - - PostgreSQL 8.1 NHibernate.Dialect.PostgreSQL81Dialect diff --git a/src/NHibernate.Config.Templates/SapSQLAnywhere.cfg.xml b/src/NHibernate.Config.Templates/SapSQLAnywhere.cfg.xml index 9512fef12d6..1ce5a50bbbc 100644 --- a/src/NHibernate.Config.Templates/SapSQLAnywhere.cfg.xml +++ b/src/NHibernate.Config.Templates/SapSQLAnywhere.cfg.xml @@ -15,7 +15,7 @@ for your own use before compiling tests in Visual Studio. UID=DBA;PWD=sql;Server=localhost;DBN=nhibernate;DBF=c:\nhibernate.db;ASTOP=No;Enlist=false; - NHibernate.Dialect.SybaseSQLAnywhere12Dialect + NHibernate.Dialect.SapSQLAnywhere17Dialect true=1;false=0 diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH3516/FixtureByCode.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH3516/FixtureByCode.cs new file mode 100644 index 00000000000..f63dcb43745 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH3516/FixtureByCode.cs @@ -0,0 +1,399 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.SqlTypes; +using NHibernate.Type; +using NUnit.Framework; +using NUnit.Framework.Internal; + +namespace NHibernate.Test.NHSpecificTest.GH3516 +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureByCodeAsync : TestCaseMappingByCode + { + + private readonly HashSet _unsupportedProperties = new(); + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Property(x => x.FirstChar); + rc.Property(x => x.CharacterEnum, m => m.Type>()); + rc.Property(x => x.UriProperty); + + rc.Property(x => x.ByteProperty); + rc.Property(x => x.DecimalProperty); + rc.Property(x => x.DoubleProperty); + rc.Property(x => x.FloatProperty); + rc.Property(x => x.ShortProperty); + rc.Property(x => x.IntProperty); + rc.Property(x => x.LongProperty); + + if (TestDialect.SupportsSqlType(SqlTypeFactory.SByte)) + rc.Property(x => x.SByteProperty); + else + _unsupportedProperties.Add(nameof(Entity.SByteProperty)); + + if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt16)) + rc.Property(x => x.UShortProperty); + else + _unsupportedProperties.Add(nameof(Entity.UShortProperty)); + + if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt32)) + rc.Property(x => x.UIntProperty); + else + _unsupportedProperties.Add(nameof(Entity.UIntProperty)); + + if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt64)) + rc.Property(x => x.ULongProperty); + else + _unsupportedProperties.Add(nameof(Entity.ULongProperty)); + + rc.Property(x => x.DateTimeProperty); + rc.Property(x => x.DateProperty, m => m.Type(NHibernateUtil.Date)); + if (TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet)) + rc.Property(x => x.DateTimeOffsetProperty); + else + _unsupportedProperties.Add(nameof(Entity.DateTimeOffsetProperty)); + rc.Property(x => x.TimeProperty, m => m.Type(NHibernateUtil.Time)); + + rc.Property(x => x.GuidProperty); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Discriminator(x => x.Column("StringDiscriminator")); + rc.Property(x => x.Name); + rc.Abstract(true); + }); + mapper.Subclass(rc => rc.DiscriminatorValue(Entity.NameWithSingleQuote)); + mapper.Subclass(rc => rc.DiscriminatorValue(Entity.NameWithEscapedSingleQuote)); + + mapper.Import(); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + private CultureInfo _backupCulture; + private CultureInfo _backupUICulture; + + protected override void OnSetUp() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + session.Save( + new Entity + { + Name = Entity.NameWithSingleQuote, + FirstChar = Entity.QuoteInitial, + CharacterEnum = CharEnum.SingleQuote, + UriProperty = Entity.UriWithSingleQuote + }); + session.Save( + new Entity + { + Name = Entity.NameWithEscapedSingleQuote, + FirstChar = Entity.BackslashInitial, + CharacterEnum = CharEnum.Backslash, + UriProperty = Entity.UriWithEscapedSingleQuote + }); + + transaction.Commit(); + + _backupCulture = CultureInfo.CurrentCulture; + _backupUICulture = CultureInfo.CurrentUICulture; + } + + protected override void OnTearDown() + { + if (_backupCulture != null) + { + CultureInfo.CurrentCulture = _backupCulture; + CultureInfo.CurrentUICulture = _backupUICulture; + } + + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + private static readonly string[] StringInjectionsProperties = + { + nameof(Entity.NameWithSingleQuote), nameof(Entity.NameWithEscapedSingleQuote) + }; + + [TestCaseSource(nameof(StringInjectionsProperties))] + public void SqlInjectionInStringsAsync(string propertyName) + { + using var session = OpenSession(); + + var query = session.CreateQuery($"from Entity e where e.Name = Entity.{propertyName}"); + IList list = null; + Assert.That(async () => list = await (query.ListAsync()), Throws.Nothing); + Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with name {propertyName}"); + } + + private static readonly string[] SpecialNames = + { + "\0; drop table Entity; --", + "\b; drop table Entity; --", + "\n; drop table Entity; --", + "\r; drop table Entity; --", + "\t; drop table Entity; --", + "\x1A; drop table Entity; --", + "\"; drop table Entity; --", + "\\; drop table Entity; --" + }; + + [TestCaseSource(nameof(SpecialNames))] + public async Task StringsWithSpecialCharactersAsync(string name) + { + // We may not even be able to insert the entity. + var wasInserted = false; + try + { + using var s = OpenSession(); + using var t = s.BeginTransaction(); + var e = new Entity { Name = name }; + await (s.SaveAsync(e)); + await (t.CommitAsync()); + + wasInserted = true; + } + catch (Exception e) + { + Assert.Warn($"The entity insertion failed with message {e}"); + } + + try + { + using var session = OpenSession(); + Entity.ArbitraryStringValue = name; + var list = await (session.CreateQuery($"from Entity e where e.Name = Entity.{nameof(Entity.ArbitraryStringValue)}").ListAsync()); + if (wasInserted && list.Count != 1) + Assert.Warn($"Unable to find entity with name {nameof(Entity.ArbitraryStringValue)}"); + } + catch (Exception e) + { + Assert.Warn($"The query has failed with message {e}"); + } + + // Check the db is not wrecked. + if (wasInserted) + { + using var session = OpenSession(); + var list = await (session + .CreateQuery("from Entity e where e.Name = :name") + .SetString("name", name) + .ListAsync()); + Assert.That(list, Has.Count.EqualTo(1)); + } + else + { + using var session = OpenSession(); + var all = await (session.CreateQuery("from Entity e").ListAsync()); + Assert.That(all, Has.Count.GreaterThan(0)); + } + } + + [Test] + public async Task SqlInjectionInStringDiscriminatorAsync() + { + using var session = OpenSession(); + + await (session.SaveAsync(new Subclass1 { Name = "Subclass1" })); + await (session.SaveAsync(new Subclass2 { Name = "Subclass2" })); + + // ObjectToSQLString is used for generating the inserts. + Assert.That(session.Flush, Throws.Nothing, "Unable to flush the subclasses"); + + foreach (var entityName in new[] { nameof(Subclass1), nameof(Subclass2) }) + { + var query = session.CreateQuery($"from {entityName}"); + IList list = null; + Assert.That(async () => list = await (query.ListAsync()), Throws.Nothing, $"Unable to list entities of {entityName}"); + Assert.That(list, Has.Count.EqualTo(1), $"Unable to find the {entityName} entity"); + } + } + + private static readonly string[] CharInjectionsProperties = + { + nameof(Entity.QuoteInitial), nameof(Entity.BackslashInitial) + }; + + [TestCaseSource(nameof(CharInjectionsProperties))] + public void SqlInjectionInCharAsync(string propertyName) + { + using var session = OpenSession(); + var query = session.CreateQuery($"from Entity e where e.FirstChar = Entity.{propertyName}"); + IList list = null; + Assert.That(async () => list = await (query.ListAsync()), Throws.Nothing); + Assert.That(list, Is.Not.Null.And.Count.EqualTo(1), $"Unable to find entity with initial {propertyName}"); + } + + private static readonly string[] CharEnumInjections = + { + nameof(CharEnum.SingleQuote), nameof(CharEnum.Backslash) + }; + + [TestCaseSource(nameof(CharEnumInjections))] + public void SqlInjectionWithCharEnumAsync(string enumName) + { + using var session = OpenSession(); + + var query = session.CreateQuery($"from Entity e where e.CharacterEnum = CharEnum.{enumName}"); + IList list = null; + Assert.That(async () => list = await (query.ListAsync()), Throws.Nothing); + Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with CharacterEnum {enumName}"); + } + + private static readonly string[] UriInjections = + { + nameof(Entity.UriWithSingleQuote), nameof(Entity.UriWithEscapedSingleQuote) + }; + + [TestCaseSource(nameof(UriInjections))] + public void SqlInjectionWithUriAsync(string propertyName) + { + using var session = OpenSession(); + + var query = session.CreateQuery($"from Entity e where e.UriProperty = Entity.{propertyName}"); + IList list = null; + Assert.That(async () => list = await (query.ListAsync()), Throws.Nothing); + Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with UriProperty {propertyName}"); + } + + private static readonly string[] NumericalTypesInjections = + { + nameof(Entity.ByteProperty), + nameof(Entity.DecimalProperty), + nameof(Entity.DoubleProperty), + nameof(Entity.FloatProperty), + nameof(Entity.ShortProperty), + nameof(Entity.IntProperty), + nameof(Entity.LongProperty), + nameof(Entity.SByteProperty), + nameof(Entity.UShortProperty), + nameof(Entity.UIntProperty), + nameof(Entity.ULongProperty) + }; + + [TestCaseSource(nameof(NumericalTypesInjections))] + public async Task SqlInjectionInNumericalTypeAsync(string propertyName) + { + Assume.That(_unsupportedProperties, Does.Not.Contains((object) propertyName), $"The {propertyName} property is unsupported by the dialect"); + + Entity.ArbitraryStringValue = "0; drop table Entity; --"; + using (var session = OpenSession()) + { + IQuery query; + // Defining that query is invalid and should throw. + try + { + query = session.CreateQuery($"from Entity e where e.{propertyName} = Entity.{nameof(Entity.ArbitraryStringValue)}"); + } + catch (Exception ex) + { + // All good. + Assert.Pass($"The wicked query creation has been rejected, as it should: {ex}"); + // Needed for the compiler who does not know "Pass" always throw. + return; + } + + // The query definition has been accepted, run it. + try + { + await (query.ListAsync()); + } + catch (Exception ex) + { + // Expecting no exception at that point, but the test is to check if the injection succeeded. + Assert.Warn($"The wicked query execution has failed: {ex}"); + } + } + + // Check if we can still query Entity. If it succeeds, at least it means the injection failed. + using (var session = OpenSession()) + { + IList list = null; + Assert.That(async () => list = await (session.CreateQuery("from Entity e").ListAsync()), Throws.Nothing); + Assert.That(list, Has.Count.GreaterThan(0)); + } + } + + private static readonly string[] DateTypesInjections = + { + nameof(Entity.DateTimeProperty), + nameof(Entity.DateProperty), + nameof(Entity.DateTimeOffsetProperty), + nameof(Entity.TimeProperty) + }; + + [TestCaseSource(nameof(DateTypesInjections))] + public void SqlInjectionWithDatetimeAsync(string propertyName) + { + Assume.That(_unsupportedProperties, Does.Not.Contains((object) propertyName), $"The {propertyName} property is unsupported by the dialect"); + + var wickedCulture = new CultureInfo("en-US"); + if (propertyName == nameof(Entity.TimeProperty)) + wickedCulture.DateTimeFormat.ShortTimePattern = "HH:mm:ss\\'\"; drop table Entity; --\""; + else + wickedCulture.DateTimeFormat.ShortDatePattern = "yyyy-MM-ddTHH:mm:ss\\'\"; drop table Entity; --\""; + CultureInfo.CurrentCulture = wickedCulture; + CultureInfo.CurrentUICulture = wickedCulture; + + using var session = OpenSession(); + + var staticPropertyName = propertyName == nameof(Entity.DateTimeOffsetProperty) ? + nameof(Entity.StaticDateTimeOffsetProperty) : nameof(Entity.StaticDateProperty); + var query = session.CreateQuery($"from Entity e where e.{propertyName} = Entity.{staticPropertyName}"); + IList list = null; + Assume.That(() => list = query.List(), Throws.Nothing, + "The first execution of the query failed, the injection has likely failed"); + // Execute again to check the table is still here. + Assert.That(async () => list = await (query.ListAsync()), Throws.Nothing, + "The second execution of the query failed although the first one did not: the injection has succeeded"); + } + + private static readonly string[] GuidInjections = + { + Entity.NameWithSingleQuote, Entity.NameWithEscapedSingleQuote + }; + + [TestCaseSource(nameof(GuidInjections))] + public void SqlInjectionWithGuidAsync(string injection) + { + Entity.ArbitraryStringValue = $"{Guid.NewGuid()}{injection}"; + using var session = OpenSession(); + + var query = session.CreateQuery($"from Entity e where e.GuidProperty = Entity.{nameof(Entity.ArbitraryStringValue)}"); + IList list = null; + Assume.That(() => list = query.List(), Throws.Nothing, + "The first execution of the query failed, the injection has likely failed"); + // Execute again to check the table is still here. + Assert.That(async () => list = await (query.ListAsync()), Throws.Nothing, + "The second execution of the query failed although the first one did not: the injection has succeeded"); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3516/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3516/Entity.cs new file mode 100644 index 00000000000..0326e557526 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3516/Entity.cs @@ -0,0 +1,54 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.GH3516 +{ + public class Entity + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual char FirstChar { get; set; } + public virtual CharEnum CharacterEnum { get; set; } = CharEnum.SimpleChar; + public virtual Uri UriProperty { get; set; } + + public virtual byte ByteProperty { get; set; } + public virtual decimal DecimalProperty { get; set; } + public virtual double DoubleProperty { get; set; } + public virtual float FloatProperty { get; set; } + public virtual short ShortProperty { get; set; } + public virtual int IntProperty { get; set; } + public virtual long LongProperty { get; set; } + public virtual sbyte SByteProperty { get; set; } + public virtual ushort UShortProperty { get; set; } + public virtual uint UIntProperty { get; set; } + public virtual ulong ULongProperty { get; set; } + + public virtual DateTime DateTimeProperty { get; set; } = StaticDateProperty; + public virtual DateTime DateProperty { get; set; } = StaticDateProperty; + public virtual DateTimeOffset DateTimeOffsetProperty { get; set; } = StaticDateProperty; + public virtual DateTime TimeProperty { get; set; } = StaticDateProperty; + + public virtual Guid GuidProperty { get; set; } = Guid.Empty; + + public const string NameWithSingleQuote = "'; drop table Entity; --"; + public const string NameWithEscapedSingleQuote = @"\'; drop table Entity; --"; + + // Do not switch to property, the feature of referencing static fields in HQL does not work with properties. + public static string ArbitraryStringValue; + + public const char QuoteInitial = '\''; + public const char BackslashInitial = '\\'; + + public static readonly Uri UriWithSingleQuote = new("https://somewhere/?sql='; drop table Entity; --"); + public static readonly Uri UriWithEscapedSingleQuote = new(@"https://somewhere/?sql=\'; drop table Entity; --"); + + public static readonly DateTime StaticDateProperty = DateTime.Today; + public static readonly DateTimeOffset StaticDateTimeOffsetProperty = DateTimeOffset.Now; + } + + public enum CharEnum + { + SimpleChar = 'A', + SingleQuote = '\'', + Backslash = '\\' + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3516/FixtureByCode.cs b/src/NHibernate.Test/NHSpecificTest/GH3516/FixtureByCode.cs new file mode 100644 index 00000000000..235fad90b75 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3516/FixtureByCode.cs @@ -0,0 +1,388 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.SqlTypes; +using NHibernate.Type; +using NUnit.Framework; +using NUnit.Framework.Internal; + +namespace NHibernate.Test.NHSpecificTest.GH3516 +{ + [TestFixture] + public class FixtureByCode : TestCaseMappingByCode + { + + private readonly HashSet _unsupportedProperties = new(); + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Property(x => x.FirstChar); + rc.Property(x => x.CharacterEnum, m => m.Type>()); + rc.Property(x => x.UriProperty); + + rc.Property(x => x.ByteProperty); + rc.Property(x => x.DecimalProperty); + rc.Property(x => x.DoubleProperty); + rc.Property(x => x.FloatProperty); + rc.Property(x => x.ShortProperty); + rc.Property(x => x.IntProperty); + rc.Property(x => x.LongProperty); + + if (TestDialect.SupportsSqlType(SqlTypeFactory.SByte)) + rc.Property(x => x.SByteProperty); + else + _unsupportedProperties.Add(nameof(Entity.SByteProperty)); + + if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt16)) + rc.Property(x => x.UShortProperty); + else + _unsupportedProperties.Add(nameof(Entity.UShortProperty)); + + if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt32)) + rc.Property(x => x.UIntProperty); + else + _unsupportedProperties.Add(nameof(Entity.UIntProperty)); + + if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt64)) + rc.Property(x => x.ULongProperty); + else + _unsupportedProperties.Add(nameof(Entity.ULongProperty)); + + rc.Property(x => x.DateTimeProperty); + rc.Property(x => x.DateProperty, m => m.Type(NHibernateUtil.Date)); + if (TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet)) + rc.Property(x => x.DateTimeOffsetProperty); + else + _unsupportedProperties.Add(nameof(Entity.DateTimeOffsetProperty)); + rc.Property(x => x.TimeProperty, m => m.Type(NHibernateUtil.Time)); + + rc.Property(x => x.GuidProperty); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Discriminator(x => x.Column("StringDiscriminator")); + rc.Property(x => x.Name); + rc.Abstract(true); + }); + mapper.Subclass(rc => rc.DiscriminatorValue(Entity.NameWithSingleQuote)); + mapper.Subclass(rc => rc.DiscriminatorValue(Entity.NameWithEscapedSingleQuote)); + + mapper.Import(); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + private CultureInfo _backupCulture; + private CultureInfo _backupUICulture; + + protected override void OnSetUp() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + session.Save( + new Entity + { + Name = Entity.NameWithSingleQuote, + FirstChar = Entity.QuoteInitial, + CharacterEnum = CharEnum.SingleQuote, + UriProperty = Entity.UriWithSingleQuote + }); + session.Save( + new Entity + { + Name = Entity.NameWithEscapedSingleQuote, + FirstChar = Entity.BackslashInitial, + CharacterEnum = CharEnum.Backslash, + UriProperty = Entity.UriWithEscapedSingleQuote + }); + + transaction.Commit(); + + _backupCulture = CultureInfo.CurrentCulture; + _backupUICulture = CultureInfo.CurrentUICulture; + } + + protected override void OnTearDown() + { + if (_backupCulture != null) + { + CultureInfo.CurrentCulture = _backupCulture; + CultureInfo.CurrentUICulture = _backupUICulture; + } + + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + private static readonly string[] StringInjectionsProperties = + { + nameof(Entity.NameWithSingleQuote), nameof(Entity.NameWithEscapedSingleQuote) + }; + + [TestCaseSource(nameof(StringInjectionsProperties))] + public void SqlInjectionInStrings(string propertyName) + { + using var session = OpenSession(); + + var query = session.CreateQuery($"from Entity e where e.Name = Entity.{propertyName}"); + IList list = null; + Assert.That(() => list = query.List(), Throws.Nothing); + Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with name {propertyName}"); + } + + private static readonly string[] SpecialNames = + { + "\0; drop table Entity; --", + "\b; drop table Entity; --", + "\n; drop table Entity; --", + "\r; drop table Entity; --", + "\t; drop table Entity; --", + "\x1A; drop table Entity; --", + "\"; drop table Entity; --", + "\\; drop table Entity; --" + }; + + [TestCaseSource(nameof(SpecialNames))] + public void StringsWithSpecialCharacters(string name) + { + // We may not even be able to insert the entity. + var wasInserted = false; + try + { + using var s = OpenSession(); + using var t = s.BeginTransaction(); + var e = new Entity { Name = name }; + s.Save(e); + t.Commit(); + + wasInserted = true; + } + catch (Exception e) + { + Assert.Warn($"The entity insertion failed with message {e}"); + } + + try + { + using var session = OpenSession(); + Entity.ArbitraryStringValue = name; + var list = session.CreateQuery($"from Entity e where e.Name = Entity.{nameof(Entity.ArbitraryStringValue)}").List(); + if (wasInserted && list.Count != 1) + Assert.Warn($"Unable to find entity with name {nameof(Entity.ArbitraryStringValue)}"); + } + catch (Exception e) + { + Assert.Warn($"The query has failed with message {e}"); + } + + // Check the db is not wrecked. + if (wasInserted) + { + using var session = OpenSession(); + var list = session + .CreateQuery("from Entity e where e.Name = :name") + .SetString("name", name) + .List(); + Assert.That(list, Has.Count.EqualTo(1)); + } + else + { + using var session = OpenSession(); + var all = session.CreateQuery("from Entity e").List(); + Assert.That(all, Has.Count.GreaterThan(0)); + } + } + + [Test] + public void SqlInjectionInStringDiscriminator() + { + using var session = OpenSession(); + + session.Save(new Subclass1 { Name = "Subclass1" }); + session.Save(new Subclass2 { Name = "Subclass2" }); + + // ObjectToSQLString is used for generating the inserts. + Assert.That(session.Flush, Throws.Nothing, "Unable to flush the subclasses"); + + foreach (var entityName in new[] { nameof(Subclass1), nameof(Subclass2) }) + { + var query = session.CreateQuery($"from {entityName}"); + IList list = null; + Assert.That(() => list = query.List(), Throws.Nothing, $"Unable to list entities of {entityName}"); + Assert.That(list, Has.Count.EqualTo(1), $"Unable to find the {entityName} entity"); + } + } + + private static readonly string[] CharInjectionsProperties = + { + nameof(Entity.QuoteInitial), nameof(Entity.BackslashInitial) + }; + + [TestCaseSource(nameof(CharInjectionsProperties))] + public void SqlInjectionInChar(string propertyName) + { + using var session = OpenSession(); + var query = session.CreateQuery($"from Entity e where e.FirstChar = Entity.{propertyName}"); + IList list = null; + Assert.That(() => list = query.List(), Throws.Nothing); + Assert.That(list, Is.Not.Null.And.Count.EqualTo(1), $"Unable to find entity with initial {propertyName}"); + } + + private static readonly string[] CharEnumInjections = + { + nameof(CharEnum.SingleQuote), nameof(CharEnum.Backslash) + }; + + [TestCaseSource(nameof(CharEnumInjections))] + public void SqlInjectionWithCharEnum(string enumName) + { + using var session = OpenSession(); + + var query = session.CreateQuery($"from Entity e where e.CharacterEnum = CharEnum.{enumName}"); + IList list = null; + Assert.That(() => list = query.List(), Throws.Nothing); + Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with CharacterEnum {enumName}"); + } + + private static readonly string[] UriInjections = + { + nameof(Entity.UriWithSingleQuote), nameof(Entity.UriWithEscapedSingleQuote) + }; + + [TestCaseSource(nameof(UriInjections))] + public void SqlInjectionWithUri(string propertyName) + { + using var session = OpenSession(); + + var query = session.CreateQuery($"from Entity e where e.UriProperty = Entity.{propertyName}"); + IList list = null; + Assert.That(() => list = query.List(), Throws.Nothing); + Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with UriProperty {propertyName}"); + } + + private static readonly string[] NumericalTypesInjections = + { + nameof(Entity.ByteProperty), + nameof(Entity.DecimalProperty), + nameof(Entity.DoubleProperty), + nameof(Entity.FloatProperty), + nameof(Entity.ShortProperty), + nameof(Entity.IntProperty), + nameof(Entity.LongProperty), + nameof(Entity.SByteProperty), + nameof(Entity.UShortProperty), + nameof(Entity.UIntProperty), + nameof(Entity.ULongProperty) + }; + + [TestCaseSource(nameof(NumericalTypesInjections))] + public void SqlInjectionInNumericalType(string propertyName) + { + Assume.That(_unsupportedProperties, Does.Not.Contains((object) propertyName), $"The {propertyName} property is unsupported by the dialect"); + + Entity.ArbitraryStringValue = "0; drop table Entity; --"; + using (var session = OpenSession()) + { + IQuery query; + // Defining that query is invalid and should throw. + try + { + query = session.CreateQuery($"from Entity e where e.{propertyName} = Entity.{nameof(Entity.ArbitraryStringValue)}"); + } + catch (Exception ex) + { + // All good. + Assert.Pass($"The wicked query creation has been rejected, as it should: {ex}"); + // Needed for the compiler who does not know "Pass" always throw. + return; + } + + // The query definition has been accepted, run it. + try + { + query.List(); + } + catch (Exception ex) + { + // Expecting no exception at that point, but the test is to check if the injection succeeded. + Assert.Warn($"The wicked query execution has failed: {ex}"); + } + } + + // Check if we can still query Entity. If it succeeds, at least it means the injection failed. + using (var session = OpenSession()) + { + IList list = null; + Assert.That(() => list = session.CreateQuery("from Entity e").List(), Throws.Nothing); + Assert.That(list, Has.Count.GreaterThan(0)); + } + } + + private static readonly string[] DateTypesInjections = + { + nameof(Entity.DateTimeProperty), + nameof(Entity.DateProperty), + nameof(Entity.DateTimeOffsetProperty), + nameof(Entity.TimeProperty) + }; + + [TestCaseSource(nameof(DateTypesInjections))] + public void SqlInjectionWithDatetime(string propertyName) + { + Assume.That(_unsupportedProperties, Does.Not.Contains((object) propertyName), $"The {propertyName} property is unsupported by the dialect"); + + var wickedCulture = new CultureInfo("en-US"); + if (propertyName == nameof(Entity.TimeProperty)) + wickedCulture.DateTimeFormat.ShortTimePattern = "HH:mm:ss\\'\"; drop table Entity; --\""; + else + wickedCulture.DateTimeFormat.ShortDatePattern = "yyyy-MM-ddTHH:mm:ss\\'\"; drop table Entity; --\""; + CultureInfo.CurrentCulture = wickedCulture; + CultureInfo.CurrentUICulture = wickedCulture; + + using var session = OpenSession(); + + var staticPropertyName = propertyName == nameof(Entity.DateTimeOffsetProperty) ? + nameof(Entity.StaticDateTimeOffsetProperty) : nameof(Entity.StaticDateProperty); + var query = session.CreateQuery($"from Entity e where e.{propertyName} = Entity.{staticPropertyName}"); + IList list = null; + Assume.That(() => list = query.List(), Throws.Nothing, + "The first execution of the query failed, the injection has likely failed"); + // Execute again to check the table is still here. + Assert.That(() => list = query.List(), Throws.Nothing, + "The second execution of the query failed although the first one did not: the injection has succeeded"); + } + + private static readonly string[] GuidInjections = + { + Entity.NameWithSingleQuote, Entity.NameWithEscapedSingleQuote + }; + + [TestCaseSource(nameof(GuidInjections))] + public void SqlInjectionWithGuid(string injection) + { + Entity.ArbitraryStringValue = $"{Guid.NewGuid()}{injection}"; + using var session = OpenSession(); + + var query = session.CreateQuery($"from Entity e where e.GuidProperty = Entity.{nameof(Entity.ArbitraryStringValue)}"); + IList list = null; + Assume.That(() => list = query.List(), Throws.Nothing, + "The first execution of the query failed, the injection has likely failed"); + // Execute again to check the table is still here. + Assert.That(() => list = query.List(), Throws.Nothing, + "The second execution of the query failed although the first one did not: the injection has succeeded"); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3516/Hierarchy.cs b/src/NHibernate.Test/NHSpecificTest/GH3516/Hierarchy.cs new file mode 100644 index 00000000000..cb5dca25868 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3516/Hierarchy.cs @@ -0,0 +1,18 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.GH3516 +{ + public class BaseClass + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + } + + public class Subclass1 : BaseClass + { + } + + public class Subclass2 : BaseClass + { + } +} diff --git a/src/NHibernate.Test/SqlCommandTest/SqlInsertBuilderFixture.cs b/src/NHibernate.Test/SqlCommandTest/SqlInsertBuilderFixture.cs index ec1d5c0cb4e..ce076e007bd 100644 --- a/src/NHibernate.Test/SqlCommandTest/SqlInsertBuilderFixture.cs +++ b/src/NHibernate.Test/SqlCommandTest/SqlInsertBuilderFixture.cs @@ -56,7 +56,7 @@ public void Commented() insert.SetTableName("test_insert_builder"); #pragma warning disable CS0618 // Type or member is obsolete - insert.AddColumn("stringColumn", "aSQLValue", (ILiteralType)NHibernateUtil.String); + insert.AddColumn("stringColumn", "aSQLValue", (ILiteralType)NHibernateUtil.AnsiString); #pragma warning restore CS0618 // Type or member is obsolete insert.SetComment("Test insert"); string expectedSql = diff --git a/src/NHibernate.Test/TestDialect.cs b/src/NHibernate.Test/TestDialect.cs index 49b2f7e0f69..0d1de762ae3 100644 --- a/src/NHibernate.Test/TestDialect.cs +++ b/src/NHibernate.Test/TestDialect.cs @@ -105,7 +105,7 @@ public bool SupportsEmptyInsertsOrHasNonIdentityNativeGenerator /// public virtual bool SupportsNonDataBoundCondition => true; - public bool SupportsSqlType(SqlType sqlType) + public virtual bool SupportsSqlType(SqlType sqlType) { try { diff --git a/src/NHibernate.Test/TestDialects/Oracle10gTestDialect.cs b/src/NHibernate.Test/TestDialects/Oracle10gTestDialect.cs index 4a5fb8d2322..281676a6c71 100644 --- a/src/NHibernate.Test/TestDialects/Oracle10gTestDialect.cs +++ b/src/NHibernate.Test/TestDialects/Oracle10gTestDialect.cs @@ -1,4 +1,6 @@ -using System.Runtime.InteropServices; +using System.Data; +using System.Runtime.InteropServices; +using NHibernate.SqlTypes; namespace NHibernate.Test.TestDialects { @@ -15,6 +17,16 @@ public Oracle10gTestDialect(Dialect.Dialect dialect) : base(dialect) public override bool SupportsAggregateInSubSelect => true; + public override bool SupportsSqlType(SqlType sqlType) + { + // The Oracle dialects define types for DbType the Oracle driver does not support. + return sqlType.DbType switch + { + DbType.UInt16 or DbType.UInt32 or DbType.UInt64 => false, + _ => base.SupportsSqlType(sqlType) + }; + } + /// /// Canceling a query hangs under Linux with OracleManagedDataClientDriver 21.6.1. public override bool SupportsCancelQuery => !RuntimeInformation.IsOSPlatform(OSPlatform.Linux); diff --git a/src/NHibernate.Test/TestDialects/SapSQLAnywhere17TestDialect.cs b/src/NHibernate.Test/TestDialects/SapSQLAnywhere17TestDialect.cs index 35a35092ce3..ff3ba11b781 100644 --- a/src/NHibernate.Test/TestDialects/SapSQLAnywhere17TestDialect.cs +++ b/src/NHibernate.Test/TestDialects/SapSQLAnywhere17TestDialect.cs @@ -1,4 +1,7 @@ -namespace NHibernate.Test.TestDialects +using System.Data; +using NHibernate.SqlTypes; + +namespace NHibernate.Test.TestDialects { public class SapSQLAnywhere17TestDialect : TestDialect { @@ -40,8 +43,18 @@ public SapSQLAnywhere17TestDialect(Dialect.Dialect dialect) public override bool HasBrokenTypeInferenceOnSelectedParameters => true; /// - /// Does not support SELECT FOR UPDATE + /// Does not support SELECT FOR UPDATE. /// public override bool SupportsSelectForUpdate => false; + + public override bool SupportsSqlType(SqlType sqlType) + { + // The Anywhere dialects define types for DbType the Anywhere driver does not support. + return sqlType.DbType switch + { + DbType.SByte => false, + _ => base.SupportsSqlType(sqlType) + }; + } } } diff --git a/src/NHibernate/Async/Type/ByteType.cs b/src/NHibernate/Async/Type/ByteType.cs index c908d33432e..4ea953fa78d 100644 --- a/src/NHibernate/Async/Type/ByteType.cs +++ b/src/NHibernate/Async/Type/ByteType.cs @@ -12,6 +12,7 @@ using System.Collections; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; diff --git a/src/NHibernate/Async/Type/Int16Type.cs b/src/NHibernate/Async/Type/Int16Type.cs index 95cec493791..0d88cbdd280 100644 --- a/src/NHibernate/Async/Type/Int16Type.cs +++ b/src/NHibernate/Async/Type/Int16Type.cs @@ -10,11 +10,12 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; -using System.Collections.Generic; -using System.Data; namespace NHibernate.Type { diff --git a/src/NHibernate/Async/Type/Int32Type.cs b/src/NHibernate/Async/Type/Int32Type.cs index e7897e1eafc..592c61d3b4c 100644 --- a/src/NHibernate/Async/Type/Int32Type.cs +++ b/src/NHibernate/Async/Type/Int32Type.cs @@ -10,11 +10,12 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; -using System.Collections.Generic; -using System.Data; namespace NHibernate.Type { diff --git a/src/NHibernate/Async/Type/Int64Type.cs b/src/NHibernate/Async/Type/Int64Type.cs index a4dae360662..566b0e6b214 100644 --- a/src/NHibernate/Async/Type/Int64Type.cs +++ b/src/NHibernate/Async/Type/Int64Type.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; diff --git a/src/NHibernate/Async/Type/TicksType.cs b/src/NHibernate/Async/Type/TicksType.cs index 0ffb06b37f6..b6e003cd753 100644 --- a/src/NHibernate/Async/Type/TicksType.cs +++ b/src/NHibernate/Async/Type/TicksType.cs @@ -11,6 +11,7 @@ using System; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; diff --git a/src/NHibernate/Async/Type/TimeAsTimeSpanType.cs b/src/NHibernate/Async/Type/TimeAsTimeSpanType.cs index 185baa88eba..4ea5f86697d 100644 --- a/src/NHibernate/Async/Type/TimeAsTimeSpanType.cs +++ b/src/NHibernate/Async/Type/TimeAsTimeSpanType.cs @@ -10,11 +10,12 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; -using System.Collections.Generic; -using System.Data; namespace NHibernate.Type { diff --git a/src/NHibernate/Async/Type/TimeSpanType.cs b/src/NHibernate/Async/Type/TimeSpanType.cs index 0917413671c..9364c6599c6 100644 --- a/src/NHibernate/Async/Type/TimeSpanType.cs +++ b/src/NHibernate/Async/Type/TimeSpanType.cs @@ -10,11 +10,12 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; -using System.Collections.Generic; -using System.Data; namespace NHibernate.Type { diff --git a/src/NHibernate/Async/Type/UInt16Type.cs b/src/NHibernate/Async/Type/UInt16Type.cs index b30c8ee87a4..7384a0c8464 100644 --- a/src/NHibernate/Async/Type/UInt16Type.cs +++ b/src/NHibernate/Async/Type/UInt16Type.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; diff --git a/src/NHibernate/Async/Type/UInt32Type.cs b/src/NHibernate/Async/Type/UInt32Type.cs index b5aa62179c0..a1f3bf5838a 100644 --- a/src/NHibernate/Async/Type/UInt32Type.cs +++ b/src/NHibernate/Async/Type/UInt32Type.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; diff --git a/src/NHibernate/Async/Type/UInt64Type.cs b/src/NHibernate/Async/Type/UInt64Type.cs index 08e7c68a1e6..bee61c38a9c 100644 --- a/src/NHibernate/Async/Type/UInt64Type.cs +++ b/src/NHibernate/Async/Type/UInt64Type.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; diff --git a/src/NHibernate/Cfg/Environment.cs b/src/NHibernate/Cfg/Environment.cs index cc107802524..27da456948e 100644 --- a/src/NHibernate/Cfg/Environment.cs +++ b/src/NHibernate/Cfg/Environment.cs @@ -119,6 +119,12 @@ public static string Version /// Enable formatting of SQL logged to the console public const string FormatSql = "format_sql"; + /// + /// Indicates if the database needs to have backslash escaped in string literals. + /// + /// The default value is dialect dependent. + public const string EscapeBackslashInStrings = "escape_backslash_in_strings"; + // Since v5.0.1 [Obsolete("This setting has no usages and will be removed in a future version")] public const string UseGetGeneratedKeys = "jdbc.use_get_generated_keys"; diff --git a/src/NHibernate/Dialect/DB2Dialect.cs b/src/NHibernate/Dialect/DB2Dialect.cs index a558e6d313d..90668e26bae 100644 --- a/src/NHibernate/Dialect/DB2Dialect.cs +++ b/src/NHibernate/Dialect/DB2Dialect.cs @@ -5,6 +5,7 @@ using NHibernate.Dialect.Function; using NHibernate.Dialect.Schema; using NHibernate.SqlCommand; +using NHibernate.SqlTypes; namespace NHibernate.Dialect { @@ -296,6 +297,30 @@ public override string ForUpdateString public override long TimestampResolutionInTicks => 10L; // Microseconds. + /// + public override string ToStringLiteral(string value, SqlType type) + { + if (value == null) + throw new System.ArgumentNullException(nameof(value)); + if (type == null) + throw new System.ArgumentNullException(nameof(value)); + + // See https://www.ibm.com/docs/en/db2/11.5?topic=elements-constants#r0000731__title__7 + var literal = new StringBuilder(value); + var isUnicode = type.DbType == DbType.String || type.DbType == DbType.StringFixedLength; + if (isUnicode) + literal.Replace(@"\", @"\\"); + + literal + .Replace("'", "''") + .Insert(0, '\'') + .Append('\''); + + if (isUnicode) + literal.Insert(0, "U&"); + return literal.ToString(); + } + #region Overridden informational metadata public override bool SupportsNullInUnique => false; diff --git a/src/NHibernate/Dialect/Dialect.cs b/src/NHibernate/Dialect/Dialect.cs index 9a4db11880e..c76992e197c 100644 --- a/src/NHibernate/Dialect/Dialect.cs +++ b/src/NHibernate/Dialect/Dialect.cs @@ -208,6 +208,7 @@ public virtual void Configure(IDictionary settings) DefaultCastLength = PropertiesHelper.GetInt32(Environment.QueryDefaultCastLength, settings, 4000); DefaultCastPrecision = PropertiesHelper.GetByte(Environment.QueryDefaultCastPrecision, settings, null) ?? 29; DefaultCastScale = PropertiesHelper.GetByte(Environment.QueryDefaultCastScale, settings, null) ?? 10; + EscapeBackslashInStrings = PropertiesHelper.GetBoolean(Environment.EscapeBackslashInStrings, settings, EscapeBackslashInStrings); } #endregion @@ -1354,14 +1355,6 @@ public virtual CaseFragment CreateCaseFragment() return new ANSICaseFragment(this); } - /// The SQL literal value to which this database maps boolean values. - /// The boolean value - /// The appropriate SQL literal. - public virtual string ToBooleanValueString(bool value) - { - return value ? "1" : "0"; - } - internal static void ExtractColumnOrAliasNames(SqlString select, out List columnsOrAliases, out Dictionary aliasToColumn, out Dictionary columnToAlias) { columnsOrAliases = new List(); @@ -2076,6 +2069,55 @@ public virtual string ConvertQuotesForCatalogName(string catalogName) #endregion + #region Literals support + + /// The SQL literal value to which this database maps boolean values. + /// The boolean value. + /// The appropriate SQL literal. + public virtual string ToBooleanValueString(bool value) + => value ? "1" : "0"; + + /// + /// if the database needs to have backslash escaped in string literals. + /// + /// by default in the base dialect, to conform to SQL standard. + protected virtual bool EscapeBackslashInStrings { get; set; } + + /// + /// if the database needs to have Unicode literals prefixed by N. + /// + /// by default in the base dialect. + protected virtual bool UseNPrefixForUnicodeStrings => false; + + /// The SQL string literal value to which this database maps string values. + /// The string value. + /// The SQL type of the string value. + /// The appropriate SQL string literal. + /// Thrown if or + /// is . + public virtual string ToStringLiteral(string value, SqlType type) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + if (type == null) + throw new ArgumentNullException(nameof(type)); + + var literal = new StringBuilder(value); + if (EscapeBackslashInStrings) + literal.Replace(@"\", @"\\"); + + literal + .Replace("'", "''") + .Insert(0, '\'') + .Append('\''); + + if (UseNPrefixForUnicodeStrings && (type.DbType == DbType.String || type.DbType == DbType.StringFixedLength)) + literal.Insert(0, 'N'); + return literal.ToString(); + } + + #endregion + #region Union subclass support /// diff --git a/src/NHibernate/Dialect/IngresDialect.cs b/src/NHibernate/Dialect/IngresDialect.cs index f6197f328ae..9f6e8e7b4e6 100644 --- a/src/NHibernate/Dialect/IngresDialect.cs +++ b/src/NHibernate/Dialect/IngresDialect.cs @@ -62,6 +62,11 @@ public IngresDialect() /// public override int MaxAliasLength => 32; + /// + /// by default for Ingres, + /// . + protected override bool UseNPrefixForUnicodeStrings => true; + #region Overridden informational metadata public override bool SupportsEmptyInList => false; diff --git a/src/NHibernate/Dialect/MsSql2000Dialect.cs b/src/NHibernate/Dialect/MsSql2000Dialect.cs index 1aec544ed2d..d6fb0e16781 100644 --- a/src/NHibernate/Dialect/MsSql2000Dialect.cs +++ b/src/NHibernate/Dialect/MsSql2000Dialect.cs @@ -755,6 +755,10 @@ public override bool SupportsSqlBatches /// public override int? MaxNumberOfParameters => 2097; + /// + /// by default for SQL Server. + protected override bool UseNPrefixForUnicodeStrings => true; + #region Overridden informational metadata public override bool SupportsEmptyInList => false; diff --git a/src/NHibernate/Dialect/MySQLDialect.cs b/src/NHibernate/Dialect/MySQLDialect.cs index d7b4be07d63..d6cf67b8097 100644 --- a/src/NHibernate/Dialect/MySQLDialect.cs +++ b/src/NHibernate/Dialect/MySQLDialect.cs @@ -530,6 +530,16 @@ public override long TimestampResolutionInTicks /// public override bool SupportsConcurrentWritingConnectionsInSameTransaction => false; + /// + /// by default for MySQL, + /// . + protected override bool EscapeBackslashInStrings { get; set; } = true; + + /// + /// by default for MySQL, + /// . + protected override bool UseNPrefixForUnicodeStrings => true; + #region Overridden informational metadata public override bool SupportsEmptyInList => false; diff --git a/src/NHibernate/Dialect/Oracle8iDialect.cs b/src/NHibernate/Dialect/Oracle8iDialect.cs index 9bbe3e7dfb9..13b9b1298ae 100644 --- a/src/NHibernate/Dialect/Oracle8iDialect.cs +++ b/src/NHibernate/Dialect/Oracle8iDialect.cs @@ -102,6 +102,7 @@ public override void Configure(IDictionary settings) // If changing the default value, keep it in sync with OracleDataClientDriverBase.Configure. UseNPrefixedTypesForUnicode = PropertiesHelper.GetBoolean(Environment.OracleUseNPrefixedTypesForUnicode, settings, false); + RegisterCharacterTypeMappings(); RegisterFloatingPointTypeMappings(); } @@ -561,6 +562,10 @@ public override long TimestampResolutionInTicks /// public override int MaxAliasLength => 30; + /// + /// Returns the same value as . + protected override bool UseNPrefixForUnicodeStrings => UseNPrefixedTypesForUnicode; + #region Overridden informational metadata public override bool SupportsEmptyInList diff --git a/src/NHibernate/Dialect/SybaseASA9Dialect.cs b/src/NHibernate/Dialect/SybaseASA9Dialect.cs index dfae7baa471..e994c85ccb8 100644 --- a/src/NHibernate/Dialect/SybaseASA9Dialect.cs +++ b/src/NHibernate/Dialect/SybaseASA9Dialect.cs @@ -9,7 +9,7 @@ namespace NHibernate.Dialect { /// - /// An SQL dialect for Sybase Adaptive Server Anywhere 9.0 + /// An SQL dialect for Sybase Adaptive Server Anywhere 9.0. (Renamed SQL Anywhere from its version 10.) /// /// ///

@@ -188,5 +188,15 @@ private static int GetAfterSelectInsertPoint(SqlString sql) } return 0; } + + /// + /// by default for SQL Anywhere, + /// . + protected override bool EscapeBackslashInStrings { get; set; } = true; + + /// + /// by default for SQL Anywhere, + /// . + protected override bool UseNPrefixForUnicodeStrings => true; } } diff --git a/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs b/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs index 290e626671f..ac5aa44f8e0 100644 --- a/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs +++ b/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs @@ -969,5 +969,15 @@ public override IDataBaseSchema GetDataBaseSchema(DbConnection connection) /// /// SQL Anywhere has a micro-second resolution. public override long TimestampResolutionInTicks => 10L; + + /// + /// by default for SQL Anywhere, + /// . + protected override bool EscapeBackslashInStrings { get; set; } = true; + + /// + /// by default for SQL Anywhere, + /// . + protected override bool UseNPrefixForUnicodeStrings => true; } } diff --git a/src/NHibernate/Type/AbstractCharType.cs b/src/NHibernate/Type/AbstractCharType.cs index 5efb630c16c..d47e3c5b21c 100644 --- a/src/NHibernate/Type/AbstractCharType.cs +++ b/src/NHibernate/Type/AbstractCharType.cs @@ -51,9 +51,7 @@ public override void Set(DbCommand cmd, object value, int index, ISessionImpleme } public override string ObjectToSQLString(object value, Dialect.Dialect dialect) - { - return '\'' + value.ToString() + '\''; - } + => dialect.ToStringLiteral(value.ToString(), SqlType); // 6.0 TODO: rename "xml" parameter as "value": it is not a xml string. The fact it generally comes from a xml // attribute value is irrelevant to the method behavior. diff --git a/src/NHibernate/Type/AbstractDateTimeType.cs b/src/NHibernate/Type/AbstractDateTimeType.cs index 8f95323cb78..07c32a7ba8a 100644 --- a/src/NHibernate/Type/AbstractDateTimeType.cs +++ b/src/NHibernate/Type/AbstractDateTimeType.cs @@ -176,7 +176,7 @@ public override object FromStringValue(string xml) public override object DefaultValue => BaseDateValue; /// - public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => - "'" + (DateTime) value + "'"; + public override string ObjectToSQLString(object value, Dialect.Dialect dialect) + => dialect.ToStringLiteral(((DateTime) value).ToString(), SqlTypeFactory.GetAnsiString(50)); } } diff --git a/src/NHibernate/Type/AbstractStringType.cs b/src/NHibernate/Type/AbstractStringType.cs index 9d47aaf07ff..72949c1093c 100644 --- a/src/NHibernate/Type/AbstractStringType.cs +++ b/src/NHibernate/Type/AbstractStringType.cs @@ -134,10 +134,9 @@ public object StringToObject(string xml) #region ILiteralType Members + /// public string ObjectToSQLString(object value, Dialect.Dialect dialect) - { - return "'" + (string)value + "'"; - } + => dialect.ToStringLiteral((string)value, SqlType); #endregion diff --git a/src/NHibernate/Type/ByteType.cs b/src/NHibernate/Type/ByteType.cs index 7c90bd6738a..c780427b2ae 100644 --- a/src/NHibernate/Type/ByteType.cs +++ b/src/NHibernate/Type/ByteType.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -54,7 +55,7 @@ public override string Name public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((byte)value).ToString(CultureInfo.InvariantCulture); } // 6.0 TODO: rename "xml" parameter as "value": it is not a xml string. The fact it generally comes from a xml diff --git a/src/NHibernate/Type/CharBooleanType.cs b/src/NHibernate/Type/CharBooleanType.cs index 9cde87c6697..c721640ef09 100644 --- a/src/NHibernate/Type/CharBooleanType.cs +++ b/src/NHibernate/Type/CharBooleanType.cs @@ -3,7 +3,6 @@ using System.Data.Common; using NHibernate.Engine; using NHibernate.SqlTypes; -using NHibernate.Util; namespace NHibernate.Type { @@ -57,9 +56,7 @@ private string ToCharacter(object value) } public override string ObjectToSQLString(object value, Dialect.Dialect dialect) - { - return "'" + ToCharacter(value) + "'"; - } + => dialect.ToStringLiteral(ToCharacter(value), SqlType); // 6.0 TODO: rename "xml" parameter as "value": it is not a xml string. The fact it generally comes from a xml // attribute value is irrelevant to the method behavior. diff --git a/src/NHibernate/Type/DateTimeOffSetType.cs b/src/NHibernate/Type/DateTimeOffSetType.cs index 37837aec289..78a9c80661b 100644 --- a/src/NHibernate/Type/DateTimeOffSetType.cs +++ b/src/NHibernate/Type/DateTimeOffSetType.cs @@ -155,8 +155,6 @@ public override object FromStringValue(string xml) } public override string ObjectToSQLString(object value, Dialect.Dialect dialect) - { - return "'" + ((DateTimeOffset) value) + "'"; - } + => dialect.ToStringLiteral(((DateTimeOffset) value).ToString(), SqlTypeFactory.GetAnsiString(50)); } } diff --git a/src/NHibernate/Type/DateType.cs b/src/NHibernate/Type/DateType.cs index 76d3fbb99e9..1a3bac85c5c 100644 --- a/src/NHibernate/Type/DateType.cs +++ b/src/NHibernate/Type/DateType.cs @@ -93,8 +93,8 @@ public override string ToString(object val) => public override object DefaultValue => customBaseDate; /// - public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => - "\'" + ((DateTime)value).ToShortDateString() + "\'"; + public override string ObjectToSQLString(object value, Dialect.Dialect dialect) + => dialect.ToStringLiteral(((DateTime) value).ToShortDateString(), SqlTypeFactory.GetAnsiString(50)); // Since v5 [Obsolete("Its only parameter, BaseValue, is obsolete.")] diff --git a/src/NHibernate/Type/DecimalType.cs b/src/NHibernate/Type/DecimalType.cs index 158fa028fc7..df6f0b60f4a 100644 --- a/src/NHibernate/Type/DecimalType.cs +++ b/src/NHibernate/Type/DecimalType.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -66,7 +67,7 @@ public override object FromStringValue(string xml) public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((decimal)value).ToString(CultureInfo.InvariantCulture); } // 6.0 TODO: rename "xml" parameter as "value": it is not a xml string. The fact it generally comes from a xml diff --git a/src/NHibernate/Type/DoubleType.cs b/src/NHibernate/Type/DoubleType.cs index 4a8cf3406a9..b7df9359bb9 100644 --- a/src/NHibernate/Type/DoubleType.cs +++ b/src/NHibernate/Type/DoubleType.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -66,7 +67,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((double)value).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/NHibernate/Type/EnumCharType.cs b/src/NHibernate/Type/EnumCharType.cs index ece3684ff6f..d73490ff19e 100644 --- a/src/NHibernate/Type/EnumCharType.cs +++ b/src/NHibernate/Type/EnumCharType.cs @@ -171,8 +171,6 @@ public override object FromStringValue(string xml) } public override string ObjectToSQLString(object value, Dialect.Dialect dialect) - { - return '\'' + GetValue(value).ToString() + '\''; - } + => dialect.ToStringLiteral(GetValue(value).ToString(), SqlType); } } diff --git a/src/NHibernate/Type/GuidType.cs b/src/NHibernate/Type/GuidType.cs index a883f3a6b5a..70d26f1d950 100644 --- a/src/NHibernate/Type/GuidType.cs +++ b/src/NHibernate/Type/GuidType.cs @@ -88,7 +88,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return "'" + value + "'"; + return dialect.ToStringLiteral(value.ToString(), SqlTypeFactory.GetAnsiString(50)); } } } diff --git a/src/NHibernate/Type/Int16Type.cs b/src/NHibernate/Type/Int16Type.cs index f517be93424..af018c60ab0 100644 --- a/src/NHibernate/Type/Int16Type.cs +++ b/src/NHibernate/Type/Int16Type.cs @@ -1,10 +1,11 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; -using System.Collections.Generic; -using System.Data; namespace NHibernate.Type { @@ -114,7 +115,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((short)value).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/NHibernate/Type/Int32Type.cs b/src/NHibernate/Type/Int32Type.cs index 9602842ae3b..2ec84eecf19 100644 --- a/src/NHibernate/Type/Int32Type.cs +++ b/src/NHibernate/Type/Int32Type.cs @@ -1,10 +1,11 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; -using System.Collections.Generic; -using System.Data; namespace NHibernate.Type { @@ -114,7 +115,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((int)value).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/NHibernate/Type/Int64Type.cs b/src/NHibernate/Type/Int64Type.cs index 5bad9a7513b..64a6cf37a63 100644 --- a/src/NHibernate/Type/Int64Type.cs +++ b/src/NHibernate/Type/Int64Type.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -114,7 +115,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((long)value).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/NHibernate/Type/SByteType.cs b/src/NHibernate/Type/SByteType.cs index 55021fb22a6..f3cbefa7503 100644 --- a/src/NHibernate/Type/SByteType.cs +++ b/src/NHibernate/Type/SByteType.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -116,7 +117,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((sbyte)value).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/NHibernate/Type/SingleType.cs b/src/NHibernate/Type/SingleType.cs index 70ca434e04d..4d299bb1c17 100644 --- a/src/NHibernate/Type/SingleType.cs +++ b/src/NHibernate/Type/SingleType.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -91,7 +92,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((float)value).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/NHibernate/Type/TicksType.cs b/src/NHibernate/Type/TicksType.cs index 4fc18a007cd..bc82d101df8 100644 --- a/src/NHibernate/Type/TicksType.cs +++ b/src/NHibernate/Type/TicksType.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -100,7 +101,7 @@ public override object Seed(ISessionImplementor session) /// public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return '\'' + ((DateTime)value).Ticks.ToString() + '\''; + return '\'' + ((DateTime)value).Ticks.ToString(CultureInfo.InvariantCulture) + '\''; } } } diff --git a/src/NHibernate/Type/TimeAsTimeSpanType.cs b/src/NHibernate/Type/TimeAsTimeSpanType.cs index e525ecfa555..fd7bd7058c7 100644 --- a/src/NHibernate/Type/TimeAsTimeSpanType.cs +++ b/src/NHibernate/Type/TimeAsTimeSpanType.cs @@ -1,10 +1,11 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; -using System.Collections.Generic; -using System.Data; namespace NHibernate.Type { @@ -141,7 +142,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return '\'' + ((TimeSpan)value).Ticks.ToString() + '\''; + return '\'' + ((TimeSpan)value).Ticks.ToString(CultureInfo.InvariantCulture) + '\''; } } } diff --git a/src/NHibernate/Type/TimeSpanType.cs b/src/NHibernate/Type/TimeSpanType.cs index 5ca576454b9..1ce300af06b 100644 --- a/src/NHibernate/Type/TimeSpanType.cs +++ b/src/NHibernate/Type/TimeSpanType.cs @@ -1,10 +1,11 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; -using System.Collections.Generic; -using System.Data; namespace NHibernate.Type { @@ -128,7 +129,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return '\'' + ((TimeSpan)value).Ticks.ToString() + '\''; + return '\'' + ((TimeSpan)value).Ticks.ToString(CultureInfo.InvariantCulture) + '\''; } } } diff --git a/src/NHibernate/Type/TimeType.cs b/src/NHibernate/Type/TimeType.cs index be487f81cff..b808619ae70 100644 --- a/src/NHibernate/Type/TimeType.cs +++ b/src/NHibernate/Type/TimeType.cs @@ -171,8 +171,6 @@ public override object DefaultValue } public override string ObjectToSQLString(object value, Dialect.Dialect dialect) - { - return "'" + ((DateTime)value).ToShortTimeString() + "'"; - } + => dialect.ToStringLiteral(((DateTime) value).ToShortTimeString(), SqlTypeFactory.GetAnsiString(50)); } } diff --git a/src/NHibernate/Type/UInt16Type.cs b/src/NHibernate/Type/UInt16Type.cs index 10e9d15cd9b..90421392d8f 100644 --- a/src/NHibernate/Type/UInt16Type.cs +++ b/src/NHibernate/Type/UInt16Type.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -114,7 +115,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((ushort)value).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/NHibernate/Type/UInt32Type.cs b/src/NHibernate/Type/UInt32Type.cs index 0590278ef68..897dca99a19 100644 --- a/src/NHibernate/Type/UInt32Type.cs +++ b/src/NHibernate/Type/UInt32Type.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -114,7 +115,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((uint)value).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/NHibernate/Type/UInt64Type.cs b/src/NHibernate/Type/UInt64Type.cs index a902b6d46fe..5665a1077d1 100644 --- a/src/NHibernate/Type/UInt64Type.cs +++ b/src/NHibernate/Type/UInt64Type.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -113,7 +114,7 @@ public override object DefaultValue public override string ObjectToSQLString(object value, Dialect.Dialect dialect) { - return value.ToString(); + return ((ulong)value).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/NHibernate/Type/UriType.cs b/src/NHibernate/Type/UriType.cs index 68f319606d3..100c3704a35 100644 --- a/src/NHibernate/Type/UriType.cs +++ b/src/NHibernate/Type/UriType.cs @@ -84,9 +84,7 @@ public override object FromStringValue(string xml) } public string ObjectToSQLString(object value, Dialect.Dialect dialect) - { - return "'" + ((Uri)value).OriginalString + "'"; - } + => dialect.ToStringLiteral(((Uri) value).OriginalString, SqlType); /// public override object Assemble(object cached, ISessionImplementor session, object owner) diff --git a/src/NHibernate/nhibernate-configuration.xsd b/src/NHibernate/nhibernate-configuration.xsd index 08d922ad963..123b2c35e60 100644 --- a/src/NHibernate/nhibernate-configuration.xsd +++ b/src/NHibernate/nhibernate-configuration.xsd @@ -176,6 +176,14 @@ + + + + Indicates if the database needs to have backslash escaped in string literals. The default is + dialect dependent. + + +