blob: f9f04ce347e367e5655ba7b80f73d827d7ed9d32 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Apache.Ignite.Tests.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Ignite.Sql;
using Ignite.Table;
using Microsoft.Extensions.Logging;
using NodaTime;
using NUnit.Framework;
using Table;
/// <summary>
/// Basic LINQ provider tests.
/// </summary>
[SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "Tests")]
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local", Justification = "Tests")]
[SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Tests")]
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Tests")]
[SuppressMessage("ReSharper", "UnusedMember.Local", Justification = "Tests")]
public partial class LinqTests : IgniteTestsBase
{
private const int Count = 10;
private IRecordView<PocoByte> PocoByteView { get; set; } = null!;
private IRecordView<PocoShort> PocoShortView { get; set; } = null!;
private IRecordView<PocoInt> PocoIntView { get; set; } = null!;
private IRecordView<PocoIntEnum> PocoIntEnumView { get; set; } = null!;
private IRecordView<PocoLong> PocoLongView { get; set; } = null!;
private IRecordView<PocoFloat> PocoFloatView { get; set; } = null!;
private IRecordView<PocoDouble> PocoDoubleView { get; set; } = null!;
private IRecordView<PocoDecimal> PocoDecimalView { get; set; } = null!;
private IRecordView<PocoBigDecimal> PocoBigDecimalView { get; set; } = null!;
private IRecordView<PocoString> PocoStringView { get; set; } = null!;
[OneTimeSetUp]
public async Task InsertData()
{
var tableNames = new[]
{
TableName, TableDateTimeName, TableDoubleName, TableFloatName, TableDecimalName, TableInt8Name,
TableInt16Name, TableInt32Name, TableInt64Name
};
foreach (var tableName in tableNames)
{
await Client.Sql.ExecuteAsync(null, "delete from " + tableName);
}
PocoByteView = (await Client.Tables.GetTableAsync(TableInt8Name))!.GetRecordView<PocoByte>();
PocoShortView = (await Client.Tables.GetTableAsync(TableInt16Name))!.GetRecordView<PocoShort>();
PocoIntView = (await Client.Tables.GetTableAsync(TableInt32Name))!.GetRecordView<PocoInt>();
PocoIntEnumView = (await Client.Tables.GetTableAsync(TableInt32Name))!.GetRecordView<PocoIntEnum>();
PocoLongView = (await Client.Tables.GetTableAsync(TableInt64Name))!.GetRecordView<PocoLong>();
PocoFloatView = (await Client.Tables.GetTableAsync(TableFloatName))!.GetRecordView<PocoFloat>();
PocoDoubleView = (await Client.Tables.GetTableAsync(TableDoubleName))!.GetRecordView<PocoDouble>();
PocoStringView = (await Client.Tables.GetTableAsync(TableStringName))!.GetRecordView<PocoString>();
var tableDecimal = await Client.Tables.GetTableAsync(TableDecimalName);
PocoDecimalView = tableDecimal!.GetRecordView<PocoDecimal>();
PocoBigDecimalView = tableDecimal.GetRecordView<PocoBigDecimal>();
for (int i = 0; i < Count; i++)
{
await PocoView.UpsertAsync(null, new Poco { Key = i, Val = "v-" + i });
await PocoByteView.UpsertAsync(null, new PocoByte((sbyte)i, (sbyte)(i / 3)));
await PocoShortView.UpsertAsync(null, new PocoShort((short)(i * 2), (short)(i * 2)));
await PocoIntView.UpsertAsync(null, new PocoInt(i, i * 100));
await PocoLongView.UpsertAsync(null, new PocoLong(i, i * 2));
await PocoFloatView.UpsertAsync(null, new(i, i));
await PocoDoubleView.UpsertAsync(null, new(i, i));
await PocoDecimalView.UpsertAsync(null, new(i, i));
await PocoStringView.UpsertAsync(null, new("k-" + i, "v-" + i * 2));
var pocoAllColumns = new PocoAllColumnsSqlNullable(
i,
"v -" + i,
(sbyte)(i + 1),
(short)(i + 2),
i + 3,
i + 4,
i + 5.5f,
i + 6.5,
new LocalDate(2022, 12, i + 1),
new LocalTime(11, 38, i + 1),
new LocalDateTime(2022, 12, 19, 11, i + 1),
Instant.FromUnixTimeSeconds(i + 1),
new byte[] { 1, 2 },
i + 7.7m,
new Guid(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, (byte)(i + 1)),
i % 2 == 0);
await PocoAllColumnsSqlNullableView.UpsertAsync(null, pocoAllColumns);
}
await PocoAllColumnsSqlNullableView.UpsertAsync(null, new PocoAllColumnsSqlNullable(100));
}
[OneTimeTearDown]
public async Task CleanTables()
{
await TupleView.DeleteAllAsync(null, Enumerable.Range(0, Count).Select(x => GetTuple(x)));
await PocoIntView.DeleteAllAsync(null, Enumerable.Range(0, Count).Select(x => new PocoInt(x, 0)));
}
[Test]
public void TestSelectOneColumn()
{
var query = PocoView.AsQueryable()
.Where(x => x.Key == 3)
.Select(x => x.Val);
string?[] res = query.ToArray();
CollectionAssert.AreEqual(new[] { "v-3" }, res);
}
[Test]
public void TestSelectOneColumnSingle()
{
var res = PocoView.AsQueryable()
.Where(x => x.Key == 3)
.Select(x => x.Val)
.Single();
Assert.AreEqual("v-3", res);
}
[Test]
public void TestSelectOneColumnSingleWithMultipleRowsThrows()
{
// ReSharper disable once ReturnValueOfPureMethodIsNotUsed
var ex = Assert.Throws<InvalidOperationException>(
() => PocoView.AsQueryable()
.Select(x => x.Val)
.Single());
const string expected = "ResultSet is expected to have one row, but has more: " +
"select _T0.VAL from PUBLIC.TBL1 as _T0 limit 2";
Assert.AreEqual(expected, ex!.Message);
}
[Test]
public void TestSelectOneColumnFirst()
{
var res = PocoView.AsQueryable()
.OrderBy(x => x.Key)
.Select(x => x.Val)
.First();
Assert.AreEqual("v-0", res);
}
[Test]
public async Task TestSelectOneColumnAsResultSet()
{
var query = PocoView.AsQueryable()
.Where(x => x.Key == 3)
.Select(x => x.Val);
await using IResultSet<string?> resultSet = await query.ToResultSetAsync();
List<string?> rows = await resultSet.ToListAsync();
CollectionAssert.AreEqual(new[] { "v-3" }, rows);
Assert.IsTrue(resultSet.HasRowSet);
Assert.IsNotNull(resultSet.Metadata);
Assert.AreEqual("VAL", resultSet.Metadata!.Columns.Single().Name);
}
[Test]
public async Task TestSelectOneColumnAsAsyncEnumerable()
{
var query = PocoView.AsQueryable()
.Where(x => x.Key == 3)
.Select(x => x.Val);
var count = 0;
await foreach (var row in query.AsAsyncEnumerable())
{
Assert.AreEqual("v-3", row);
count++;
}
Assert.AreEqual(1, count);
}
[Test]
public void TestSelectEntireObject()
{
Poco[] res = PocoView.AsQueryable()
.Where(x => x.Key == 3)
.ToArray();
Assert.AreEqual(1, res.Length);
Assert.AreEqual(3, res[0].Key);
Assert.AreEqual("v-3", res[0].Val);
}
[Test]
public void TestSelectEntireRecordObject()
{
PocoInt res = PocoIntView.AsQueryable().Single(x => x.Key == 3);
Assert.AreEqual(3, res.Key);
Assert.AreEqual(300, res.Val);
}
[Test]
public void TestSelectTwoColumns()
{
var res = PocoView.AsQueryable()
.Where(x => x.Key == 2)
.Select(x => new { x.Key, x.Val })
.ToArray();
Assert.AreEqual(1, res.Length);
Assert.AreEqual(2, res[0].Key);
Assert.AreEqual("v-2", res[0].Val);
}
[Test]
public void TestSelectComputedColumnIntoAnonymousType()
{
var res = PocoView.AsQueryable()
.Where(x => x.Key == 7)
.Select(x => new { x.Key, x.Val, Key2 = x.Key + 1 })
.ToArray();
Assert.AreEqual(1, res.Length);
Assert.AreEqual(7, res[0].Key);
Assert.AreEqual(8, res[0].Key2);
Assert.AreEqual("v-7", res[0].Val);
}
[Test]
public void TestSelectComputedColumnIntoPoco()
{
var res = PocoView.AsQueryable()
.Where(x => x.Key == 3)
.Select(x => new Poco { Val = x.Val, Key = x.Key - 1 })
.ToArray();
Assert.AreEqual(1, res.Length);
Assert.AreEqual(2, res[0].Key);
Assert.AreEqual("v-3", res[0].Val);
}
[Test]
public void TestSkip()
{
var query = PocoView.AsQueryable()
.OrderBy(x => x.Key)
.Select(x => x.Key)
.Skip(7);
List<long> res = query.ToList();
Assert.AreEqual(new long[] { 7, 8, 9 }, res);
StringAssert.Contains(
"select _T0.KEY from PUBLIC.TBL1 as _T0 " +
"order by _T0.KEY asc " +
"offset ?",
query.ToString());
}
[Test]
public void TestTake()
{
var query = PocoView.AsQueryable()
.OrderBy(x => x.Key)
.Take(2);
List<Poco> res = query.ToList();
Assert.AreEqual(new long[] { 0, 1 }, res.Select(x => x.Key));
StringAssert.Contains(
"select _T0.KEY, _T0.VAL from PUBLIC.TBL1 as _T0 " +
"order by _T0.KEY asc " +
"limit ?",
query.ToString());
}
[Test]
public void TestOrderBySkipTake()
{
var query = PocoView.AsQueryable()
.OrderByDescending(x => x.Key)
.Select(x => x.Key)
.Skip(1)
.Take(2);
List<long> res = query.ToList();
Assert.AreEqual(new long[] { 8, 7 }, res);
StringAssert.Contains(
"select _T0.KEY from PUBLIC.TBL1 as _T0 " +
"order by _T0.KEY desc " +
"limit ? offset ?",
query.ToString());
}
[Test]
public void TestSkipTakeFirst()
{
var query = PocoView.AsQueryable()
.OrderByDescending(x => x.Key)
.Select(x => x.Key)
.Skip(1)
.Take(2);
long res = query.First();
Assert.AreEqual(8, res);
StringAssert.Contains(
"select _T0.KEY from PUBLIC.TBL1 as _T0 " +
"order by _T0.KEY desc " +
"limit ? offset ?",
query.ToString());
}
[Test]
public void TestOrderBySkipTakeBeforeSelect()
{
var query = PocoView.AsQueryable()
.OrderByDescending(x => x.Key)
.Skip(1)
.Take(2)
.Select(x => x.Key);
List<long> res = query.ToList();
Assert.AreEqual(new long[] { 8, 7 }, res);
StringAssert.Contains(
"select _T0.KEY from (" +
"select * from PUBLIC.TBL1 as _T1 " +
"order by _T1.KEY desc " +
"limit ? offset ?) as _T0",
query.ToString());
}
[Test]
public void TestOrderByMultiple()
{
var query = PocoAllColumnsSqlView.AsQueryable()
.Where(x => x.Key < 10)
.OrderBy(x => x.Key)
.ThenByDescending(x => x.Int8)
.ThenBy(x => x.Int16)
.Select(x => new { x.Decimal, x.Double });
var res = query.ToList();
Assert.AreEqual(6.5d, res[0].Double);
Assert.AreEqual(7.7m, res[0].Decimal);
StringAssert.Contains(
"select _T0.\"DECIMAL\", _T0.\"DOUBLE\" " +
"from PUBLIC.TBL_ALL_COLUMNS_SQL as _T0 " +
"where (_T0.KEY < ?) " +
"order by _T0.KEY asc, _T0.INT8 desc, _T0.INT16 asc",
query.ToString());
}
[Test]
public void TestContains()
{
var keys = new long[] { 4, 2 };
var query = PocoView.AsQueryable()
.Where(x => keys.Contains(x.Key))
.Select(x => x.Val);
List<string?> res = query.ToList();
CollectionAssert.AreEquivalent(new[] { "v-2", "v-4" }, res);
StringAssert.Contains(
"select _T0.VAL from PUBLIC.TBL1 as _T0 where (_T0.KEY IN (?, ?)), Parameters = [ 4, 2 ]",
query.ToString());
}
[Test]
public void TestDistinctOneField()
{
var query = PocoByteView.AsQueryable()
.Select(x => x.Val)
.Distinct();
List<sbyte?> res = query.ToList();
CollectionAssert.AreEquivalent(new[] { 0, 1, 2, 3 }, res);
StringAssert.Contains("select distinct _T0.VAL from PUBLIC.TBL_INT8 as _T0", query.ToString());
}
[Test]
public void TestDistinctEntireObject()
{
var query = PocoByteView.AsQueryable()
.Distinct();
List<PocoByte> res = query.ToList();
Assert.AreEqual(10, res.Count);
StringAssert.Contains("select distinct _T0.KEY, _T0.VAL from PUBLIC.TBL_INT8 as _T0", query.ToString());
}
[Test]
public void TestDistinctProjection()
{
var query = PocoByteView.AsQueryable()
.Select(x => new { Id = x.Val + 10, V = x.Val })
.Distinct();
var res = query.ToList();
Assert.AreEqual(4, res.Count);
StringAssert.Contains(
"select distinct (cast(_T0.VAL as int) + ?) as ID, _T0.VAL " +
"from PUBLIC.TBL_INT8 as _T0",
query.ToString());
}
[Test]
public void TestDistinctAfterOrderBy()
{
var query = PocoByteView.AsQueryable()
.Select(x => new { Id = x.Val + 10, V = x.Val })
.OrderByDescending(x => x.V)
.Distinct();
var res = query.ToList();
Assert.AreEqual(4, res.Count);
Assert.AreEqual(13, res[0].Id);
StringAssert.Contains(
"select distinct (cast(_T0.VAL as int) + ?) as ID, _T0.VAL " +
"from PUBLIC.TBL_INT8 as _T0 " +
"order by _T0.VAL desc",
query.ToString());
}
[Test]
public void TestDistinctBeforeOrderBy()
{
var query = PocoByteView.AsQueryable()
.Select(x => new { Id = x.Val + 10, V = x.Val })
.Distinct()
.OrderByDescending(x => x.Id);
var res = query.ToList();
Assert.AreEqual(4, res.Count);
Assert.AreEqual(13, res[0].Id);
StringAssert.Contains(
"select * from " +
"(select distinct (cast(_T0.VAL as int) + ?) as ID, _T0.VAL from PUBLIC.TBL_INT8 as _T0) as _T1 " +
"order by _T1.ID desc",
query.ToString());
}
[Test]
public void TestCustomColumnNameMapping()
{
var res = Table.GetRecordView<PocoCustomNames>().AsQueryable()
.Select(x => new { Key = x.Id, Val = x.Name })
.Where(x => x.Key == 3)
.ToArray();
Assert.AreEqual(1, res.Length);
Assert.AreEqual(3, res[0].Key);
Assert.AreEqual("v-3", res[0].Val);
}
[Test]
public async Task TestTransactionIsPropagatedToServer()
{
using var server = new FakeServer();
using var client = await server.ConnectClientAsync();
var tx = await client.Transactions.BeginAsync();
var tbl = await client.Tables.GetTableAsync(FakeServer.ExistingTableName);
var pocoView = tbl!.GetRecordView<PocoInt>();
_ = pocoView.AsQueryable(tx).Select(x => x.Key).ToArray();
Assert.AreEqual(0, server.LastSqlTxId);
}
[Test]
public void TestEnumeration()
{
var query = PocoView.AsQueryable(options: new QueryableOptions(PageSize: 2)).OrderBy(x => x.Key);
int count = 0;
foreach (var poco in query)
{
Assert.AreEqual(count++, poco.Key);
}
Assert.AreEqual(10, count);
}
[Test]
public void TestSelectDecimalIntoAnonymousTypeUsesCorrectScale()
{
var query = PocoDecimalView.AsQueryable()
.OrderByDescending(x => x.Val)
.Select(x => new
{
Id = x.Key
});
var res = query.ToList();
Assert.AreEqual(9.0m, res[0].Id);
}
[Test]
public void TestSelectDecimalIntoUserDefinedTypeUsesCorrectScale()
{
var query = PocoDecimalView.AsQueryable()
.OrderByDescending(x => x.Val)
.Select(x => new PocoDecimal(x.Key, x.Val));
var res = query.ToList();
Assert.AreEqual(9.0m, res[0].Val);
}
[Test]
public void TestSelectAllColumnTypes()
{
var res = PocoAllColumnsSqlView.AsQueryable()
.OrderBy(x => x.Key)
.Take(3)
.ToList();
Assert.AreEqual(0, res[0].Key);
Assert.AreEqual(1, res[0].Int8);
Assert.AreEqual(2, res[0].Int16);
Assert.AreEqual(3, res[0].Int32);
Assert.AreEqual(4, res[0].Int64);
Assert.AreEqual(5.5f, res[0].Float);
Assert.AreEqual(6.5, res[0].Double);
Assert.AreEqual(7.7, res[0].Decimal);
Assert.AreEqual(new LocalDate(2022, 12, 1), res[0].Date);
Assert.AreEqual(new LocalTime(11, 38, 1), res[0].Time);
Assert.AreEqual(new LocalDateTime(2022, 12, 19, 11, 1), res[0].DateTime);
Assert.AreEqual(Instant.FromUnixTimeSeconds(1), res[0].Timestamp);
Assert.AreEqual(new byte[] { 1, 2 }, res[0].Blob);
Assert.AreEqual(new Guid(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1), res[0].Uuid);
Assert.IsTrue(res[0].Boolean);
}
[Test]
public void TestSelectAllColumnTypesNullable()
{
var res = PocoAllColumnsSqlNullableView.AsQueryable()
.OrderBy(x => x.Key)
.ToList();
Assert.AreEqual(0, res[0].Key);
Assert.AreEqual(1, res[0].Int8);
Assert.AreEqual(2, res[0].Int16);
Assert.AreEqual(3, res[0].Int32);
Assert.AreEqual(4, res[0].Int64);
Assert.AreEqual(5.5f, res[0].Float);
Assert.AreEqual(6.5, res[0].Double);
Assert.AreEqual(7.7, res[0].Decimal);
Assert.AreEqual(new LocalDate(2022, 12, 1), res[0].Date);
Assert.AreEqual(new LocalTime(11, 38, 1), res[0].Time);
Assert.AreEqual(new LocalDateTime(2022, 12, 19, 11, 1), res[0].DateTime);
Assert.AreEqual(Instant.FromUnixTimeSeconds(1), res[0].Timestamp);
Assert.AreEqual(new byte[] { 1, 2 }, res[0].Blob);
Assert.AreEqual(new Guid(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1), res[0].Uuid);
Assert.IsTrue(res[0].Boolean);
}
[Test]
public void TestSelectAllColumnTypesNullableNull()
{
var res = PocoAllColumnsSqlNullableView.AsQueryable()
.OrderByDescending(x => x.Key)
.ToList();
Assert.AreEqual(100, res[0].Key);
Assert.IsNull(res[0].Str);
Assert.IsNull(res[0].Int8);
Assert.IsNull(res[0].Int16);
Assert.IsNull(res[0].Int32);
Assert.IsNull(res[0].Int64);
Assert.IsNull(res[0].Float);
Assert.IsNull(res[0].Double);
Assert.IsNull(res[0].Decimal);
Assert.IsNull(res[0].Date);
Assert.IsNull(res[0].Time);
Assert.IsNull(res[0].DateTime);
Assert.IsNull(res[0].Timestamp);
Assert.IsNull(res[0].Blob);
Assert.IsNull(res[0].Uuid);
Assert.IsNull(res[0].Boolean);
}
[Test]
public void TestWhereNull()
{
var query = PocoAllColumnsSqlNullableView.AsQueryable()
.Where(x => x.Int8 == null);
StringAssert.Contains("where (_T0.INT8 IS NOT DISTINCT FROM ?)", query.ToString());
var res = query.ToList();
Assert.AreEqual(100, res[0].Key);
Assert.AreEqual(1, res.Count);
}
[Test]
public void TestWhereNotNull()
{
var query = PocoAllColumnsSqlNullableView.AsQueryable()
.Where(x => x.Int8 != null)
.OrderBy(x => x.Key);
StringAssert.Contains("where (_T0.INT8 IS DISTINCT FROM ?)", query.ToString());
var res = query.ToList();
Assert.AreEqual(0, res[0].Key);
Assert.AreEqual(10, res.Count);
}
[Test]
public void TestFilterAndSelectEnumColumn()
{
var query = PocoIntEnumView.AsQueryable()
.Where(x => x.Val == TestEnum.B)
.Select(x => new { x.Key, Res = x.Val });
StringAssert.Contains(
"select _T0.KEY, _T0.VAL from PUBLIC.TBL_INT32 as _T0 where (cast(_T0.VAL as int) IS NOT DISTINCT FROM ?), Parameters = [ 300 ]",
query.ToString());
var res = query.ToList();
var resEnum = res[0].Res;
Assert.AreEqual(3, res[0].Key);
Assert.AreEqual(TestEnum.B, resEnum);
Assert.AreEqual(1, res.Count);
}
[Test]
[SuppressMessage("Globalization", "CA1311:Specify a culture or use an invariant version", Justification = "SQL")]
[SuppressMessage("Performance", "CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons", Justification = "SQL")]
public async Task TestGeneratedSqlIsLoggedWithDebugLevel()
{
var config = GetConfig();
var logger = new ListLoggerFactory(new[] { LogLevel.Debug });
config.LoggerFactory = logger;
using var client = await IgniteClient.StartAsync(config);
var table = (await client.Tables.GetTableAsync(TableName))!;
var view = table.GetRecordView<Poco>();
_ = await view.AsQueryable(null, new QueryableOptions { Timeout = TimeSpan.FromSeconds(15), PageSize = 123 })
.Where(x => x.Key > 5 && x.Val!.ToUpper() == "abc")
.SumAsync(x => x.Key);
var logEntry = logger.Entries.Single(x => x.Category == "Apache.Ignite.Internal.Linq.IgniteQueryExecutor");
var expectedMessage =
"Executing SQL statement generated by LINQ provider [statement=SqlStatement { " +
"Query = select sum(_T0.KEY) from PUBLIC.TBL1 as _T0 where ((_T0.KEY > ?) and (upper(_T0.VAL) IS NOT DISTINCT FROM ?)), " +
"Timeout = 00:00:15, " +
"Schema = PUBLIC, " +
"PageSize = 123, " +
"Properties = { } }, " +
"parameters=5, abc]";
Assert.AreEqual(expectedMessage, logEntry.Message);
Assert.AreEqual(LogLevel.Debug, logEntry.Level);
Assert.IsNull(logEntry.Exception);
}
private enum TestEnum
{
None = 0,
A = 100,
B = 300
}
private record PocoByte(sbyte Key, sbyte? Val);
private record PocoShort(short Key, short? Val);
private record PocoInt(int Key, int? Val);
private record PocoLong(long Key, long? Val);
private record PocoFloat(float Key, float? Val);
private record PocoDouble(double Key, double? Val);
private record PocoDecimal(decimal Key, decimal? Val);
private record PocoBigDecimal(BigDecimal Key, BigDecimal? Val);
private record PocoString(string Key, string? Val);
private record PocoDate(LocalDate Key, LocalDate? Val);
private record PocoTime(LocalTime Key, LocalTime? Val);
private record PocoDateTime(LocalDateTime Key, LocalDateTime? Val);
private record PocoIntEnum(int Key, TestEnum? Val);
}