You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2023/01/24 14:45:57 UTC
[ignite-3] branch main updated: IGNITE-18478 .NET: Add ExecuteReader to SQL API (#1567)
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 d5bdbf1c8c IGNITE-18478 .NET: Add ExecuteReader to SQL API (#1567)
d5bdbf1c8c is described below
commit d5bdbf1c8c1d811cbbda0dfb9b543894edb611b6
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Tue Jan 24 16:45:52 2023 +0200
IGNITE-18478 .NET: Add ExecuteReader to SQL API (#1567)
Add `ISql.ExecuteReaderAsync`:
* `IgniteDbDataReader` inherits from `System.Data.Common.DbDataReader`, providing a standard interface to work with query results.
* `DbDataReader` API is an efficient way to retrieve query results without object mapping and with minimal memory allocation. This is a good alternative to allocation-heavy `IResultSet<IIgniteTuple>`.
---
.../dotnet/Apache.Ignite.Benchmarks/Program.cs | 4 +-
.../Sql/ResultSetBenchmarks.cs | 33 +-
.../Proto/BinaryTuple/BinaryTupleTests.cs | 10 +
.../Sql/IgniteDbDataReaderTests.cs | 650 +++++++++++++++++++++
.../dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs | 4 +
.../Proto/BinaryTuple/BinaryTupleReader.cs | 7 +
.../dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs | 58 +-
.../dotnet/Apache.Ignite/Internal/Sql/Sql.cs | 15 +-
.../Internal/Sql/SqlColumnTypeExtensions.cs | 16 +
modules/platforms/dotnet/Apache.Ignite/Sql/ISql.cs | 10 +
.../dotnet/Apache.Ignite/Sql/IgniteDbColumn.cs | 49 ++
.../dotnet/Apache.Ignite/Sql/IgniteDbDataReader.cs | 537 +++++++++++++++++
modules/platforms/dotnet/DEVNOTES.md | 6 +-
13 files changed, 1379 insertions(+), 20 deletions(-)
diff --git a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
index 72570b8afb..04fd5dde56 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
@@ -18,12 +18,12 @@
namespace Apache.Ignite.Benchmarks;
using BenchmarkDotNet.Running;
-using Proto.BinaryTuple;
+using Sql;
internal static class Program
{
private static void Main()
{
- BenchmarkRunner.Run<BinaryTupleReaderBenchmarks>();
+ BenchmarkRunner.Run<ResultSetBenchmarks>();
}
}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Sql/ResultSetBenchmarks.cs b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Sql/ResultSetBenchmarks.cs
index f2164061ad..2b465a076a 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Sql/ResultSetBenchmarks.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Sql/ResultSetBenchmarks.cs
@@ -20,6 +20,7 @@ namespace Apache.Ignite.Benchmarks.Sql
using System;
using System.Collections.Generic;
using System.Linq;
+ using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -30,13 +31,14 @@ namespace Apache.Ignite.Benchmarks.Sql
/// <summary>
/// Measures SQL result set enumeration with two pages of data (two network requests per enumeration).
/// <para />
- /// Results on Intel Core i7-7700HQ, .NET 6, GC = Concurrent Server, Ubuntu 20.04:
- /// | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated |
- /// |------------------------------- |-----------:|---------:|---------:|------:|--------:|--------:|-------:|----------:|
- /// | ToListAsync | 967.2 us | 18.17 us | 17.00 us | 1.00 | 0.00 | 15.6250 | - | 392 KB |
- /// | AsyncEnumerable | 1,172.7 us | 23.41 us | 27.87 us | 1.21 | 0.03 | 15.6250 | 1.9531 | 393 KB |
- /// | LinqToListAsync | 786.1 us | 14.94 us | 15.34 us | 0.81 | 0.02 | 2.9297 | - | 80 KB |
- /// | LinqSelectOneColumnToListAsync | 677.0 us | 13.42 us | 11.21 us | 0.70 | 0.02 | 1.9531 | - | 57 KB |.
+ /// Results on i9-12900H, .NET SDK 6.0.405, Ubuntu 22.04:
+ /// | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Allocated |
+ /// |------------------------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|
+ /// | ToListAsync | 229.0 us | 3.69 us | 3.45 us | 1.00 | 0.00 | 1.4648 | 392 KB |
+ /// | AsyncEnumerable | 263.2 us | 5.05 us | 4.72 us | 1.15 | 0.02 | 1.4648 | 393 KB |
+ /// | LinqToListAsync | 180.1 us | 3.18 us | 2.65 us | 0.79 | 0.02 | 0.2441 | 80 KB |
+ /// | LinqSelectOneColumnToListAsync | 156.0 us | 3.06 us | 4.48 us | 0.68 | 0.02 | 0.2441 | 57 KB |
+ /// | DbDataReader | 189.3 us | 1.61 us | 1.51 us | 0.83 | 0.01 | - | 51 KB |.
/// </summary>
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net60)]
@@ -118,6 +120,23 @@ namespace Apache.Ignite.Benchmarks.Sql
}
}
+ [Benchmark]
+ public async Task DbDataReader()
+ {
+ await using var reader = await _client!.Sql.ExecuteReaderAsync(null, "select 1");
+ var rows = new List<int>(1100);
+
+ while (await reader.ReadAsync(CancellationToken.None))
+ {
+ rows.Add(reader.GetInt32(0));
+ }
+
+ if (rows.Count != 1012)
+ {
+ throw new Exception("Wrong count");
+ }
+ }
+
private record Rec(int Id);
}
}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
index a93aef1c7f..6394a9b0eb 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
@@ -400,6 +400,16 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
CollectionAssert.AreEqual(bytes, res);
}
+ [Test]
+ public void TestBytesSpan([Values(0, 1, 123)] int count)
+ {
+ var bytes = Enumerable.Range(1, count).Select(x => (byte)x).ToArray();
+ var reader = BuildAndRead((ref BinaryTupleBuilder b) => b.AppendBytes(bytes));
+ var res = reader.GetBytesSpan(0).ToArray();
+
+ CollectionAssert.AreEqual(bytes, res);
+ }
+
[Test]
public void TestBitMask([Values(0, 1, 123)] int count)
{
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbDataReaderTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbDataReaderTests.cs
new file mode 100644
index 0000000000..a0adbae08a
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbDataReaderTests.cs
@@ -0,0 +1,650 @@
+/*
+ * 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.Sql;
+
+using System;
+using System.Collections.ObjectModel;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Ignite.Sql;
+using Internal.Sql;
+using NodaTime;
+using NUnit.Framework;
+using Table;
+
+/// <summary>
+/// Tests for <see cref="ISql.ExecuteReaderAsync"/>.
+/// </summary>
+public class IgniteDbDataReaderTests : IgniteTestsBase
+{
+ private const string AllColumnsQuery = "select \"KEY\", \"STR\", \"INT8\", \"INT16\", \"INT32\", \"INT64\", \"FLOAT\", " +
+ "\"DOUBLE\", \"DATE\", \"TIME\", \"DATETIME\", \"TIMESTAMP\", \"BLOB\", \"DECIMAL\" " +
+ "from TBL_ALL_COLUMNS_SQL ORDER BY KEY";
+
+ private static readonly LocalDate LocalDate = new(2023, 01, 18);
+
+ private static readonly LocalTime LocalTime = new(09, 28);
+
+ private static readonly LocalDateTime LocalDateTime = new(2023, 01, 18, 09, 29);
+ private static readonly Instant Instant = Instant.FromUnixTimeSeconds(123);
+ private static readonly byte[] Bytes = { 1, 2 };
+
+ [OneTimeSetUp]
+ public async Task InsertTestData()
+ {
+ await Client.Sql.ExecuteAsync(null, "delete from TBL_ALL_COLUMNS_SQL");
+
+ var pocoAllColumns1 = new PocoAllColumnsSqlNullable(
+ Key: 1,
+ Str: "v-1",
+ Int8: 2,
+ Int16: 3,
+ Int32: 4,
+ Int64: 5,
+ Float: 6.5F,
+ Double: 7.5D,
+ Date: LocalDate,
+ Time: LocalTime,
+ DateTime: LocalDateTime,
+ Timestamp: Instant,
+ Blob: Bytes,
+ Decimal: 8.7M);
+
+ var pocoAllColumns2 = new PocoAllColumnsSqlNullable(
+ Key: 2,
+ Str: "v-2",
+ Int8: sbyte.MinValue,
+ Int16: short.MinValue,
+ Int32: int.MinValue,
+ Int64: long.MinValue,
+ Float: float.MinValue,
+ Double: double.MinValue);
+
+ await PocoAllColumnsSqlNullableView.UpsertAllAsync(null, new[] { pocoAllColumns1, pocoAllColumns2 });
+
+ for (int i = 3; i < 10; i++)
+ {
+ await PocoAllColumnsSqlNullableView.UpsertAsync(null, new(i));
+ }
+ }
+
+ [Test]
+ [SuppressMessage("ReSharper", "ReturnValueOfPureMethodIsNotUsed", Justification = "Reviewed.")]
+ public async Task TestBasicUsage()
+ {
+ await using IgniteDbDataReader reader = await Client.Sql.ExecuteReaderAsync(
+ null,
+ "select KEY, INT8 from TBL_ALL_COLUMNS_SQL ORDER BY KEY");
+
+ Assert.AreEqual(2, reader.FieldCount);
+ Assert.IsTrue(reader.HasRows);
+ Assert.AreEqual(0, reader.Depth);
+ Assert.AreEqual(-1, reader.RecordsAffected);
+
+ Assert.AreEqual("KEY", reader.Metadata.Columns[0].Name);
+ Assert.AreEqual("INT8", reader.Metadata.Columns[1].Name);
+
+ Assert.Throws<InvalidOperationException>(() => reader.GetByte(1));
+
+ await reader.ReadAsync();
+ Assert.AreEqual(2, reader.GetByte(1));
+ }
+
+ [Test]
+ public async Task TestAllColumnTypes()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.AreEqual(14, reader.FieldCount);
+
+ Assert.AreEqual(1, reader.GetInt64("KEY"));
+ Assert.AreEqual("v-1", reader.GetString("STR"));
+ Assert.AreEqual(2, reader.GetByte("INT8"));
+ Assert.AreEqual(3, reader.GetInt16("INT16"));
+ Assert.AreEqual(4, reader.GetInt32("INT32"));
+ Assert.AreEqual(5, reader.GetInt64("INT64"));
+ Assert.AreEqual(6.5f, reader.GetFloat("FLOAT"));
+ Assert.AreEqual(7.5d, reader.GetDouble("DOUBLE"));
+ Assert.AreEqual(new DateTime(2023, 01, 18), reader.GetDateTime("DATE"));
+ Assert.AreEqual(LocalTime, reader.GetFieldValue<LocalTime>("TIME"));
+ Assert.AreEqual(new DateTime(2023, 01, 18, 09, 29, 0), reader.GetDateTime("DATETIME"));
+ Assert.AreEqual(Instant.ToDateTimeUtc(), reader.GetDateTime("TIMESTAMP"));
+ Assert.AreEqual(8.7m, reader.GetDecimal("DECIMAL"));
+ Assert.AreEqual(2, reader.GetBytes("BLOB", 0, null!, 0, 0));
+ }
+
+ [Test]
+ public async Task TestAllColumnTypesGetFieldValue()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.AreEqual(1, reader.GetFieldValue<long>("KEY"));
+ Assert.AreEqual(1, reader.GetFieldValue<int>("KEY"));
+ Assert.AreEqual(1, reader.GetFieldValue<byte>("KEY"));
+ Assert.AreEqual(1, reader.GetFieldValue<short>("KEY"));
+
+ Assert.AreEqual("v-1", reader.GetFieldValue<string>("STR"));
+
+ Assert.AreEqual(2, reader.GetFieldValue<byte>("INT8"));
+ Assert.AreEqual(2, reader.GetFieldValue<short>("INT8"));
+ Assert.AreEqual(2, reader.GetFieldValue<int>("INT8"));
+ Assert.AreEqual(2, reader.GetFieldValue<long>("INT8"));
+
+ Assert.AreEqual(3, reader.GetFieldValue<short>("INT16"));
+ Assert.AreEqual(3, reader.GetFieldValue<byte>("INT16"));
+ Assert.AreEqual(3, reader.GetFieldValue<int>("INT16"));
+ Assert.AreEqual(3, reader.GetFieldValue<long>("INT16"));
+
+ Assert.AreEqual(4, reader.GetFieldValue<int>("INT32"));
+ Assert.AreEqual(4, reader.GetFieldValue<byte>("INT32"));
+ Assert.AreEqual(4, reader.GetFieldValue<short>("INT32"));
+ Assert.AreEqual(4, reader.GetFieldValue<long>("INT32"));
+
+ Assert.AreEqual(5, reader.GetFieldValue<long>("INT64"));
+ Assert.AreEqual(5, reader.GetFieldValue<byte>("INT64"));
+ Assert.AreEqual(5, reader.GetFieldValue<short>("INT64"));
+ Assert.AreEqual(5, reader.GetFieldValue<int>("INT64"));
+
+ Assert.AreEqual(6.5f, reader.GetFieldValue<float>("FLOAT"));
+ Assert.AreEqual(6.5f, reader.GetFieldValue<double>("FLOAT"));
+
+ Assert.AreEqual(7.5d, reader.GetFieldValue<double>("DOUBLE"));
+ Assert.AreEqual(7.5d, reader.GetFieldValue<float>("DOUBLE"));
+
+ Assert.AreEqual(LocalDate, reader.GetFieldValue<LocalDate>("DATE"));
+ Assert.AreEqual(LocalDate.ToDateTimeUnspecified(), reader.GetFieldValue<DateTime>("DATE"));
+
+ Assert.AreEqual(LocalTime, reader.GetFieldValue<LocalTime>("TIME"));
+ Assert.AreEqual(LocalDateTime, reader.GetFieldValue<LocalDateTime>("DATETIME"));
+ Assert.AreEqual(LocalDateTime.ToDateTimeUnspecified(), reader.GetFieldValue<DateTime>("DATETIME"));
+
+ Assert.AreEqual(Instant, reader.GetFieldValue<Instant>("TIMESTAMP"));
+ Assert.AreEqual(8.7m, reader.GetFieldValue<decimal>("DECIMAL"));
+ Assert.AreEqual(Bytes, reader.GetFieldValue<byte[]>("BLOB"));
+
+ Assert.Throws<InvalidCastException>(() => reader.GetFieldValue<Array>("TIME"));
+ }
+
+ [Test]
+ public async Task TestAllColumnTypesGetFieldValueAsync()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.AreEqual(1, await reader.GetFieldValueAsync<long>("KEY"));
+ Assert.AreEqual(1, await reader.GetFieldValueAsync<int>("KEY"));
+ Assert.AreEqual(1, await reader.GetFieldValueAsync<byte>("KEY"));
+ Assert.AreEqual(1, await reader.GetFieldValueAsync<short>("KEY"));
+
+ Assert.AreEqual("v-1", await reader.GetFieldValueAsync<string>("STR"));
+
+ Assert.AreEqual(2, await reader.GetFieldValueAsync<byte>("INT8"));
+ Assert.AreEqual(2, await reader.GetFieldValueAsync<short>("INT8"));
+ Assert.AreEqual(2, await reader.GetFieldValueAsync<int>("INT8"));
+ Assert.AreEqual(2, await reader.GetFieldValueAsync<long>("INT8"));
+
+ Assert.AreEqual(3, await reader.GetFieldValueAsync<short>("INT16"));
+ Assert.AreEqual(3, await reader.GetFieldValueAsync<byte>("INT16"));
+ Assert.AreEqual(3, await reader.GetFieldValueAsync<int>("INT16"));
+ Assert.AreEqual(3, await reader.GetFieldValueAsync<long>("INT16"));
+
+ Assert.AreEqual(4, await reader.GetFieldValueAsync<int>("INT32"));
+ Assert.AreEqual(4, await reader.GetFieldValueAsync<byte>("INT32"));
+ Assert.AreEqual(4, await reader.GetFieldValueAsync<short>("INT32"));
+ Assert.AreEqual(4, await reader.GetFieldValueAsync<long>("INT32"));
+
+ Assert.AreEqual(5, await reader.GetFieldValueAsync<long>("INT64"));
+ Assert.AreEqual(5, await reader.GetFieldValueAsync<byte>("INT64"));
+ Assert.AreEqual(5, await reader.GetFieldValueAsync<short>("INT64"));
+ Assert.AreEqual(5, await reader.GetFieldValueAsync<int>("INT64"));
+
+ Assert.AreEqual(6.5f, await reader.GetFieldValueAsync<float>("FLOAT"));
+ Assert.AreEqual(6.5f, await reader.GetFieldValueAsync<double>("FLOAT"));
+
+ Assert.AreEqual(7.5d, await reader.GetFieldValueAsync<double>("DOUBLE"));
+ Assert.AreEqual(7.5d, await reader.GetFieldValueAsync<float>("DOUBLE"));
+
+ Assert.AreEqual(LocalDate, await reader.GetFieldValueAsync<LocalDate>("DATE"));
+ Assert.AreEqual(LocalDate.ToDateTimeUnspecified(), await reader.GetFieldValueAsync<DateTime>("DATE"));
+
+ Assert.AreEqual(LocalTime, await reader.GetFieldValueAsync<LocalTime>("TIME"));
+ Assert.AreEqual(LocalDateTime, await reader.GetFieldValueAsync<LocalDateTime>("DATETIME"));
+ Assert.AreEqual(LocalDateTime.ToDateTimeUnspecified(), await reader.GetFieldValueAsync<DateTime>("DATETIME"));
+
+ Assert.AreEqual(Instant, await reader.GetFieldValueAsync<Instant>("TIMESTAMP"));
+ Assert.AreEqual(8.7m, await reader.GetFieldValueAsync<decimal>("DECIMAL"));
+ Assert.AreEqual(Bytes, await reader.GetFieldValueAsync<byte[]>("BLOB"));
+ }
+
+ [Test]
+ public async Task TestAllColumnTypesAsCompatibleTypes()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.AreEqual(2, reader.GetByte("INT8"));
+ Assert.AreEqual(2, reader.GetInt16("INT8"));
+ Assert.AreEqual(2, reader.GetInt32("INT8"));
+ Assert.AreEqual(2, reader.GetInt64("INT8"));
+
+ Assert.AreEqual(3, reader.GetByte("INT16"));
+ Assert.AreEqual(3, reader.GetInt16("INT16"));
+ Assert.AreEqual(3, reader.GetInt32("INT16"));
+ Assert.AreEqual(3, reader.GetInt64("INT16"));
+
+ Assert.AreEqual(4, reader.GetByte("INT32"));
+ Assert.AreEqual(4, reader.GetInt16("INT32"));
+ Assert.AreEqual(4, reader.GetInt32("INT32"));
+ Assert.AreEqual(4, reader.GetInt64("INT32"));
+
+ Assert.AreEqual(5, reader.GetByte("INT64"));
+ Assert.AreEqual(5, reader.GetInt16("INT64"));
+ Assert.AreEqual(5, reader.GetInt32("INT64"));
+ Assert.AreEqual(5, reader.GetInt64("INT64"));
+
+ Assert.AreEqual(6.5f, reader.GetFloat("FLOAT"));
+ Assert.AreEqual(6.5f, reader.GetDouble("FLOAT"));
+
+ Assert.AreEqual(7.5d, reader.GetFloat("DOUBLE"));
+ Assert.AreEqual(7.5d, reader.GetDouble("DOUBLE"));
+
+ await reader.ReadAsync();
+
+ Assert.AreEqual(unchecked((byte)sbyte.MinValue), reader.GetByte("INT8"));
+ Assert.AreEqual(sbyte.MinValue, reader.GetInt16("INT8"));
+ Assert.AreEqual(sbyte.MinValue, reader.GetInt32("INT8"));
+ Assert.AreEqual(sbyte.MinValue, reader.GetInt64("INT8"));
+
+ Assert.AreEqual(short.MinValue, reader.GetInt16("INT16"));
+ Assert.AreEqual(short.MinValue, reader.GetInt32("INT16"));
+ Assert.AreEqual(short.MinValue, reader.GetInt64("INT16"));
+
+ Assert.AreEqual(int.MinValue, reader.GetInt32("INT32"));
+ Assert.AreEqual(int.MinValue, reader.GetInt64("INT32"));
+
+ Assert.AreEqual(long.MinValue, reader.GetInt64("INT64"));
+
+ Assert.AreEqual(float.MinValue, reader.GetFloat("FLOAT"));
+ Assert.AreEqual(float.MinValue, reader.GetDouble("FLOAT"));
+
+ Assert.AreEqual(double.MinValue, reader.GetDouble("DOUBLE"));
+ }
+
+ [Test]
+ [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "Reviewed.")]
+ public async Task TestIntFloatColumnsValueOutOfRangeThrows()
+ {
+ await using IgniteDbDataReader reader = await Client.Sql.ExecuteReaderAsync(null, AllColumnsQuery);
+ await reader.ReadAsync();
+ await reader.ReadAsync();
+
+ Test(() => reader.GetByte("INT16"), 1, 2);
+ Test(() => reader.GetByte("INT32"), 1, 4);
+ Test(() => reader.GetByte("INT64"), 1, 8);
+
+ Test(() => reader.GetInt16("INT32"), 2, 4);
+ Test(() => reader.GetInt16("INT64"), 2, 8);
+
+ Test(() => reader.GetInt32("INT64"), 4, 8);
+
+ Test(() => reader.GetFloat("DOUBLE"), 4, 8);
+
+ static void Test(TestDelegate testDelegate, int expected, int actual)
+ {
+ var ex = Assert.Throws<InvalidOperationException>(testDelegate);
+ StringAssert.StartsWith("Binary tuple element with index", ex!.Message);
+ StringAssert.Contains($"has invalid length (expected {expected}, actual {actual}).", ex.Message);
+ }
+ }
+
+ [Test]
+ [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "Reviewed.")]
+ public async Task TestAllColumnTypesAsIncompatibleTypeThrows()
+ {
+ await using var reader = await ExecuteReader();
+
+ Test(() => reader.GetBoolean("STR"), "STR", SqlColumnType.String, typeof(bool), typeof(string));
+ Test(() => reader.GetString("INT8"), "INT8", SqlColumnType.Int8, typeof(string), typeof(sbyte));
+ Test(() => reader.GetGuid("INT16"), "INT16", SqlColumnType.Int16, typeof(Guid), typeof(short));
+ Test(() => reader.GetDateTime("INT32"), "INT32", SqlColumnType.Int32, typeof(DateTime), typeof(int));
+ Test(() => reader.GetFloat("INT64"), "INT64", SqlColumnType.Int64, typeof(float), typeof(long));
+ Test(() => reader.GetDouble("INT64"), "INT64", SqlColumnType.Int64, typeof(double), typeof(long));
+ Test(() => reader.GetString("INT64"), "INT64", SqlColumnType.Int64, typeof(string), typeof(long));
+ Test(() => reader.GetByte("STR"), "STR", SqlColumnType.String, typeof(byte), typeof(string));
+ Test(() => reader.GetBytes("STR", 0, null!, 0, 0), "STR", SqlColumnType.String, typeof(byte[]), typeof(string));
+ Test(() => reader.GetDecimal("STR"), "STR", SqlColumnType.String, typeof(decimal), typeof(string));
+ Test(() => reader.GetInt16("STR"), "STR", SqlColumnType.String, typeof(short), typeof(string));
+ Test(() => reader.GetInt32("STR"), "STR", SqlColumnType.String, typeof(int), typeof(string));
+ Test(() => reader.GetInt64("STR"), "STR", SqlColumnType.String, typeof(long), typeof(string));
+
+ static void Test(TestDelegate testDelegate, string columnName, SqlColumnType columnType, Type expectedType, Type actualType)
+ {
+ var ex = Assert.Throws<InvalidCastException>(testDelegate);
+
+ Assert.AreEqual(
+ $"Column {columnName} of type {columnType.ToSqlTypeName()} ({actualType}) can not be cast to {expectedType}.",
+ ex!.Message);
+ }
+ }
+
+ [Test]
+ [SuppressMessage("Performance", "CA1849:Call async methods when in an async method", Justification = "Testing sync method.")]
+ public async Task TestMultiplePages([Values(true, false)] bool async)
+ {
+ var statement = new SqlStatement(AllColumnsQuery, pageSize: 2);
+ await using var reader = await Client.Sql.ExecuteReaderAsync(null, statement);
+
+ var count = 0;
+
+ while (async ? await reader.ReadAsync() : reader.Read())
+ {
+ count++;
+
+ Assert.AreEqual(count, reader.GetInt32(0));
+ }
+
+ Assert.IsFalse(reader.Read());
+ Assert.IsFalse(await reader.ReadAsync());
+
+ Assert.AreEqual(9, count);
+ }
+
+ [Test]
+ public async Task TestGetColumnSchema([Values(true, false)] bool async)
+ {
+ await using var reader = await ExecuteReader();
+
+ ReadOnlyCollection<DbColumn> schema = async ? await reader.GetColumnSchemaAsync() : reader.GetColumnSchema();
+
+ Assert.AreEqual(14, schema.Count);
+
+ Assert.AreEqual("KEY", schema[0].ColumnName);
+ Assert.AreEqual(0, schema[0].ColumnOrdinal);
+ Assert.IsNull(schema[0].ColumnSize);
+ Assert.AreEqual(typeof(long), schema[0].DataType);
+ Assert.AreEqual("bigint", schema[0].DataTypeName);
+ Assert.IsFalse(schema[0].AllowDBNull);
+ Assert.AreEqual(19, schema[0].NumericPrecision);
+ Assert.AreEqual(0, schema[0].NumericScale);
+ Assert.IsNotNull((schema[0] as IgniteDbColumn)?.ColumnMetadata);
+
+ Assert.AreEqual("STR", schema[1].ColumnName);
+ Assert.AreEqual(1, schema[1].ColumnOrdinal);
+ Assert.IsNull(schema[1].ColumnSize);
+ Assert.AreEqual(typeof(string), schema[1].DataType);
+ Assert.AreEqual("varchar", schema[1].DataTypeName);
+ Assert.IsTrue(schema[1].AllowDBNull);
+ Assert.AreEqual(65536, schema[1].NumericPrecision);
+ Assert.IsNull(schema[1].NumericScale);
+ Assert.IsNotNull((schema[1] as IgniteDbColumn)?.ColumnMetadata);
+ }
+
+ [Test]
+ public async Task TestGetName()
+ {
+ // ReSharper disable once UseAwaitUsing (test sync variant)
+ using var reader = await ExecuteReader();
+
+ Assert.AreEqual("KEY", reader.GetName(0));
+ Assert.AreEqual("STR", reader.GetName(1));
+ Assert.AreEqual("DECIMAL", reader.GetName(13));
+ }
+
+ [Test]
+ public async Task TestGetValue()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.AreEqual(1, reader.GetValue("KEY"));
+ Assert.AreEqual("v-1", reader.GetValue("STR"));
+ Assert.AreEqual(2, reader.GetValue("INT8"));
+ Assert.AreEqual(3, reader.GetValue("INT16"));
+ Assert.AreEqual(4, reader.GetValue("INT32"));
+ Assert.AreEqual(5, reader.GetValue("INT64"));
+ Assert.AreEqual(6.5f, reader.GetValue("FLOAT"));
+ Assert.AreEqual(7.5d, reader.GetValue("DOUBLE"));
+ Assert.AreEqual(LocalDate, reader.GetValue("DATE"));
+ Assert.AreEqual(LocalTime, reader.GetValue("TIME"));
+ Assert.AreEqual(LocalDateTime, reader.GetValue("DATETIME"));
+ Assert.AreEqual(Instant, reader.GetValue("TIMESTAMP"));
+ Assert.AreEqual(8.7m, reader.GetValue("DECIMAL"));
+ Assert.AreEqual(Bytes, reader.GetValue("BLOB"));
+ }
+
+ [Test]
+ public async Task TestGetValues()
+ {
+ await using var reader = await ExecuteReader();
+
+ var values = new object[reader.FieldCount];
+ var count = reader.GetValues(values);
+
+ var expected = new object[] { 1, "v-1", 2, 3, 4, 5, 6.5f, 7.5d, LocalDate, LocalTime, LocalDateTime, Instant, Bytes, 8.7m };
+
+ CollectionAssert.AreEqual(expected, values);
+ Assert.AreEqual(reader.FieldCount, count);
+ }
+
+ [Test]
+ public async Task TestGetChar()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.Throws<NotSupportedException>(() => reader.GetChar("KEY"));
+ }
+
+ [Test]
+ public async Task TestGetBytes()
+ {
+ await using var reader = await ExecuteReader();
+
+ var bytesLen = reader.GetBytes(name: "BLOB", dataOffset: 0, buffer: null!, bufferOffset: 0, length: 0);
+ var bytes = new byte[bytesLen];
+
+ Assert.AreEqual(2, bytesLen);
+ Assert.AreEqual(2, reader.GetBytes(name: "BLOB", dataOffset: 0L, buffer: bytes, bufferOffset: 0, length: (int)bytesLen));
+ Assert.AreEqual(Bytes, bytes);
+
+ Assert.Throws<ArgumentOutOfRangeException>(() =>
+ reader.GetBytes(ordinal: 0, dataOffset: -1, buffer: null, bufferOffset: 0, length: 0));
+
+ Assert.Throws<ArgumentOutOfRangeException>(() =>
+ reader.GetBytes(ordinal: 0, dataOffset: 0, buffer: bytes, bufferOffset: 10, length: 0));
+
+ Assert.Throws<ArgumentOutOfRangeException>(() =>
+ reader.GetBytes(ordinal: 0, dataOffset: 0, buffer: bytes, bufferOffset: 0, length: 10));
+ }
+
+ [Test]
+ public async Task TestGetChars()
+ {
+ await using var reader = await ExecuteReader();
+
+ var len = reader.GetChars(name: "STR", dataOffset: 0, buffer: null!, bufferOffset: 0, length: 0);
+ var chars = new char[len];
+ var count = reader.GetChars(name: "STR", dataOffset: 1, buffer: chars, bufferOffset: 1, length: 2);
+
+ Assert.AreEqual(3, len);
+ Assert.AreEqual(2, count);
+ Assert.AreEqual(new[] { (char)0, '-', '1' }, chars);
+
+ Assert.Throws<ArgumentOutOfRangeException>(() =>
+ reader.GetChars(ordinal: 0, dataOffset: -1, buffer: null, bufferOffset: 0, length: 0));
+
+ Assert.Throws<ArgumentOutOfRangeException>(() =>
+ reader.GetChars(ordinal: 0, dataOffset: 0, buffer: chars, bufferOffset: 10, length: 0));
+
+ Assert.Throws<ArgumentOutOfRangeException>(() =>
+ reader.GetChars(ordinal: 0, dataOffset: 0, buffer: chars, bufferOffset: 0, length: 10));
+ }
+
+ [Test]
+ public async Task TestIndexers()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.AreEqual(1, reader[0]);
+ Assert.AreEqual(1, reader["KEY"]);
+
+ Assert.AreEqual("v-1", reader[1]);
+ Assert.AreEqual("v-1", reader["STR"]);
+
+ Assert.Throws<ArgumentOutOfRangeException>(() => _ = reader[100]);
+ Assert.Throws<InvalidOperationException>(() => _ = reader["ABC"]);
+ }
+
+ [Test]
+ [SuppressMessage("Performance", "CA1849:Call async methods when in an async method", Justification = "Testing sync method.")]
+ [SuppressMessage("ReSharper", "MethodHasAsyncOverload", Justification = "Testing sync method.")]
+ public async Task TestClose([Values(true, false)] bool async)
+ {
+ await using var reader = await ExecuteReader();
+ Assert.IsFalse(reader.IsClosed);
+
+ if (async)
+ {
+ await reader.CloseAsync();
+ }
+ else
+ {
+ reader.Close();
+ }
+
+ Assert.IsTrue(reader.IsClosed);
+ }
+
+ [Test]
+ public async Task TestReadAllRowsClosesReader()
+ {
+ await using var reader = await ExecuteReader();
+ Assert.IsFalse(reader.IsClosed);
+
+ while (await reader.ReadAsync())
+ {
+ // No-op.
+ }
+
+ Assert.IsTrue(reader.IsClosed);
+ }
+
+ [Test]
+ public async Task TestIsDbNull()
+ {
+ await using var reader = await ExecuteReader();
+ await reader.ReadAsync();
+
+ /* ReSharper disable MethodHasAsyncOverload */
+ Assert.IsFalse(reader.IsDBNull("KEY"));
+ Assert.IsTrue(reader.IsDBNull("BLOB"));
+ /* ReSharper restore MethodHasAsyncOverload */
+ }
+
+ [Test]
+ public async Task TestNextResult()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.Throws<NotSupportedException>(() => reader.NextResult());
+ Assert.ThrowsAsync<NotSupportedException>(() => reader.NextResultAsync());
+ }
+
+ [Test]
+ public async Task TestGetEnumerator()
+ {
+ await using var reader = await Client.Sql.ExecuteReaderAsync(null, AllColumnsQuery);
+
+ foreach (DbDataRecord row in reader)
+ {
+ // DbDataRecord delegates to methods in DbDataReader, no need to test everything here.
+ Assert.AreEqual(14, row.FieldCount);
+ Assert.AreEqual("KEY", row.GetName(0));
+ }
+ }
+
+ [Test]
+ public async Task TestGetFieldType()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.AreEqual(typeof(long), reader.GetFieldType(0));
+ Assert.AreEqual(typeof(string), reader.GetFieldType(1));
+ Assert.AreEqual(typeof(sbyte), reader.GetFieldType(2));
+ Assert.AreEqual(typeof(short), reader.GetFieldType(3));
+ Assert.AreEqual(typeof(int), reader.GetFieldType(4));
+ Assert.AreEqual(typeof(long), reader.GetFieldType(5));
+ Assert.AreEqual(typeof(float), reader.GetFieldType(6));
+ Assert.AreEqual(typeof(double), reader.GetFieldType(7));
+ Assert.AreEqual(typeof(LocalDate), reader.GetFieldType(8));
+ Assert.AreEqual(typeof(LocalTime), reader.GetFieldType(9));
+ Assert.AreEqual(typeof(LocalDateTime), reader.GetFieldType(10));
+ Assert.AreEqual(typeof(Instant), reader.GetFieldType(11));
+ Assert.AreEqual(typeof(byte[]), reader.GetFieldType(12));
+ Assert.AreEqual(typeof(decimal), reader.GetFieldType(13));
+ }
+
+ [Test]
+ public async Task TestGetDataTypeName()
+ {
+ await using var reader = await ExecuteReader();
+
+ Assert.AreEqual("bigint", reader.GetDataTypeName(0));
+ Assert.AreEqual("varchar", reader.GetDataTypeName(1));
+ Assert.AreEqual("tinyint", reader.GetDataTypeName(2));
+ Assert.AreEqual("smallint", reader.GetDataTypeName(3));
+ Assert.AreEqual("int", reader.GetDataTypeName(4));
+ Assert.AreEqual("bigint", reader.GetDataTypeName(5));
+ Assert.AreEqual("real", reader.GetDataTypeName(6));
+ Assert.AreEqual("double", reader.GetDataTypeName(7));
+ Assert.AreEqual("date", reader.GetDataTypeName(8));
+ Assert.AreEqual("time", reader.GetDataTypeName(9));
+ Assert.AreEqual("timestamp", reader.GetDataTypeName(10));
+ Assert.AreEqual("timestamp_tz", reader.GetDataTypeName(11));
+ Assert.AreEqual("varbinary", reader.GetDataTypeName(12));
+ Assert.AreEqual("decimal", reader.GetDataTypeName(13));
+ }
+
+ [Test]
+ public async Task TestDataTableLoad()
+ {
+ await using var reader = await Client.Sql.ExecuteReaderAsync(null, AllColumnsQuery);
+
+ // This calls GetSchemaTable underneath.
+ var dt = new DataTable();
+ dt.Load(reader);
+
+ Assert.AreEqual(14, dt.Columns.Count);
+ Assert.AreEqual(9, dt.Rows.Count);
+ }
+
+ [Test]
+ public void TestExecuteReaderThrowsOnDmlQuery()
+ {
+ var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
+ await Client.Sql.ExecuteReaderAsync(null, "UPDATE TBL_ALL_COLUMNS_SQL SET STR='s' WHERE KEY > 100"));
+
+ Assert.AreEqual("ExecuteReaderAsync does not support queries without row set (DDL, DML).", ex!.Message);
+ }
+
+ private async Task<IgniteDbDataReader> ExecuteReader()
+ {
+ var reader = await Client.Sql.ExecuteReaderAsync(null, AllColumnsQuery);
+ await reader.ReadAsync();
+
+ return reader;
+ }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
index 815b8c6194..60ee7a72b1 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
@@ -294,12 +294,16 @@ namespace Apache.Ignite.Tests.Sql
Assert.AreEqual("TESTDDLDML", columns[0].Origin!.TableName);
Assert.IsTrue(columns[0].Nullable);
Assert.AreEqual(SqlColumnType.String, columns[0].Type);
+ Assert.AreEqual(int.MinValue, columns[0].Scale); // TODO IGNITE-18602 should conform to javadoc/xmldoc.
+ Assert.AreEqual(65536, columns[0].Precision);
Assert.AreEqual("ID", columns[1].Name);
Assert.AreEqual("ID", columns[1].Origin!.ColumnName);
Assert.AreEqual("PUBLIC", columns[1].Origin!.SchemaName);
Assert.AreEqual("TESTDDLDML", columns[1].Origin!.TableName);
Assert.IsFalse(columns[1].Nullable);
+ Assert.AreEqual(0, columns[1].Scale);
+ Assert.AreEqual(10, columns[1].Precision);
Assert.AreEqual("ID + 1", columns[2].Name);
Assert.IsNull(columns[2].Origin);
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
index e33ff2b83a..fdb1ac0d69 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
@@ -463,6 +463,13 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
/// <returns>Value.</returns>
public byte[]? GetBytesNullable(int index) => IsNull(index) ? null : GetBytes(index);
+ /// <summary>
+ /// Gets bytes.
+ /// </summary>
+ /// <param name="index">Index.</param>
+ /// <returns>Value.</returns>
+ public ReadOnlySpan<byte> GetBytesSpan(int index) => Seek(index);
+
/// <summary>
/// Gets an object value according to the specified type.
/// </summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs
index a1031ead5e..d4003f3e84 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs
@@ -42,8 +42,6 @@ namespace Apache.Ignite.Internal.Sql
private readonly PooledBuffer? _buffer;
- private readonly int _bufferOffset;
-
private readonly bool _hasMorePages;
private readonly RowReader<T>? _rowReader;
@@ -79,8 +77,7 @@ namespace Apache.Ignite.Internal.Sql
if (HasRowSet)
{
- _buffer = buf;
- _bufferOffset = reader.Consumed;
+ _buffer = buf.Slice(reader.Consumed);
}
else
{
@@ -110,6 +107,11 @@ namespace Apache.Ignite.Internal.Sql
/// <inheritdoc/>
public bool WasApplied { get; }
+ /// <summary>
+ /// Gets a value indicating whether this instance is disposed.
+ /// </summary>
+ internal bool IsDisposed => (_resourceId == null || _resourceClosed) && _bufferReleased > 0;
+
/// <inheritdoc/>
public async ValueTask<List<T>> ToListAsync() =>
await CollectAsync(
@@ -146,22 +148,22 @@ namespace Apache.Ignite.Internal.Sql
var hasMore = _hasMorePages;
TResult? res = default;
- ReadPage(_buffer!.Value, _bufferOffset);
+ ReadPage(_buffer!.Value);
ReleaseBuffer();
while (hasMore)
{
using var pageBuf = await FetchNextPage().ConfigureAwait(false);
- ReadPage(pageBuf, 0);
+ ReadPage(pageBuf);
}
_resourceClosed = true;
return res!;
- void ReadPage(PooledBuffer buf, int offset)
+ void ReadPage(PooledBuffer buf)
{
- var reader = buf.GetReader(offset);
+ var reader = buf.GetReader();
var pageSize = reader.ReadArrayHeader();
var capacity = hasMore ? pageSize * 2 : pageSize;
@@ -222,6 +224,44 @@ namespace Apache.Ignite.Internal.Sql
return EnumerateRows().GetAsyncEnumerator(cancellationToken);
}
+ /// <summary>
+ /// Enumerates ResultSet pages.
+ /// </summary>
+ /// <returns>ResultSet pages.</returns>
+ internal async IAsyncEnumerable<PooledBuffer> EnumeratePagesInternal()
+ {
+ ValidateAndSetIteratorState();
+
+ yield return _buffer!.Value;
+
+ ReleaseBuffer();
+
+ if (!_hasMorePages)
+ {
+ yield break;
+ }
+
+ while (true)
+ {
+ using var buffer = await FetchNextPage().ConfigureAwait(false);
+
+ yield return buffer;
+
+ if (!HasMore(buffer))
+ {
+ break;
+ }
+ }
+
+ static bool HasMore(PooledBuffer buf)
+ {
+ var reader = buf.GetReader();
+ reader.Skip();
+
+ return !reader.End && reader.ReadBoolean();
+ }
+ }
+
private static ResultSetMetadata ReadMeta(ref MsgPackReader reader)
{
var size = reader.ReadArrayHeader();
@@ -265,7 +305,7 @@ namespace Apache.Ignite.Internal.Sql
{
var hasMore = _hasMorePages;
var cols = Metadata!.Columns;
- var offset = _bufferOffset;
+ var offset = 0;
// First page.
foreach (var row in EnumeratePage(_buffer!.Value))
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs
index 0201bf68e5..71e10acf18 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs
@@ -62,6 +62,19 @@ namespace Apache.Ignite.Internal.Sql
throw new NotSupportedException();
}
+ /// <inheritdoc/>
+ public async Task<IgniteDbDataReader> ExecuteReaderAsync(ITransaction? transaction, SqlStatement statement, params object?[]? args)
+ {
+ var resultSet = await ExecuteAsyncInternal<object>(transaction, statement, _ => null!, args).ConfigureAwait(false);
+
+ if (!resultSet.HasRowSet)
+ {
+ throw new InvalidOperationException($"{nameof(ExecuteReaderAsync)} does not support queries without row set (DDL, DML).");
+ }
+
+ return new IgniteDbDataReader(resultSet);
+ }
+
/// <summary>
/// Reads column value.
/// </summary>
@@ -110,7 +123,7 @@ namespace Apache.Ignite.Internal.Sql
/// <param name="args">Arguments for the statement.</param>
/// <typeparam name="T">Row type.</typeparam>
/// <returns>SQL result set.</returns>
- internal async Task<IResultSet<T>> ExecuteAsyncInternal<T>(
+ internal async Task<ResultSet<T>> ExecuteAsyncInternal<T>(
ITransaction? transaction,
SqlStatement statement,
RowReaderFactory<T> rowReaderFactory,
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/SqlColumnTypeExtensions.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/SqlColumnTypeExtensions.cs
index 68a1790aec..9b331d05c5 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/SqlColumnTypeExtensions.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/SqlColumnTypeExtensions.cs
@@ -124,4 +124,20 @@ internal static class SqlColumnTypeExtensions
/// <returns>SQL column type, or null.</returns>
public static SqlColumnType? ToSqlColumnType(this Type type) =>
ClrToSql.TryGetValue(Nullable.GetUnderlyingType(type) ?? type, out var sqlType) ? sqlType : null;
+
+ /// <summary>
+ /// Gets a value indicating whether specified column type is an integer of any size (int8 to int64).
+ /// </summary>
+ /// <param name="sqlColumnType">SQL column type.</param>
+ /// <returns>Whether the type is integer.</returns>
+ public static bool IsAnyInt(this SqlColumnType sqlColumnType) =>
+ sqlColumnType is SqlColumnType.Int8 or SqlColumnType.Int16 or SqlColumnType.Int32 or SqlColumnType.Int64;
+
+ /// <summary>
+ /// Gets a value indicating whether specified column type is a floating point of any size (float32 to float64).
+ /// </summary>
+ /// <param name="sqlColumnType">SQL column type.</param>
+ /// <returns>Whether the type is floating point.</returns>
+ public static bool IsAnyFloat(this SqlColumnType sqlColumnType) =>
+ sqlColumnType is SqlColumnType.Float or SqlColumnType.Double;
}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/ISql.cs b/modules/platforms/dotnet/Apache.Ignite/Sql/ISql.cs
index 247950b05d..ac81dfcb56 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Sql/ISql.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/ISql.cs
@@ -17,6 +17,7 @@
namespace Apache.Ignite.Sql
{
+ using System.Data.Common;
using System.Threading.Tasks;
using Table;
using Transactions;
@@ -44,5 +45,14 @@ namespace Apache.Ignite.Sql
/// <typeparam name="T">Row type.</typeparam>
/// <returns>SQL result set.</returns>
Task<IResultSet<T>> ExecuteAsync<T>(ITransaction? transaction, SqlStatement statement, params object?[]? args);
+
+ /// <summary>
+ /// Executes single SQL statement and returns a <see cref="DbDataReader"/> to consume them in an efficient, forward-only way.
+ /// </summary>
+ /// <param name="transaction">Optional transaction.</param>
+ /// <param name="statement">Statement to execute.</param>
+ /// <param name="args">Arguments for the statement.</param>
+ /// <returns>Data reader.</returns>
+ Task<IgniteDbDataReader> ExecuteReaderAsync(ITransaction? transaction, SqlStatement statement, params object?[]? args);
}
}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbColumn.cs b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbColumn.cs
new file mode 100644
index 0000000000..f535676fcb
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbColumn.cs
@@ -0,0 +1,49 @@
+/*
+ * 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.Sql;
+
+using System.Data.Common;
+using Internal.Sql;
+
+/// <summary>
+/// Represents a column within Ignite result set.
+/// </summary>
+public sealed class IgniteDbColumn : DbColumn
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgniteDbColumn"/> class.
+ /// </summary>
+ /// <param name="column">Column.</param>
+ /// <param name="ordinal">Column ordinal.</param>
+ internal IgniteDbColumn(IColumnMetadata column, int ordinal)
+ {
+ ColumnMetadata = column;
+ ColumnName = column.Name;
+ ColumnOrdinal = ordinal;
+ DataTypeName = column.Type.ToSqlTypeName();
+ DataType = column.Type.ToClrType();
+ AllowDBNull = column.Nullable;
+ NumericPrecision = column.Precision < 0 ? null : column.Precision;
+ NumericScale = column.Scale < 0 ? null : column.Scale;
+ }
+
+ /// <summary>
+ /// Gets Ignite-specific column metadata.
+ /// </summary>
+ public IColumnMetadata ColumnMetadata { get; }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbDataReader.cs b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbDataReader.cs
new file mode 100644
index 0000000000..f18312ef2a
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbDataReader.cs
@@ -0,0 +1,537 @@
+/*
+ * 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.Sql;
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Internal.Buffers;
+using Internal.Common;
+using Internal.Proto;
+using Internal.Proto.BinaryTuple;
+using Internal.Sql;
+using NodaTime;
+
+/// <summary>
+/// Reads a forward-only stream of rows from an Ignite result set.
+/// </summary>
+[SuppressMessage("Design", "CA1010:Generic interface should also be implemented", Justification = "Generic IEnumerable is not applicable.")]
+[SuppressMessage("Usage", "CA2215:Dispose methods should call base class dispose", Justification = "Base class dispose is empty.")]
+public sealed class IgniteDbDataReader : DbDataReader, IDbColumnSchemaGenerator
+{
+ private static readonly Task<bool> TrueTask = Task.FromResult(true);
+
+ private readonly ResultSet<object> _resultSet;
+
+ private readonly IAsyncEnumerator<PooledBuffer> _pageEnumerator;
+
+ private int _pageRowCount = -1;
+
+ private int _pageRowIndex = -1;
+
+ private int _pageRowOffset = -1;
+
+ private int _pageRowSize = -1;
+
+ private ReadOnlyCollection<DbColumn>? _schema;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgniteDbDataReader"/> class.
+ /// </summary>
+ /// <param name="resultSet">Result set.</param>
+ internal IgniteDbDataReader(ResultSet<object> resultSet)
+ {
+ Debug.Assert(resultSet.HasRowSet, "_resultSet.HasRowSet");
+
+ _resultSet = resultSet;
+
+ _pageEnumerator = _resultSet.EnumeratePagesInternal().GetAsyncEnumerator();
+ }
+
+ /// <inheritdoc/>
+ public override int FieldCount => Metadata.Columns.Count;
+
+ /// <inheritdoc/>
+ public override int RecordsAffected => checked((int)_resultSet.AffectedRows);
+
+ /// <inheritdoc/>
+ public override bool HasRows => _resultSet.HasRowSet;
+
+ /// <inheritdoc/>
+ public override bool IsClosed => _resultSet.IsDisposed;
+
+ /// <summary>
+ /// Gets a value indicating the depth of nesting for the current row. Always zero in Ignite.
+ /// </summary>
+ /// <returns>The level of nesting.</returns>
+ public override int Depth => 0;
+
+ /// <summary>
+ /// Gets Ignite-specific result set metadata.
+ /// </summary>
+ public IResultSetMetadata Metadata => _resultSet.Metadata!;
+
+ /// <inheritdoc/>
+ public override object this[int ordinal] => GetValue(ordinal);
+
+ /// <inheritdoc/>
+ [SuppressMessage(
+ "Design",
+ "CA1065:Do not raise exceptions in unexpected locations",
+ Justification = "Indexer must raise an exception on invalid column name.")]
+ public override object this[string name] =>
+ Metadata.IndexOf(name) is var index and >= 0
+ ? GetValue(index)
+ : throw new InvalidOperationException($"Column '{name}' is not present in this reader.");
+
+ /// <inheritdoc />
+ public override bool GetBoolean(int ordinal) => GetReader(ordinal, typeof(bool)).GetByteAsBool(ordinal);
+
+ /// <inheritdoc/>
+ public override byte GetByte(int ordinal) => Metadata.Columns[ordinal] switch
+ {
+ var c when c.Type.IsAnyInt() => unchecked((byte)GetReader().GetByte(ordinal)),
+ var c => throw GetInvalidColumnTypeException(typeof(byte), c)
+ };
+
+ /// <inheritdoc/>
+ public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length)
+ {
+ if (dataOffset is < 0 or > int.MaxValue)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(dataOffset),
+ dataOffset,
+ $"{nameof(dataOffset)} must be between {0} and {int.MaxValue}");
+ }
+
+ if (buffer != null && (bufferOffset < 0 || bufferOffset >= buffer.Length + 1))
+ {
+ throw new ArgumentOutOfRangeException($"{nameof(bufferOffset)} must be between {0} and {(buffer.Length)}");
+ }
+
+ if (buffer != null && (length < 0 || length > buffer.Length - bufferOffset))
+ {
+ throw new ArgumentOutOfRangeException($"{nameof(length)} must be between {0} and {buffer.Length - bufferOffset}");
+ }
+
+ var span = GetReader(ordinal, typeof(byte[])).GetBytesSpan(ordinal);
+
+ if (buffer == null)
+ {
+ return span.Length;
+ }
+
+ var slice = span.Slice(checked((int)dataOffset), length);
+ slice.CopyTo(buffer);
+
+ return slice.Length;
+ }
+
+ /// <inheritdoc/>
+ public override char GetChar(int ordinal) => throw new NotSupportedException("char data type is not supported");
+
+ /// <inheritdoc/>
+ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length)
+ {
+ if (dataOffset is < 0 or > int.MaxValue)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(dataOffset),
+ dataOffset,
+ $"{nameof(dataOffset)} must be between {0} and {int.MaxValue}");
+ }
+
+ if (buffer != null && (bufferOffset < 0 || bufferOffset >= buffer.Length + 1))
+ {
+ throw new ArgumentOutOfRangeException($"{nameof(bufferOffset)} must be between {0} and {(buffer.Length)}");
+ }
+
+ if (buffer != null && (length < 0 || length > buffer.Length - bufferOffset))
+ {
+ throw new ArgumentOutOfRangeException($"{nameof(length)} must be between {0} and {buffer.Length - bufferOffset}");
+ }
+
+ var span = GetReader(ordinal, typeof(string)).GetBytesSpan(ordinal);
+
+ if (buffer == null)
+ {
+ return ProtoCommon.StringEncoding.GetCharCount(span);
+ }
+
+ return ProtoCommon.StringEncoding.GetChars(
+ span.Slice(checked((int)dataOffset)),
+ buffer.AsSpan().Slice(bufferOffset, length));
+ }
+
+ /// <inheritdoc/>
+ public override string GetDataTypeName(int ordinal) => Metadata.Columns[ordinal].Type.ToSqlTypeName();
+
+ /// <inheritdoc/>
+ public override DateTime GetDateTime(int ordinal)
+ {
+ var column = Metadata.Columns[ordinal];
+
+ return column.Type switch
+ {
+ SqlColumnType.Date => GetReader().GetDate(ordinal).ToDateTimeUnspecified(),
+ SqlColumnType.Datetime => GetReader().GetDateTime(ordinal).ToDateTimeUnspecified(),
+ SqlColumnType.Timestamp => GetReader().GetTimestamp(ordinal).ToDateTimeUtc(),
+ _ => throw GetInvalidColumnTypeException(typeof(DateTime), column)
+ };
+ }
+
+ /// <inheritdoc/>
+ public override decimal GetDecimal(int ordinal)
+ {
+ var column = Metadata.Columns[ordinal];
+
+ ValidateColumnType(typeof(decimal), column);
+
+ return GetReader().GetDecimal(ordinal, column.Scale);
+ }
+
+ /// <inheritdoc/>
+ public override double GetDouble(int ordinal) => Metadata.Columns[ordinal] switch
+ {
+ var c when c.Type.IsAnyFloat() => GetReader().GetDouble(ordinal),
+ var c => throw GetInvalidColumnTypeException(typeof(double), c)
+ };
+
+ /// <inheritdoc/>
+ public override float GetFloat(int ordinal) => Metadata.Columns[ordinal] switch
+ {
+ var c when c.Type.IsAnyFloat() => GetReader().GetFloat(ordinal),
+ var c => throw GetInvalidColumnTypeException(typeof(float), c)
+ };
+
+ /// <inheritdoc/>
+ public override Guid GetGuid(int ordinal) => GetReader(ordinal, typeof(Guid)).GetGuid(ordinal);
+
+ /// <inheritdoc/>
+ public override short GetInt16(int ordinal) => Metadata.Columns[ordinal] switch
+ {
+ var c when c.Type.IsAnyInt() => GetReader().GetShort(ordinal),
+ var c => throw GetInvalidColumnTypeException(typeof(short), c)
+ };
+
+ /// <inheritdoc/>
+ public override int GetInt32(int ordinal) => Metadata.Columns[ordinal] switch
+ {
+ var c when c.Type.IsAnyInt() => GetReader().GetInt(ordinal),
+ var c => throw GetInvalidColumnTypeException(typeof(int), c)
+ };
+
+ /// <inheritdoc/>
+ public override long GetInt64(int ordinal) => Metadata.Columns[ordinal] switch
+ {
+ var c when c.Type.IsAnyInt() => GetReader().GetLong(ordinal),
+ var c => throw GetInvalidColumnTypeException(typeof(long), c)
+ };
+
+ /// <inheritdoc/>
+ public override string GetName(int ordinal) => Metadata.Columns[ordinal].Name;
+
+ /// <inheritdoc/>
+ public override int GetOrdinal(string name) => Metadata.IndexOf(name);
+
+ /// <inheritdoc/>
+ public override string GetString(int ordinal) => GetReader(ordinal, typeof(string)).GetString(ordinal);
+
+ /// <inheritdoc/>
+ public override object GetValue(int ordinal)
+ {
+ var reader = GetReader();
+
+ return Sql.ReadColumnValue(ref reader, Metadata.Columns[ordinal], ordinal)!;
+ }
+
+ /// <inheritdoc/>
+ public override int GetValues(object[] values)
+ {
+ IgniteArgumentCheck.NotNull(values, nameof(values));
+
+ var cols = Metadata.Columns;
+ var count = Math.Min(values.Length, cols.Count);
+
+ var reader = GetReader();
+
+ for (int i = 0; i < count; i++)
+ {
+ values[i] = Sql.ReadColumnValue(ref reader, cols[i], i)!;
+ }
+
+ return count;
+ }
+
+ /// <inheritdoc/>
+ public override bool IsDBNull(int ordinal) => GetReader().IsNull(ordinal);
+
+ /// <inheritdoc/>
+ public override bool NextResult() => throw new NotSupportedException("Batched result sets are not supported.");
+
+ /// <inheritdoc/>
+ public override bool Read() => ReadNextRowInCurrentPage() || FetchNextPage().GetAwaiter().GetResult();
+
+ /// <inheritdoc/>
+ public override Task<bool> ReadAsync(CancellationToken cancellationToken) => ReadNextRowInCurrentPage() ? TrueTask : FetchNextPage();
+
+ /// <inheritdoc/>
+ public override IEnumerator GetEnumerator() => new DbEnumerator(this);
+
+ /// <inheritdoc/>
+ public ReadOnlyCollection<DbColumn> GetColumnSchema()
+ {
+ if (_schema == null)
+ {
+ var schema = new List<DbColumn>(FieldCount);
+
+ for (var i = 0; i < Metadata.Columns.Count; i++)
+ {
+ schema.Add(new IgniteDbColumn(Metadata.Columns[i], i));
+ }
+
+ _schema = schema.AsReadOnly();
+ }
+
+ return _schema;
+ }
+
+ /// <inheritdoc/>
+ public override DataTable GetSchemaTable()
+ {
+ var table = new DataTable("SchemaTable");
+
+ table.Columns.Add(SchemaTableColumn.ColumnName, typeof(string));
+ table.Columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int));
+ table.Columns.Add(SchemaTableColumn.ColumnSize, typeof(int));
+ table.Columns.Add(SchemaTableColumn.NumericPrecision, typeof(int));
+ table.Columns.Add(SchemaTableColumn.NumericScale, typeof(int));
+ table.Columns.Add(SchemaTableColumn.IsUnique, typeof(bool));
+ table.Columns.Add(SchemaTableColumn.IsKey, typeof(bool));
+ table.Columns.Add(SchemaTableColumn.BaseColumnName, typeof(string));
+ table.Columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string));
+ table.Columns.Add(SchemaTableColumn.BaseTableName, typeof(string));
+ table.Columns.Add(SchemaTableColumn.DataType, typeof(Type));
+ table.Columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool));
+ table.Columns.Add(SchemaTableColumn.ProviderType, typeof(int));
+ table.Columns.Add(SchemaTableColumn.IsAliased, typeof(bool));
+ table.Columns.Add(SchemaTableColumn.IsExpression, typeof(bool));
+ table.Columns.Add(SchemaTableColumn.IsLong, typeof(bool));
+
+ foreach (var column in GetColumnSchema())
+ {
+ var row = table.NewRow();
+
+ row[SchemaTableColumn.ColumnName] = column.ColumnName;
+ row[SchemaTableColumn.ColumnOrdinal] = column.ColumnOrdinal ?? -1;
+ row[SchemaTableColumn.ColumnSize] = column.ColumnSize ?? -1;
+ row[SchemaTableColumn.NumericPrecision] = column.NumericPrecision ?? 0;
+ row[SchemaTableColumn.NumericScale] = column.NumericScale ?? 0;
+ row[SchemaTableColumn.IsUnique] = column.IsUnique == true;
+ row[SchemaTableColumn.IsKey] = column.IsKey == true;
+ row[SchemaTableColumn.BaseColumnName] = column.BaseColumnName;
+ row[SchemaTableColumn.BaseSchemaName] = column.BaseSchemaName;
+ row[SchemaTableColumn.BaseTableName] = column.BaseTableName;
+ row[SchemaTableColumn.DataType] = column.DataType;
+ row[SchemaTableColumn.AllowDBNull] = column.AllowDBNull == null ? DBNull.Value : column.AllowDBNull.Value;
+ row[SchemaTableColumn.ProviderType] = (int)((IgniteDbColumn)column).ColumnMetadata.Type;
+ row[SchemaTableColumn.IsAliased] = column.IsAliased == true;
+ row[SchemaTableColumn.IsExpression] = column.IsExpression == true;
+ row[SchemaTableColumn.IsLong] = column.IsLong == true;
+
+ table.Rows.Add(row);
+ }
+
+ return table;
+ }
+
+ /// <inheritdoc/>
+ public override async ValueTask DisposeAsync()
+ {
+ await _pageEnumerator.DisposeAsync().ConfigureAwait(false);
+ await _resultSet.DisposeAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc/>
+ public override void Close() => Dispose();
+
+ /// <inheritdoc/>
+ public override Task CloseAsync() => DisposeAsync().AsTask();
+
+ /// <inheritdoc/>
+ public override T GetFieldValue<T>(int ordinal)
+ {
+ if (typeof(T) == typeof(string))
+ {
+ return (T)(object)GetString(ordinal);
+ }
+
+ if (typeof(T) == typeof(int))
+ {
+ return (T)(object)GetInt32(ordinal);
+ }
+
+ if (typeof(T) == typeof(long))
+ {
+ return (T)(object)GetInt64(ordinal);
+ }
+
+ if (typeof(T) == typeof(short))
+ {
+ return (T)(object)GetInt16(ordinal);
+ }
+
+ if (typeof(T) == typeof(float))
+ {
+ return (T)(object)GetFloat(ordinal);
+ }
+
+ if (typeof(T) == typeof(double))
+ {
+ return (T)(object)GetDouble(ordinal);
+ }
+
+ if (typeof(T) == typeof(decimal))
+ {
+ return (T)(object)GetDecimal(ordinal);
+ }
+
+ if (typeof(T) == typeof(byte))
+ {
+ return (T)(object)GetByte(ordinal);
+ }
+
+ if (typeof(T) == typeof(byte[]))
+ {
+ return (T)(object)GetReader(ordinal, typeof(byte[])).GetBytes(ordinal);
+ }
+
+ if (typeof(T) == typeof(LocalTime))
+ {
+ return (T)(object)GetReader(ordinal, typeof(LocalTime)).GetTime(ordinal);
+ }
+
+ if (typeof(T) == typeof(LocalDate))
+ {
+ return (T)(object)GetReader(ordinal, typeof(LocalDate)).GetDate(ordinal);
+ }
+
+ if (typeof(T) == typeof(LocalDateTime))
+ {
+ return (T)(object)GetReader(ordinal, typeof(LocalDateTime)).GetDateTime(ordinal);
+ }
+
+ if (typeof(T) == typeof(DateTime))
+ {
+ return (T)(object)GetDateTime(ordinal);
+ }
+
+ if (typeof(T) == typeof(Instant))
+ {
+ return (T)(object)GetReader(ordinal, typeof(Instant)).GetTimestamp(ordinal);
+ }
+
+ throw GetInvalidColumnTypeException(typeof(T), Metadata.Columns[ordinal]);
+ }
+
+ /// <inheritdoc/>
+ public override Type GetFieldType(int ordinal) => Metadata.Columns[ordinal].Type.ToClrType();
+
+ /// <inheritdoc/>
+ protected override void Dispose(bool disposing) => DisposeAsync().AsTask().GetAwaiter().GetResult();
+
+ private static void ValidateColumnType(Type type, IColumnMetadata column)
+ {
+ if (column.Type != type.ToSqlColumnType())
+ {
+ throw GetInvalidColumnTypeException(type, column);
+ }
+ }
+
+ private static InvalidCastException GetInvalidColumnTypeException(Type type, IColumnMetadata column) =>
+ new($"Column {column.Name} of type {column.Type.ToSqlTypeName()} ({column.Type.ToClrType()}) can not be cast to {type}.");
+
+ private BinaryTupleReader GetReader(int ordinal, Type type)
+ {
+ var column = Metadata.Columns[ordinal];
+
+ ValidateColumnType(type, column);
+
+ return GetReader();
+ }
+
+ private BinaryTupleReader GetReader()
+ {
+ if (_pageRowCount < 0)
+ {
+ throw new InvalidOperationException(
+ $"No data exists for the row/column. Reading has not started. Call {nameof(ReadAsync)} or {nameof(Read)}.");
+ }
+
+ var reader = _pageEnumerator.Current.GetReader(_pageRowOffset);
+ var tupleSpan = reader.ReadBinary();
+
+ return new BinaryTupleReader(tupleSpan, FieldCount);
+ }
+
+ private bool ReadNextRowInCurrentPage()
+ {
+ if (_pageRowCount <= 0 || _pageRowIndex >= _pageRowCount - 1)
+ {
+ return false;
+ }
+
+ _pageRowIndex++;
+ _pageRowOffset += _pageRowSize;
+
+ var reader = _pageEnumerator.Current.GetReader(_pageRowOffset);
+ _pageRowSize = reader.ReadBinaryHeader() + reader.Consumed;
+
+ return true;
+ }
+
+ private async Task<bool> FetchNextPage()
+ {
+ if (!await _pageEnumerator.MoveNextAsync().ConfigureAwait(false))
+ {
+ return false;
+ }
+
+ ReadFirstRowInCurrentPage();
+
+ return true;
+
+ void ReadFirstRowInCurrentPage()
+ {
+ var reader = _pageEnumerator.Current.GetReader();
+
+ _pageRowCount = reader.ReadArrayHeader();
+ _pageRowOffset = reader.Consumed;
+ _pageRowSize = reader.ReadBinaryHeader() + reader.Consumed - _pageRowOffset;
+ _pageRowIndex = 0;
+ }
+ }
+}
diff --git a/modules/platforms/dotnet/DEVNOTES.md b/modules/platforms/dotnet/DEVNOTES.md
index 5928d00a8a..ddd5d9a7b6 100644
--- a/modules/platforms/dotnet/DEVNOTES.md
+++ b/modules/platforms/dotnet/DEVNOTES.md
@@ -14,14 +14,18 @@ In this dir: `dotnet build`
## Run Tests
In this dir: `dotnet test --logger "console;verbosity=normal"`
+
Specific test: `dotnet test --logger "console;verbosity=normal" --filter ClientSocketTests`
## Start a Test Node
-* `gradlew :ignite-runner:runnerPlatformTest --no-daemon`
+`gradlew :ignite-runner:runnerPlatformTest --no-daemon`
To debug or profile Java side of the tests, run `org.apache.ignite.internal.runner.app.PlatformTestNodeRunner` class in IDEA with a debugger or profiler,
then run .NET tests with `dotnet test` or `dotnet test --filter TEST_NAME`. When a server node is present, .NET tests will use it instead of starting a new one.
+The test node will stop after 30 minutes by default.
+To change this, set `IGNITE_PLATFORM_TEST_NODE_RUNNER_RUN_TIME_MINUTES` environment variable.
+
## Static Code Analysis
Static code analysis (Roslyn-based) runs as part of the build and includes code style check. Build fails on any warning.