This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 7028e1ed7b IGNITE-21604 .NET: Pass client time zone to server (#3742)
7028e1ed7b is described below

commit 7028e1ed7b4be67a423bec6b7b93e6764e6e645b
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Mon May 13 16:26:39 2024 +0300

    IGNITE-21604 .NET: Pass client time zone to server (#3742)
    
    Add `string TimeZoneId` to `SqlStatement`, propagate to server.
    
    We use `string` type so that the user is free to get this ID from any API, 
such as `TimeZoneInfo` from the standard library or `DateTimeZone` from 
NodaTime.
---
 .../dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs     | 88 +++++++++++++++++++++-
 .../dotnet/Apache.Ignite/Internal/Sql/Sql.cs       |  3 +-
 .../dotnet/Apache.Ignite/Sql/SqlStatement.cs       | 33 +++++++-
 3 files changed, 121 insertions(+), 3 deletions(-)

diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
index c51560b9a2..d9e9cea5dd 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
@@ -19,11 +19,15 @@ namespace Apache.Ignite.Tests.Sql
 {
     using System;
     using System.Collections.Generic;
+    using System.Collections.ObjectModel;
     using System.Diagnostics.CodeAnalysis;
     using System.Linq;
     using System.Threading.Tasks;
     using Ignite.Sql;
     using Ignite.Table;
+    using Internal.Common;
+    using Microsoft.Extensions.Logging.Abstractions;
+    using NodaTime;
     using NUnit.Framework;
 
     /// <summary>
@@ -395,6 +399,7 @@ namespace Apache.Ignite.Tests.Sql
                 timeout: TimeSpan.FromSeconds(123),
                 schema: "schema-1",
                 pageSize: 987,
+                timeZoneId: "Europe/London",
                 properties: new Dictionary<string, object?> { { "prop1", 10 }, 
{ "prop-2", "xyz" } });
 
             await using var res = await client.Sql.ExecuteAsync(null, 
sqlStatement);
@@ -410,6 +415,7 @@ namespace Apache.Ignite.Tests.Sql
             Assert.AreEqual("SELECT PROPS", props["sql"]);
             Assert.AreEqual("10", props["prop1"]);
             Assert.AreEqual("xyz", props["prop-2"]);
+            Assert.AreEqual("Europe/London", props["timeZoneId"]);
         }
 
         [Test]
@@ -476,7 +482,8 @@ namespace Apache.Ignite.Tests.Sql
                 timeout: TimeSpan.FromSeconds(123),
                 schema: "schema-1",
                 pageSize: 987,
-                properties: new Dictionary<string, object?> { { "prop1", 10 }, 
{ "prop-2", "xyz" } });
+                properties: new Dictionary<string, object?> { { "prop1", 10 }, 
{ "prop-2", "xyz" } },
+                timeZoneId: "Europe/Nicosia");
 
             await client.Sql.ExecuteScriptAsync(sqlStatement);
             var resProps = server.LastSqlScriptProps;
@@ -487,6 +494,7 @@ namespace Apache.Ignite.Tests.Sql
             Assert.AreEqual("SELECT PROPS", resProps["sql"]);
             Assert.AreEqual(10, resProps["prop1"]);
             Assert.AreEqual("xyz", resProps["prop-2"]);
+            Assert.AreEqual(sqlStatement.TimeZoneId, resProps["timeZoneId"]);
         }
 
         [Test]
@@ -515,5 +523,83 @@ namespace Apache.Ignite.Tests.Sql
 
             Assert.AreEqual(3.333333333333333m, res[0]);
         }
