diff --git a/ChangeLog/7.1.5_dev.txt b/ChangeLog/7.1.5_dev.txt index 6d8d30bd68..ca609b4728 100644 --- a/ChangeLog/7.1.5_dev.txt +++ b/ChangeLog/7.1.5_dev.txt @@ -1 +1,3 @@ -[main] Addressed certain cases of overlaping server-side error by new one when temporary tables are used in query \ No newline at end of file +[main] Addressed certain cases of overlaping server-side error by new one when temporary tables are used in query +[postgresql] Improved .Milliseconds translation for types which have this part +[postgresql] Improved TimeSpan.TotalMilliseconds translation \ No newline at end of file diff --git a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Compiler.cs b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Compiler.cs index 1ccbf10bf2..9534747fd2 100644 --- a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Compiler.cs +++ b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Compiler.cs @@ -122,7 +122,7 @@ public override void Visit(SqlFunctionCall node) ((node.Arguments[0] / SqlDml.Literal(nanosecondsPerSecond)) * OneSecondInterval).AcceptVisitor(this); return; case SqlFunctionType.IntervalToMilliseconds: - SqlHelper.IntervalToMilliseconds(node.Arguments[0]).AcceptVisitor(this); + VisitIntervalToMilliseconds(node); return; case SqlFunctionType.IntervalToNanoseconds: SqlHelper.IntervalToNanoseconds(node.Arguments[0]).AcceptVisitor(this); @@ -296,6 +296,11 @@ public override void Visit(SqlCustomFunctionCall node) base.Visit(node); } + protected virtual void VisitIntervalToMilliseconds(SqlFunctionCall node) + { + SqlHelper.IntervalToMilliseconds(node.Arguments[0]).AcceptVisitor(this); + } + private static SqlExpression DateTimeToStringIso(SqlExpression dateTime, in string isoFormat) => SqlDml.FunctionCall("TO_CHAR", dateTime, isoFormat); diff --git a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Translator.cs b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Translator.cs index 0073a33d09..c355052273 100644 --- a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Translator.cs +++ b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Translator.cs @@ -410,11 +410,11 @@ public override void Translate(SqlCompilerContext context, SqlExtract node, Extr } switch (section) { case ExtractSection.Entry: - _ = context.Output.AppendOpeningPunctuation(isSecond ? "(trunc(extract(" : "(extract("); + _ = context.Output.AppendOpeningPunctuation(isSecond || isMillisecond ? "(trunc(extract(" : "(extract("); break; case ExtractSection.Exit: _ = context.Output.Append(isMillisecond - ? ")::int8 % 1000)" + ? "))::int8 % 1000)" : isSecond ? ")))" : ")::int8)" ); break; diff --git a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v9_0/Compiler.cs b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v9_0/Compiler.cs index 95509ae6a9..093fd128a2 100644 --- a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v9_0/Compiler.cs +++ b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v9_0/Compiler.cs @@ -4,11 +4,21 @@ // Created by: Denis Krjuchkov // Created: 2012.06.06 +using Xtensive.Sql.Dml; + namespace Xtensive.Sql.Drivers.PostgreSql.v9_0 { internal class Compiler : v8_4.Compiler { // Constructors + protected override void VisitIntervalToMilliseconds(SqlFunctionCall node) + { + AppendSpaceIfNecessary(); + _ = context.Output.Append("(TRUNC(EXTRACT(EPOCH FROM ("); + node.Arguments[0].AcceptVisitor(this); + _ = context.Output.Append(")) * 1000))"); + + } public Compiler(SqlDriver driver) : base(driver) diff --git a/Orm/Xtensive.Orm.Tests.Sql/PostgreSql/IntervalToMillisecondsTest.cs b/Orm/Xtensive.Orm.Tests.Sql/PostgreSql/IntervalToMillisecondsTest.cs new file mode 100644 index 0000000000..7a9d84d834 --- /dev/null +++ b/Orm/Xtensive.Orm.Tests.Sql/PostgreSql/IntervalToMillisecondsTest.cs @@ -0,0 +1,198 @@ +// Copyright (C) 2025 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. + +using System; +using NUnit.Framework; +using Xtensive.Sql; +using Xtensive.Sql.Dml; + +namespace Xtensive.Orm.Tests.Sql.PostgreSql +{ + public sealed class IntervalToMillisecondsTest : SqlTest + { + private const string IdColumnName = "Id"; + private const string ValueColumnName = "Value"; + private const string TableName = "IntervalToMsTest"; + + private TypeMapping longMapping; + private TypeMapping timeSpanMapping; + private TypeMapping doubleMapping; + + private SqlSelect selectQuery; + + private static TimeSpan[] TestValues + { + get => new[] { + TimeSpan.MinValue, + TimeSpan.MaxValue, + TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(1)), + TimeSpan.FromMinutes(10).Add(TimeSpan.FromSeconds(10)), + TimeSpan.FromMinutes(15).Add(TimeSpan.FromSeconds(15)), + TimeSpan.FromMinutes(27).Add(TimeSpan.FromSeconds(27)), + TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)), + TimeSpan.FromMinutes(43).Add(TimeSpan.FromSeconds(43)), + TimeSpan.FromMinutes(55).Add(TimeSpan.FromSeconds(55)), + TimeSpan.FromMinutes(59).Add(TimeSpan.FromSeconds(59)), + TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(1))), + TimeSpan.FromHours(10).Add(TimeSpan.FromMinutes(10).Add(TimeSpan.FromSeconds(10))), + TimeSpan.FromHours(15).Add(TimeSpan.FromMinutes(15).Add(TimeSpan.FromSeconds(15))), + TimeSpan.FromHours(20).Add(TimeSpan.FromMinutes(27).Add(TimeSpan.FromSeconds(27))), + TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30))), + TimeSpan.FromDays(1).Add(TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(1)))), + TimeSpan.FromDays(30).Add(TimeSpan.FromHours(10).Add(TimeSpan.FromMinutes(10).Add(TimeSpan.FromSeconds(10)))), + TimeSpan.FromDays(15).Add(TimeSpan.FromHours(15).Add(TimeSpan.FromMinutes(15).Add(TimeSpan.FromSeconds(15)))), + TimeSpan.FromDays(20).Add(TimeSpan.FromHours(20).Add(TimeSpan.FromMinutes(27).Add(TimeSpan.FromSeconds(27)))), + TimeSpan.FromDays(23).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + TimeSpan.FromDays(28).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + TimeSpan.FromDays(29).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + TimeSpan.FromDays(32).Add(TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(1)))), + TimeSpan.FromDays(40).Add(TimeSpan.FromHours(10).Add(TimeSpan.FromMinutes(10).Add(TimeSpan.FromSeconds(10)))), + TimeSpan.FromDays(65).Add(TimeSpan.FromHours(15).Add(TimeSpan.FromMinutes(15).Add(TimeSpan.FromSeconds(15)))), + TimeSpan.FromDays(181).Add(TimeSpan.FromHours(20).Add(TimeSpan.FromMinutes(27).Add(TimeSpan.FromSeconds(27)))), + TimeSpan.FromDays(182).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + TimeSpan.FromDays(360).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + TimeSpan.FromDays(363).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + TimeSpan.FromDays(364).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + TimeSpan.FromDays(365).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + TimeSpan.FromDays(366).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + TimeSpan.FromDays(730).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))), + + TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(1)).Negate(), + TimeSpan.FromMinutes(10).Add(TimeSpan.FromSeconds(10)).Negate(), + TimeSpan.FromMinutes(15).Add(TimeSpan.FromSeconds(15)).Negate(), + TimeSpan.FromMinutes(27).Add(TimeSpan.FromSeconds(27)).Negate(), + TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)).Negate(), + TimeSpan.FromMinutes(43).Add(TimeSpan.FromSeconds(43)).Negate(), + TimeSpan.FromMinutes(55).Add(TimeSpan.FromSeconds(55)).Negate(), + TimeSpan.FromMinutes(59).Add(TimeSpan.FromSeconds(59)).Negate(), + TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(1))).Negate(), + TimeSpan.FromHours(10).Add(TimeSpan.FromMinutes(10).Add(TimeSpan.FromSeconds(10))).Negate(), + TimeSpan.FromHours(15).Add(TimeSpan.FromMinutes(15).Add(TimeSpan.FromSeconds(15))).Negate(), + TimeSpan.FromHours(20).Add(TimeSpan.FromMinutes(27).Add(TimeSpan.FromSeconds(27))).Negate(), + TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30))).Negate(), + TimeSpan.FromDays(1).Add(TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(1)))).Negate(), + TimeSpan.FromDays(30).Add(TimeSpan.FromHours(10).Add(TimeSpan.FromMinutes(10).Add(TimeSpan.FromSeconds(10)))).Negate(), + TimeSpan.FromDays(15).Add(TimeSpan.FromHours(15).Add(TimeSpan.FromMinutes(15).Add(TimeSpan.FromSeconds(15)))).Negate(), + TimeSpan.FromDays(20).Add(TimeSpan.FromHours(20).Add(TimeSpan.FromMinutes(27).Add(TimeSpan.FromSeconds(27)))).Negate(), + TimeSpan.FromDays(23).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate(), + TimeSpan.FromDays(28).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate(), + TimeSpan.FromDays(29).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate(), + TimeSpan.FromDays(32).Add(TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(1)))).Negate(), + TimeSpan.FromDays(40).Add(TimeSpan.FromHours(10).Add(TimeSpan.FromMinutes(10).Add(TimeSpan.FromSeconds(10)))).Negate(), + TimeSpan.FromDays(65).Add(TimeSpan.FromHours(15).Add(TimeSpan.FromMinutes(15).Add(TimeSpan.FromSeconds(15)))).Negate(), + TimeSpan.FromDays(181).Add(TimeSpan.FromHours(20).Add(TimeSpan.FromMinutes(27).Add(TimeSpan.FromSeconds(27)))).Negate(), + TimeSpan.FromDays(182).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate(), + TimeSpan.FromDays(360).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate(), + TimeSpan.FromDays(363).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate(), + TimeSpan.FromDays(364).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate(), + TimeSpan.FromDays(365).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate(), + TimeSpan.FromDays(366).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate(), + TimeSpan.FromDays(730).Add(TimeSpan.FromHours(23).Add(TimeSpan.FromMinutes(30).Add(TimeSpan.FromSeconds(30)))).Negate() + }; + } + + protected override void CheckRequirements() => Require.ProviderIs(StorageProvider.PostgreSql); + + protected override void TestFixtureSetUp() + { + base.TestFixtureSetUp(); + + longMapping = Driver.TypeMappings[typeof(long)]; + timeSpanMapping = Driver.TypeMappings[typeof(TimeSpan)]; + doubleMapping = Driver.TypeMappings[typeof(double)]; + + var dropTableCommand = Connection + .CreateCommand( + $"DROP TABLE IF EXISTS \"{TableName}\";"); + using (dropTableCommand) { + _ = dropTableCommand.ExecuteNonQuery(); + } + + var createTableCommand = Connection + .CreateCommand( + $"CREATE TABLE IF NOT EXISTS \"{TableName}\" (\"{IdColumnName}\" bigint CONSTRAINT PK_{TableName} PRIMARY KEY, \"{ValueColumnName}\" interval);"); + using (createTableCommand) { + _ = createTableCommand.ExecuteNonQuery(); + } + + var schema = ExtractDefaultSchema(); + var tableRef = SqlDml.TableRef(schema.Tables[TableName]); + var selectTotalMsQuery = SqlDml.Select(tableRef); + selectTotalMsQuery.Columns.Add(tableRef[IdColumnName], "id"); + selectTotalMsQuery.Columns.Add(tableRef[ValueColumnName], "timespan"); + selectTotalMsQuery.Columns.Add(SqlDml.IntervalToMilliseconds(tableRef[ValueColumnName]), "totalMs"); + selectTotalMsQuery.Where = tableRef[IdColumnName] == SqlDml.ParameterRef("pId"); + selectQuery = selectTotalMsQuery; + } + + protected override void TestFixtureTearDown() + { + longMapping = null; + timeSpanMapping = null; + doubleMapping = null; + selectQuery = null; + + base.TestFixtureTearDown(); + } + + + [Test] + [TestCaseSource(nameof(TestValues))] + public void MainTest(TimeSpan testCase) + { + TestValue(testCase); + } + + + private void TestValue(TimeSpan testCase) + { + InsertValue(testCase.Ticks, testCase); + var rowFromDb = SelectValue(testCase.Ticks); + var trueTotalMilliseconds = testCase.TotalMilliseconds; + var databaseValueTotalMilliseconds = rowFromDb.Item2.TotalMilliseconds; + var extractedTotalMilliseconds = rowFromDb.Item3; + + Assert.That(databaseValueTotalMilliseconds, Is.EqualTo(trueTotalMilliseconds)); + Assert.That(extractedTotalMilliseconds, Is.EqualTo(trueTotalMilliseconds)); + } + + private void InsertValue(long id, TimeSpan testCase) + { + var command = Connection.CreateCommand($"INSERT INTO \"{TableName}\"(\"{IdColumnName}\", \"{ValueColumnName}\") VALUES (@pId, @pValue)"); + var pId = Connection.CreateParameter(); + pId.ParameterName = "pId"; + longMapping.BindValue(pId, id); + _ = command.Parameters.Add(pId); + + var pValue = Connection.CreateParameter(); + pValue.ParameterName = "pValue"; + timeSpanMapping.BindValue(pValue, testCase); + _ = command.Parameters.Add(pValue); + using (command) { + _ = command.ExecuteNonQuery(); + } + } + + private (long, TimeSpan, double) SelectValue(long id) + { + var command = Connection.CreateCommand(selectQuery); + var pId = Connection.CreateParameter(); + pId.ParameterName = "pId"; + longMapping.BindValue(pId, id); + _ = command.Parameters.Add(pId); + + using (command) + using (var reader = command.ExecuteReader()) { + while (reader.Read()) { + var idFromDb = (long) longMapping.ReadValue(reader, 0); + var valueFromDb = (TimeSpan) timeSpanMapping.ReadValue(reader, 1); + var totalMs = (double) doubleMapping.ReadValue(reader, 2); + return (idFromDb, valueFromDb, totalMs); + } + } + + return default; + } + } +} diff --git a/Orm/Xtensive.Orm/Sql/Dml/Expressions/SqlExtract.cs b/Orm/Xtensive.Orm/Sql/Dml/Expressions/SqlExtract.cs index f190db218d..aa11172d45 100644 --- a/Orm/Xtensive.Orm/Sql/Dml/Expressions/SqlExtract.cs +++ b/Orm/Xtensive.Orm/Sql/Dml/Expressions/SqlExtract.cs @@ -65,9 +65,6 @@ public override void ReplaceWith(SqlExpression expression) internalValue = replacingExpression.internalValue; typeMarker = replacingExpression.typeMarker; typeHasTime = replacingExpression.typeHasTime; - //DateTimePart = replacingExpression.DateTimePart; - //DateTimeOffsetPart = replacingExpression.DateTimeOffsetPart; - //IntervalPart = replacingExpression.IntervalPart; Operand = replacingExpression.Operand; } @@ -75,11 +72,6 @@ internal override object Clone(SqlNodeCloneContext context) => context.NodeMapping.TryGetValue(this, out var clone) ? clone : context.NodeMapping[this] = new SqlExtract(internalValue, typeMarker, (SqlExpression)Operand.Clone(context)); - //DateTimePart!=SqlDateTimePart.Nothing - //? new SqlExtract(DateTimePart, (SqlExpression) Operand.Clone(context)) - //: IntervalPart!=SqlIntervalPart.Nothing - // ? new SqlExtract(IntervalPart, (SqlExpression) Operand.Clone(context)) - // : new SqlExtract(DateTimeOffsetPart, (SqlExpression) Operand.Clone(context)); public override void AcceptVisitor(ISqlVisitor visitor) { @@ -94,10 +86,6 @@ internal SqlExtract(SqlDateTimePart dateTimePart, SqlExpression operand) internalValue = dateTimePart.ToDtoPartFast(); typeMarker = DateTimeTypeId; typeHasTime = true; - - //DateTimePart = dateTimePart; - //DateTimeOffsetPart = SqlDateTimeOffsetPart.Nothing; - //IntervalPart = SqlIntervalPart.Nothing; Operand = operand; } @@ -107,10 +95,6 @@ internal SqlExtract(SqlIntervalPart intervalPart, SqlExpression operand) internalValue = intervalPart.ToDtoPartFast(); typeMarker = IntervalTypeId; typeHasTime = true; - - //DateTimePart = SqlDateTimePart.Nothing; - //DateTimeOffsetPart = SqlDateTimeOffsetPart.Nothing; - //IntervalPart = intervalPart; Operand = operand; }