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>