+
+        [Test]
+        public async Task TestStatementTimeZoneWithAllZones([Values(true, 
false)] bool useNodaTime)
+        {
+            using var client = await IgniteClient.StartAsync(GetConfig() with 
{ LoggerFactory = NullLoggerFactory.Instance });
+            var statement = new SqlStatement("SELECT CURRENT_TIMESTAMP");
+
+            ICollection<string> zoneIds = useNodaTime
+                ? DateTimeZoneProviders.Tzdb.Ids
+                : TimeZoneInfo.GetSystemTimeZones().Select(x => x.Id).ToList();
+
+            var zoneProvider = useNodaTime
+                ? DateTimeZoneProviders.Tzdb
+                : DateTimeZoneProviders.Bcl;
+
+            List<Exception> failures = new();
+
+            foreach (var zoneId in zoneIds)
+            {
+                try
+                {
+                    await using var resultSet = await 
client.Sql.ExecuteAsync(null, statement with { TimeZoneId = zoneId });
+
+                    var resTime = (LocalDateTime)(await 
resultSet.SingleAsync())[0]!;
+
+                    var currentTimeInZone = 
SystemClock.Instance.GetCurrentInstant()
+                        .InZone(zoneProvider[zoneId])
+                        .LocalDateTime;
+
+                    AssertLocalDateTimeSimilar(currentTimeInZone, resTime, 
zoneId);
+                }
+                catch (Exception e)
+                {
+                    failures.Add(e);
+                    Console.WriteLine("Time zone mismatch between .NET and 
Java: " + e.Message);
+                }
+            }
+
+            // JDK, CLR, and NodaTime have time zone databases that are 
updated at different times, we expect some mismatches.
+            if (failures.Count > 30)
+            {
+                throw new AggregateException("Too many failures: " + 
failures.Count, failures);
+            }
+
+            Console.WriteLine($"{zoneIds.Count - failures.Count} time zones 
match in .NET and Java.");
+        }
+
+        [Test]
+        public async Task TestStatementTimezoneAsUtcOffset([Values(0, 5, 10)] 
int offset)
+        {
+            var statement = new SqlStatement("SELECT CURRENT_TIMESTAMP", 
timeZoneId: $"UTC+{offset}");
+            await using var resultSet = await Client.Sql.ExecuteAsync(null, 
statement);
+            var resTime = (LocalDateTime)(await resultSet.SingleAsync())[0]!;
+
+            var expectedTime = SystemClock.Instance.GetCurrentInstant()
+                .InZone(DateTimeZone.ForOffset(Offset.FromHours(offset)))
+                .LocalDateTime;
+
+            AssertLocalDateTimeSimilar(expectedTime, resTime, $"Offset: 
{offset}");
+        }
+
+        private static void AssertLocalDateTimeSimilar(LocalDateTime expected, 
LocalDateTime actual, string message)
+        {
+            double deltaSeconds = 10;
+
+            var expectedSeconds = ToUnixTimeSeconds(expected);
+            var actualSeconds = ToUnixTimeSeconds(actual);
+            var diff = Math.Abs(expectedSeconds - actualSeconds);
+
+            if (diff > deltaSeconds)
+            {
+                throw new InvalidOperationException(
+                    $"Expected: {expectedSeconds}, actual: {actualSeconds}, 
diff: {diff / 3600} hours ({message})");
+            }
+
+            static double ToUnixTimeSeconds(LocalDateTime localDateTime) =>
+                new 
DateTimeOffset(localDateTime.ToDateTimeUnspecified()).ToUnixTimeSeconds();
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs
index 565b4835b3..cdccdc99cf 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs
@@ -26,6 +26,7 @@ namespace Apache.Ignite.Internal.Sql
     using Ignite.Table;
     using Ignite.Transactions;
     using Linq;
+    using NodaTime;
     using Proto;
     using Proto.BinaryTuple;
     using Proto.MsgPack;
@@ -238,7 +239,7 @@ namespace Apache.Ignite.Internal.Sql
             w.Write(statement.PageSize);
             w.Write((long)statement.Timeout.TotalMilliseconds);
             w.WriteNil(); // Session timeout (unused, session is closed by the 
server immediately).
-            w.WriteNil(); // TODO: IGNITE-21604 Time zone id.
+            w.Write(statement.TimeZoneId);
 
             WriteProperties(statement, ref w);
             w.Write(statement.Query);
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/SqlStatement.cs 
b/modules/platforms/dotnet/Apache.Ignite/Sql/SqlStatement.cs
index b6f1060258..a1464ac755 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Sql/SqlStatement.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/SqlStatement.cs
@@ -21,6 +21,7 @@ namespace Apache.Ignite.Sql
     using System;
     using System.Collections.Generic;
     using Internal.Common;
+    using NodaTime;
 
     /// <summary>
     /// SQL statement.
@@ -55,12 +56,26 @@ namespace Apache.Ignite.Sql
         /// <param name="schema">Schema.</param>
         /// <param name="pageSize">Page size.</param>
         /// <param name="properties">Properties.</param>
+        /// <param name="timeZoneId">
+        /// Time zone id. Examples: <c>"America/New_York"</c>, <c>"UTC+3"</c>.
+        /// <para />
+        /// Affects time-related SQL functions (e.g. <c>CURRENT_TIME</c>)
+        /// and string literal conversions (e.g. <c>TIMESTAMP WITH LOCAL TIME 
ZONE '1992-01-18 02:30:00.123'</c>).
+        /// <para />
+        /// Defaults to local time zone: <see cref="TimeZoneInfo.Local"/>.
+        /// <para />
+        /// Can be obtained using the standard library with <see 
cref="TimeZoneInfo.Id"/>
+        /// or using NodaTime with <see cref="DateTimeZone.Id"/>.
+        /// <para />
+        /// For more information, see <see 
href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneId.html#of(java.lang.String)"/>.
+        /// </param>
         public SqlStatement(
             string query,
             TimeSpan? timeout = null,
             string? schema = null,
             int? pageSize = null,
-            IReadOnlyDictionary<string, object?>? properties = null)
+            IReadOnlyDictionary<string, object?>? properties = null,
+            string? timeZoneId = null)
         {
             IgniteArgumentCheck.NotNull(query);
             IgniteArgumentCheck.Ensure(pageSize is null or > 0, 
nameof(pageSize), "Page size must be positive.");
@@ -70,6 +85,7 @@ namespace Apache.Ignite.Sql
             Schema = schema ?? DefaultSchema;
             PageSize = pageSize ?? DefaultPageSize;
             Properties = properties == null || ReferenceEquals(properties, 
EmptyProperties) ? EmptyProperties : new(properties);
+            TimeZoneId = timeZoneId ?? TimeZoneInfo.Local.Id;
         }
 
         /// <summary>
@@ -97,6 +113,21 @@ namespace Apache.Ignite.Sql
         /// </summary>
         public IReadOnlyDictionary<string, object?> Properties { get; init; }
 
+        /// <summary>
+        /// Gets the time zone id. Examples: <c>"America/New_York"</c>, 
<c>"UTC+3"</c>.
+        /// <para />
+        /// Affects time-related SQL functions (e.g. <c>CURRENT_TIME</c>)
+        /// and string literal conversions (e.g. <c>TIMESTAMP WITH LOCAL TIME 
ZONE '1992-01-18 02:30:00.123'</c>).
+        /// <para />
+        /// Defaults to local time zone: <see cref="TimeZoneInfo.Local"/>.
+        /// <para />
+        /// Can be obtained using the standard library with <see 
cref="TimeZoneInfo.Id"/>
+        /// or using NodaTime with <see cref="DateTimeZone.Id"/>.
+        /// <para />
+        /// For more information, see <see 
href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneId.html#of(java.lang.String)"/>.
+        /// </summary>
+        public string TimeZoneId { get; init; }
+
         /// <summary>
         /// Converts a query string to an instance of <see 
cref="SqlStatement"/>.
         /// </summary>

Reply via email to