blob: c5ff7b5277f571ac46b0572f3899c68da961e122 [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.Sql
{
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Ignite.Sql;
using Ignite.Table;
using Ignite.Transactions;
using Microsoft.Extensions.Logging.Abstractions;
using NodaTime;
using NUnit.Framework;
/// <summary>
/// Tests for SQL API: <see cref="ISql"/>.
/// </summary>
[SuppressMessage("ReSharper", "NotDisposedResource", Justification = "Tests")]
[SuppressMessage("ReSharper", "NotDisposedResourceIsReturned", Justification = "Tests")]
public class SqlTests : IgniteTestsBase
{
[OneTimeSetUp]
public async Task CreateTestTable()
{
var createRes = await Client.Sql.ExecuteAsync(null, "CREATE TABLE IF NOT EXISTS TEST(ID INT PRIMARY KEY, VAL VARCHAR)");
if (!createRes.WasApplied)
{
await Client.Sql.ExecuteAsync(null, "DELETE FROM TEST");
}
for (var i = 0; i < 10; i++)
{
await Client.Sql.ExecuteAsync(null, "INSERT INTO TEST VALUES (?, ?)", i, "s-" + i);
}
}
[OneTimeTearDown]
public async Task DeleteTestTables()
{
await Client.Sql.ExecuteAsync(null, "DROP TABLE TEST");
await Client.Sql.ExecuteAsync(null, "DROP TABLE IF EXISTS TestDdlDml");
await Client.Sql.ExecuteAsync(null, "DROP TABLE IF EXISTS TestExecuteScript");
}
[SetUp]
public async Task ResetData()
{
await Client.Sql.ExecuteScriptAsync("DELETE FROM TEST WHERE ID >= 10");
}
[Test]
public async Task TestSimpleQuery()
{
await using IResultSet<IIgniteTuple> resultSet = await Client.Sql.ExecuteAsync(null, "select 1 as num, 'hello' as str");
var rows = await resultSet.ToListAsync();
Assert.AreEqual(-1, resultSet.AffectedRows);
Assert.IsFalse(resultSet.WasApplied);
Assert.IsTrue(resultSet.HasRowSet);
Assert.IsNotNull(resultSet.Metadata);
Assert.AreEqual("NUM", resultSet.Metadata!.Columns[0].Name);
Assert.AreEqual("STR", resultSet.Metadata!.Columns[1].Name);
Assert.AreEqual(0, resultSet.Metadata!.IndexOf("NUM"));
Assert.AreEqual(1, resultSet.Metadata!.IndexOf("STR"));
Assert.AreEqual(
"ResultSetMetadata { Columns = [ " +
"ColumnMetadata { Name = NUM, Type = Int32, Precision = 10, Scale = 0, Nullable = False, Origin = }, " +
"ColumnMetadata { Name = STR, Type = String, Precision = 5, Scale = -2147483648, Nullable = False, Origin = } ] }",
resultSet.Metadata.ToString());
Assert.AreEqual(1, rows.Count);
Assert.AreEqual("IgniteTuple { NUM = 1, STR = hello }", rows[0].ToString());
}
[Test]
public async Task TestNullArgs()
{
await using IResultSet<IIgniteTuple> resultSet = await Client.Sql.ExecuteAsync(
transaction: null,
statement: "select 1",
args: null);
var rows = await resultSet.ToListAsync();
Assert.AreEqual(1, rows.Single()[0]);
}
[Test]
public async Task TestNullArg()
{
await Client.Sql.ExecuteAsync(null, "insert into TEST (ID, VAL) VALUES (-1, NULL)");
await using IResultSet<IIgniteTuple> resultSet = await Client.Sql.ExecuteAsync(
transaction: null,
statement: "select ID from TEST where VAL = ?",
args: new object?[] { null });
var rows = await resultSet.ToListAsync();
await using IResultSet<IIgniteTuple> resultSet2 = await Client.Sql.ExecuteAsync(
transaction: null,
statement: "select ID from TEST where VAL is null");
var rows2 = await resultSet2.ToListAsync();
CollectionAssert.IsEmpty(rows);
Assert.AreEqual(1, rows2.Count);
}
[Test]
public async Task TestGetAllMultiplePages([Values(1, 2, 3, 4, 5, 6)] int pageSize)
{
var statement = new SqlStatement("SELECT ID, VAL FROM TEST ORDER BY VAL", pageSize: pageSize);
await using var resultSet = await Client.Sql.ExecuteAsync(null, statement);
var rows = await resultSet.ToListAsync();
Assert.AreEqual(10, rows.Count);
Assert.AreEqual("IgniteTuple { ID = 0, VAL = s-0 }", rows[0].ToString());
Assert.AreEqual("IgniteTuple { ID = 9, VAL = s-9 }", rows[9].ToString());
}
[Test]
public async Task TestToDictionary()
{
var statement = new SqlStatement("SELECT ID, VAL FROM TEST WHERE VAL IS NOT NULL ORDER BY VAL", pageSize: 2);
await using var resultSet = await Client.Sql.ExecuteAsync(null, statement);
Dictionary<int, string> res = await resultSet.ToDictionaryAsync(x => (int)x["ID"]!, x => (string)x["VAL"]!);
Assert.AreEqual(10, res.Count);
Assert.AreEqual("s-3", res[3]);
}
[Test]
public async Task TestToDictionaryCustomComparer()
{
await using var resultSet = await Client.Sql.ExecuteAsync(null, "SELECT ID, VAL FROM TEST WHERE VAL IS NOT NULL ORDER BY VAL");
Dictionary<string, int> res = await resultSet.ToDictionaryAsync(
x => (string)x["VAL"]!,
x => (int)x["ID"]!,
StringComparer.OrdinalIgnoreCase);
Assert.AreEqual(10, res.Count);
Assert.AreEqual(3, res["s-3"]);
Assert.AreEqual(3, res["S-3"]);
Assert.IsFalse(res.ContainsKey("x-3"));
}
[Test]
public async Task TestCollect()
{
var statement = new SqlStatement("SELECT ID, VAL FROM TEST WHERE VAL IS NOT NULL ORDER BY VAL", pageSize: 2);
await using var resultSet = await Client.Sql.ExecuteAsync(null, statement);
HashSet<int> res = await resultSet.CollectAsync(capacity => new HashSet<int>(capacity), (set, row) => set.Add((int)row["ID"]!));
Assert.AreEqual(10, res.Count);
Assert.IsTrue(res.Contains(1));
Assert.IsFalse(res.Contains(111));
}
[Test]
public async Task TestExists()
{
await using var resultSet = await Client.Sql.ExecuteAsync(null, "SELECT EXISTS (SELECT 1 FROM TEST WHERE ID > 1)");
var rows = await resultSet.ToListAsync();
Assert.AreEqual(1, rows.Count);
Assert.AreEqual(true, rows[0][0]);
}
[Test]
public async Task TestEnumerateMultiplePages()
{
var statement = new SqlStatement("SELECT ID, VAL FROM TEST ORDER BY VAL", pageSize: 4);
await using var resultSet = await Client.Sql.ExecuteAsync(null, statement);
var i = 0;
await foreach (var row in resultSet)
{
Assert.AreEqual(i, row["ID"]);
Assert.AreEqual("s-" + i, row["VAL"]);
i++;
}
Assert.AreEqual(10, i);
}
[Test]
public async Task TestLinqAsyncMultiplePages()
{
var statement = new SqlStatement("SELECT ID, VAL FROM TEST ORDER BY VAL", pageSize: 4);
await using var resultSet = await Client.Sql.ExecuteAsync(null, statement);
var rows = await resultSet.Where(x => (int)x["ID"]! < 5).ToArrayAsync();
Assert.AreEqual(5, rows.Length);
Assert.AreEqual("IgniteTuple { ID = 0, VAL = s-0 }", rows[0].ToString());
Assert.AreEqual("IgniteTuple { ID = 4, VAL = s-4 }", rows[4].ToString());
}
[Test]
public async Task TestMultipleEnumerationThrows()
{
// GetAll -> GetAsyncEnumerator.
await using var resultSet = await Client.Sql.ExecuteAsync(null, "SELECT 1");
await resultSet.ToListAsync();
var ex = Assert.Throws<IgniteClientException>(() => resultSet.GetAsyncEnumerator());
Assert.AreEqual("Query result set can not be iterated more than once.", ex!.Message);
Assert.ThrowsAsync<IgniteClientException>(async () => await resultSet.ToListAsync());
// GetAsyncEnumerator -> GetAll.
await using var resultSet2 = await Client.Sql.ExecuteAsync(null, "SELECT 1");
_ = resultSet2.GetAsyncEnumerator();
Assert.ThrowsAsync<IgniteClientException>(async () => await resultSet2.ToListAsync());
Assert.Throws<IgniteClientException>(() => resultSet2.GetAsyncEnumerator());
}
[Test]
public async Task TestIterateAfterDisposeThrows()
{
var statement = new SqlStatement("SELECT ID, VAL FROM TEST ORDER BY VAL", pageSize: 1);
var resultSet = await Client.Sql.ExecuteAsync(null, statement);
var resultSet2 = await Client.Sql.ExecuteAsync(null, statement);
await resultSet.DisposeAsync();
await resultSet2.DisposeAsync();
Assert.ThrowsAsync<ObjectDisposedException>(async () => await resultSet.ToListAsync());
var enumerator = resultSet2.GetAsyncEnumerator();
Assert.ThrowsAsync<ObjectDisposedException>(async () => await enumerator.MoveNextAsync());
}
[Test]
public async Task TestMultipleDisposeIsAllowed()
{
var statement = new SqlStatement("SELECT ID, VAL FROM TEST ORDER BY VAL", pageSize: 1);
await using var resultSet = await Client.Sql.ExecuteAsync(null, statement);
resultSet.Dispose();
await resultSet.DisposeAsync();
}
[Test]
public async Task TestPutSqlGetKv()
{
var table = await Client.Tables.GetTableAsync("TEST");
var res = await table!.RecordBinaryView.GetAsync(null, new IgniteTuple { ["ID"] = 1 });
Assert.AreEqual("s-1", res.Value["VAL"]);
}
[Test]
public async Task TestDdlDml()
{
// Create table.
await using var createRes = await Client.Sql.ExecuteAsync(null, "CREATE TABLE TestDdlDml(ID INT PRIMARY KEY, VAL VARCHAR)");
Assert.IsTrue(createRes.WasApplied);
Assert.AreEqual(-1, createRes.AffectedRows);
Assert.IsFalse(createRes.HasRowSet);
Assert.Throws<IgniteClientException>(() => createRes.GetAsyncEnumerator());
Assert.ThrowsAsync<IgniteClientException>(async () => await createRes.ToListAsync());
Assert.IsNull(createRes.Metadata);
// Insert data.
for (var i = 0; i < 10; i++)
{
await using var insertRes = await Client.Sql.ExecuteAsync(null, "INSERT INTO TestDdlDml VALUES (?, ?)", i, "hello " + i);
Assert.IsFalse(insertRes.HasRowSet);
Assert.IsFalse(insertRes.WasApplied);
Assert.IsNull(insertRes.Metadata);
Assert.AreEqual(1, insertRes.AffectedRows);
}
// Query data.
await using var selectRes = await Client.Sql.ExecuteAsync(null, "SELECT VAL as MYVALUE, ID, ID + 1 FROM TestDdlDml ORDER BY ID");
Assert.IsTrue(selectRes.HasRowSet);
Assert.IsFalse(selectRes.WasApplied);
Assert.AreEqual(-1, selectRes.AffectedRows);
var columns = selectRes.Metadata!.Columns;
Assert.AreEqual(3, columns.Count);
Assert.AreEqual("MYVALUE", columns[0].Name);
Assert.AreEqual("VAL", columns[0].Origin!.ColumnName);
Assert.AreEqual("PUBLIC", columns[0].Origin!.SchemaName);
Assert.AreEqual("TESTDDLDML", columns[0].Origin!.TableName);
Assert.IsTrue(columns[0].Nullable);
Assert.AreEqual(ColumnType.String, columns[0].Type);
Assert.AreEqual(int.MinValue, columns[0].Scale);
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);
// Update data.
await using var updateRes = await Client.Sql.ExecuteAsync(null, "UPDATE TESTDDLDML SET VAL='upd' WHERE ID < 5");
Assert.IsFalse(updateRes.WasApplied);
Assert.IsFalse(updateRes.HasRowSet);
Assert.IsNull(updateRes.Metadata);
Assert.AreEqual(5, updateRes.AffectedRows);
// Drop table.
await using var deleteRes = await Client.Sql.ExecuteAsync(null, "DROP TABLE TESTDDLDML");
Assert.IsFalse(deleteRes.HasRowSet);
Assert.IsNull(deleteRes.Metadata);
Assert.IsTrue(deleteRes.WasApplied);
}
[Test]
public void TestInvalidTableNameInSqlThrowsException()
{
var ex = Assert.ThrowsAsync<SqlException>(async () => await Client.Sql.ExecuteAsync(null, "select x from bad"));
StringAssert.Contains("From line 1, column 15 to line 1, column 17: Object 'BAD' not found", ex!.Message);
}
[Test]
public void TestInvalidSqlThrowsException()
{
var ex = Assert.ThrowsAsync<SqlException>(async () => await Client.Sql.ExecuteAsync(null, "foo bar baz"));
var inner = (SqlException)ex!.InnerException!;
Assert.AreEqual(
"Invalid query, check inner exceptions for details: SqlStatement { " +
"Query = foo bar baz, Timeout = 00:00:00, Schema = PUBLIC, PageSize = 1024, Properties = { } }",
ex.Message);
Assert.AreEqual(
"Failed to parse query: Non-query expression encountered in illegal context. At line 1, column 1",
inner.Message);
}
[Test]
public void TestCreateTableExistsThrowsException()
{
var ex = Assert.ThrowsAsync<SqlException>(
async () => await Client.Sql.ExecuteAsync(null, "CREATE TABLE TEST(ID INT PRIMARY KEY)"));
StringAssert.Contains("Table with name 'PUBLIC.TEST' already exists", ex!.Message);
}
[Test]
public void TestAlterTableNotFoundThrowsException()
{
var ex = Assert.ThrowsAsync<SqlException>(
async () => await Client.Sql.ExecuteAsync(null, "ALTER TABLE NOT_EXISTS_TABLE ADD COLUMN VAL1 VARCHAR"));
StringAssert.Contains("Table with name 'PUBLIC.NOT_EXISTS_TABLE' not found", ex!.Message);
}
[Test]
public void TestAlterTableColumnExistsThrowsException()
{
var ex = Assert.ThrowsAsync<SqlException>(
async () => await Client.Sql.ExecuteAsync(null, "ALTER TABLE TEST ADD COLUMN ID INT"));
StringAssert.Contains("Failed to validate query. Column with name 'ID' already exists", ex!.Message);
}
[Test]
public async Task TestStatementProperties()
{
using var server = new FakeServer();
using var client = await server.ConnectClientAsync();
var sqlStatement = new SqlStatement(
query: "SELECT PROPS",
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);
var rows = await res.ToListAsync();
var props = rows.ToDictionary(x => (string)x["NAME"]!, x => (string)x["VAL"]!);
Assert.IsTrue(res.HasRowSet);
Assert.AreEqual(9, props.Count);
Assert.AreEqual("schema-1", props["schema"]);
Assert.AreEqual("987", props["pageSize"]);
Assert.AreEqual("123000", props["timeoutMs"]);
Assert.AreEqual("SELECT PROPS", props["sql"]);
Assert.AreEqual("10", props["prop1"]);
Assert.AreEqual("xyz", props["prop-2"]);
Assert.AreEqual("Europe/London", props["timeZoneId"]);
}
[Test]
public async Task TestResultSetToString()
{
await using IResultSet<IIgniteTuple> resultSet = await Client.Sql.ExecuteAsync(null, "select 1 as num, 'hello' as str");
Assert.AreEqual(
"ResultSet`1[IIgniteTuple] { HasRowSet = True, AffectedRows = -1, WasApplied = False, " +
"Metadata = ResultSetMetadata { Columns = [ " +
"ColumnMetadata { Name = NUM, Type = Int32, Precision = 10, Scale = 0, Nullable = False, Origin = }, " +
"ColumnMetadata { Name = STR, Type = String, Precision = 5, Scale = -2147483648, Nullable = False, Origin = } ] } }",
resultSet.ToString());
}
[Test]
public async Task TestSelectNull()
{
await using IResultSet<IIgniteTuple> resultSet = await Client.Sql.ExecuteAsync(null, "select null");
var rows = await resultSet.ToListAsync();
Assert.AreEqual(1, rows.Count);
Assert.IsNull(rows[0][0]);
Assert.AreEqual(
"ResultSet`1[IIgniteTuple] { HasRowSet = True, AffectedRows = -1, WasApplied = False, " +
"Metadata = ResultSetMetadata { Columns = [ " +
"ColumnMetadata { Name = NULL, Type = Null, Precision = -1, Scale = -2147483648, Nullable = True, Origin = } ] } }",
resultSet.ToString());
}
[Test]
public async Task TestExecuteScript()
{
var id = Random.Shared.Next(100);
await Client.Sql.ExecuteScriptAsync(
"CREATE TABLE TestExecuteScript(ID INT PRIMARY KEY, VAL VARCHAR); " +
"DELETE FROM TestExecuteScript;" +
"INSERT INTO TestExecuteScript VALUES (?, ?); " +
"INSERT INTO TestExecuteScript VALUES (?, ?);",
CancellationToken.None,
id,
"a",
id + 1,
"b");
await using var resultSet = await Client.Sql.ExecuteAsync(null, "SELECT * FROM TESTEXECUTESCRIPT ORDER BY ID");
var rows = await resultSet.ToListAsync();
Assert.AreEqual(2, rows.Count);
Assert.AreEqual($"IgniteTuple {{ ID = {id}, VAL = a }}", rows[0].ToString());
Assert.AreEqual($"IgniteTuple {{ ID = {id + 1}, VAL = b }}", rows[1].ToString());
}
[Test]
public async Task TestScriptProperties()
{
using var server = new FakeServer();
using var client = await server.ConnectClientAsync();
var sqlStatement = new SqlStatement(
query: "SELECT PROPS",
timeout: TimeSpan.FromSeconds(123),
schema: "schema-1",
pageSize: 987,
properties: new Dictionary<string, object?> { { "prop1", 10 }, { "prop-2", "xyz" } },
timeZoneId: "Europe/Nicosia");
await client.Sql.ExecuteScriptAsync(sqlStatement);
var resProps = server.LastSqlScriptProps;
Assert.AreEqual("schema-1", resProps["schema"]);
Assert.AreEqual(987, resProps["pageSize"]);
Assert.AreEqual(123000, resProps["timeoutMs"]);
Assert.AreEqual("SELECT PROPS", resProps["sql"]);
Assert.AreEqual(10, resProps["prop1"]);
Assert.AreEqual("xyz", resProps["prop-2"]);
Assert.AreEqual(sqlStatement.TimeZoneId, resProps["timeZoneId"]);
}
[Test]
public void TestInvalidScriptThrowsSqlException()
{
var ex = Assert.ThrowsAsync<SqlException>(async () => await Client.Sql.ExecuteScriptAsync("CREATE SOMETHING"));
var inner = (SqlException)ex!.InnerException!;
Assert.AreEqual(
"Invalid query, check inner exceptions for details: SqlStatement { " +
"Query = CREATE SOMETHING, " +
"Timeout = 00:00:00, " +
"Schema = PUBLIC, " +
"PageSize = 1024, " +
"Properties = { } }",
ex.Message);
Assert.AreEqual("Failed to parse query: Encountered \"SOMETHING\" at line 1, column 8", inner.Message);
}
[Test]
public async Task TestCustomDecimalScale()
{
await using var resultSet = await Client.Sql.ExecuteAsync(null, "select cast((10 / ?) as decimal(20, 5))", 3m);
IIgniteTuple res = await resultSet.SingleAsync();
var bigDecimal = (BigDecimal)res[0]!;
Assert.AreEqual(3.33333m, bigDecimal.ToDecimal());
Assert.AreEqual(5, bigDecimal.Scale);
}
[Test]
public async Task TestMaxDecimalScale()
{
await using var resultSet = await Client.Sql.ExecuteAsync(null, "select (10 / ?::decimal)", 3m);
IIgniteTuple res = await resultSet.SingleAsync();
var bigDecimal = (BigDecimal)res[0]!;
Assert.AreEqual(32757, bigDecimal.Scale);
Assert.AreEqual(13603, bigDecimal.UnscaledValue.GetByteCount());
StringAssert.StartsWith("3.3333333333", bigDecimal.ToString(CultureInfo.InvariantCulture));
}
[Test]
public async Task TestStatementTimeZoneWithAllZones()
{
using var client = await IgniteClient.StartAsync(GetConfig() with { LoggerFactory = NullLoggerFactory.Instance });
var statement = new SqlStatement("SELECT CURRENT_TIMESTAMP");
ICollection<string> zoneIds = DateTimeZoneProviders.Tzdb.Ids;
var zoneProvider = DateTimeZoneProviders.Tzdb;
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 = (Instant)(await resultSet.SingleAsync())[0]!;
var currentTimeInZone = SystemClock.Instance.GetCurrentInstant();
AssertInstantSimilar(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 = (Instant)(await resultSet.SingleAsync())[0]!;
var expectedTime = SystemClock.Instance.GetCurrentInstant();
AssertInstantSimilar(expectedTime, resTime, $"Offset: {offset}");
}
[Test]
public async Task TestExecuteBatch()
{
long[] res = await Client.Sql.ExecuteBatchAsync(
transaction: null,
statement: "INSERT INTO TEST VALUES (?, ?)",
args: [[100, "x"], [101, "y"], [102, "z"]]);
CollectionAssert.AreEqual(new[] { 1L, 1L, 1L }, res);
await using var resultSet = await Client.Sql.ExecuteAsync(
null, "SELECT ID, VAL FROM TEST WHERE ID >= 100 AND ID <= 102 ORDER BY ID");
List<IIgniteTuple> rows = await resultSet.ToListAsync();
Assert.AreEqual(3, rows.Count);
Assert.AreEqual("IgniteTuple { ID = 100, VAL = x }", rows[0].ToString());
Assert.AreEqual("IgniteTuple { ID = 101, VAL = y }", rows[1].ToString());
Assert.AreEqual("IgniteTuple { ID = 102, VAL = z }", rows[2].ToString());
}
[Test]
public async Task TestExecuteBatchInsertUpdateDelete()
{
long[] insertRes = await Client.Sql.ExecuteBatchAsync(
transaction: null,
statement: "INSERT INTO TEST VALUES (?, ?)",
args: [[100, "x"], [101, "y"], [102, "z"]]);
CollectionAssert.AreEqual(new[] { 1L, 1L, 1L }, insertRes);
long[] updateRes = await Client.Sql.ExecuteBatchAsync(
transaction: null,
statement: "UPDATE TEST SET VAL = ? WHERE ID >= ? AND ID <= ?",
args: [["update1", 100, 101], ["update2", 102, 103]]);
CollectionAssert.AreEqual(new[] { 2L, 1L }, updateRes);
long[] deleteRes = await Client.Sql.ExecuteBatchAsync(
transaction: null,
statement: "DELETE FROM TEST WHERE ID >= ? AND ID <= ?",
args: [[100, 102]]);
CollectionAssert.AreEqual(new[] { 3L }, deleteRes);
}
[Test]
public async Task TestExecuteBatchArgsCollections()
{
var statement = "INSERT INTO TEST VALUES (?, ?)";
// Array.
object[][] arr =
[
[200, "x"],
[201, "y"],
];
await Client.Sql.ExecuteBatchAsync(null, statement, arr);
// List.
List<List<object>> args =
[
[300, "x"],
[301, "y"]
];
await Client.Sql.ExecuteBatchAsync(null, statement, args);
// Lazy.
IEnumerable<IEnumerable<object>> collection = Yield(
Yield<object>(401, "x1"),
Yield<object>(402, "x2"));
await Client.Sql.ExecuteBatchAsync(null, statement, collection);
static IEnumerable<T> Yield<T>(params T[] args)
{
foreach (var arg in args)
{
yield return arg;
}
}
}
[Test]
public async Task TestExecuteBatchWithTx()
{
await using var tx = await Client.Transactions.BeginAsync();
Assert.AreEqual(0, await GetCount(tx));
await Client.Sql.ExecuteBatchAsync(tx, "INSERT INTO TEST VALUES (?, ?)", [[110, "x"], [111, "y"]]);
Assert.AreEqual(1, await GetCount(tx));
Assert.AreEqual(0, await GetCount(null));
async Task<int> GetCount(ITransaction? txn)
{
await using var resultSet = await Client.Sql.ExecuteAsync(txn, "SELECT ID, VAL FROM TEST WHERE ID = 110");
var rows = await resultSet.ToListAsync();
return rows.Count;
}
}
[Test]
public async Task TestExecuteBatchNullArg()
{
var res = await Client.Sql.ExecuteBatchAsync(null, "DELETE FROM TEST WHERE VAL IS NOT DISTINCT FROM ?", [[null]]);
Assert.AreEqual(new[] { 0L }, res);
}
[Test]
public void TestExecuteBatchMissingArgs()
{
var ex = Assert.ThrowsAsync<ArgumentException>(
async () => await Client.Sql.ExecuteBatchAsync(null, "select 1", []));
StringAssert.Contains("Batch arguments must not be empty.", ex.Message);
var ex2 = Assert.ThrowsAsync<ArgumentException>(
async () => await Client.Sql.ExecuteBatchAsync(null, "select 1", [[]]));
StringAssert.Contains("Batch arguments must not contain empty rows.", ex2.Message);
}
[Test]
public void TestExecuteBatchMismatchingArgs()
{
var ex = Assert.ThrowsAsync<ArgumentException>(
async () => await Client.Sql.ExecuteBatchAsync(null, "select 1", [[1], [2, 3]]));
Assert.AreEqual("Inconsistent batch argument size: Expected 1 objects, but got more.", ex.Message);
var ex2 = Assert.ThrowsAsync<ArgumentException>(
async () => await Client.Sql.ExecuteBatchAsync(null, "select 1", [[1, 2], [3]]));
Assert.AreEqual("Inconsistent batch argument size: Expected 2 objects, but got 1.", ex2.Message);
var ex3 = Assert.ThrowsAsync<ArgumentException>(
async () => await Client.Sql.ExecuteBatchAsync(null, "select 1", [[1], []]));
Assert.AreEqual("Inconsistent batch argument size: Expected 1 objects, but got 0.", ex3.Message);
}
[Test]
public void TestExecuteBatchNullArgRow()
{
Assert.ThrowsAsync<ArgumentNullException>(
async () => await Client.Sql.ExecuteBatchAsync(null, "select 1", [[1], null!, [2]]));
}
[Test]
public void TestExecuteBatchInvalidStatement()
{
var ex = Assert.ThrowsAsync<SqlBatchException>(
async () => await Client.Sql.ExecuteBatchAsync(null, "select 1", [[1]]));
Assert.AreEqual("Invalid SQL statement type. Expected [DML] but got QUERY.", ex.Message);
}
[Test]
public async Task TestCancelQueryCursor([Values(true, false)] bool beforeIter)
{
var cts = new CancellationTokenSource();
await using var cursor = await Client.Sql.ExecuteAsync(
transaction: null,
new SqlStatement("SELECT * FROM TEST") { PageSize = 1 },
cts.Token);
if (beforeIter)
{
cts.Cancel();
}
Assert.ThrowsAsync<OperationCanceledException>(async () =>
{
await foreach (var unused in cursor)
{
if (!beforeIter)
{
cts.Cancel();
}
}
});
Assert.IsFalse(TestUtils.HasCallbacks(cts));
}
[Test]
public async Task TestCancelQueryExecute([Values("sql", "sql-mapped", "script", "reader", "batch")] string mode)
{
// Cross join will produce 10^N rows, which takes a while to execute.
var manyRowsQuery = $"select count (*) from ({GenerateCrossJoin(8)})";
using var cts = new CancellationTokenSource();
Task task = mode switch
{
"sql" => Client.Sql.ExecuteAsync(transaction: null, manyRowsQuery, cts.Token),
"sql-mapped" => Client.Sql.ExecuteAsync<int>(transaction: null, manyRowsQuery, cts.Token),
"script" => Client.Sql.ExecuteScriptAsync($"DELETE FROM {TableName} WHERE KEY = ({manyRowsQuery})", cts.Token),
"reader" => Client.Sql.ExecuteReaderAsync(transaction: null, manyRowsQuery, cts.Token),
"batch" => Client.Sql.ExecuteBatchAsync(null, $"DELETE FROM {TableName} WHERE KEY = ({manyRowsQuery}) + ?", [[1]], cts.Token),
_ => throw new ArgumentException("Invalid mode: " + mode)
};
// Wait for request to be sent and callbacks to be registered, then cancel.
TestUtils.WaitForCancellationRegistrations(cts);
await cts.CancelAsync();
var ex = Assert.ThrowsAsync<OperationCanceledException>(async () => await task);
Assert.AreEqual("The query was cancelled while executing.", ex!.Message);
if (mode == "batch")
{
Assert.IsInstanceOf<SqlBatchException>(ex.InnerException);
}
else
{
Assert.IsInstanceOf<SqlException>(ex.InnerException);
}
Assert.IsFalse(TestUtils.HasCallbacks(cts));
}
private static string GenerateCrossJoin(int depth)
{
var sb = new StringBuilder("SELECT ");
for (var i = 1; i <= depth; i++)
{
sb.Append($"t{i}.ID AS ID{i}");
if (i < depth)
{
sb.Append(", ");
}
}
sb.Append(" FROM ");
for (var i = 1; i <= depth; i++)
{
sb.Append($"TEST t{i}");
if (i < depth)
{
sb.Append(" CROSS JOIN ");
}
}
return sb.ToString();
}
private static void AssertInstantSimilar(Instant expected, Instant actual, string message)
{
double deltaSeconds = 10;
var expectedSeconds = expected.ToUnixTimeSeconds();
var actualSeconds = actual.ToUnixTimeSeconds();
var diff = Math.Abs(expectedSeconds - actualSeconds);
if (diff > deltaSeconds)
{
throw new InvalidOperationException(
$"Expected: {expectedSeconds}, actual: {actualSeconds}, diff: {diff / 3600} hours ({message})");
}
}
}
}