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 8e864e6a32 IGNITE-18246 .NET: LINQ: Add support for Intersect, Union,
Except (#1386)
8e864e6a32 is described below
commit 8e864e6a3295f2fd49231265f7c04c83dd316abf
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Mon Nov 28 21:54:57 2022 +0200
IGNITE-18246 .NET: LINQ: Add support for Intersect, Union, Except (#1386)
---
.../dotnet/Apache.Ignite.Tests/JavaServer.cs | 3 +-
.../Linq/LinqSqlGenerationTests.cs | 26 ++++
.../Apache.Ignite.Tests/Linq/LinqTests.GroupBy.cs | 40 ++++++
.../Linq/LinqTests.UnionIntersectExcept.cs | 159 +++++++++++++++++++++
.../Internal/Linq/IgniteQueryExecutor.cs | 11 ++
.../Internal/Linq/IgniteQueryModelVisitor.cs | 87 +++++------
.../Table/Serialization/ReflectionUtils.cs | 11 +-
.../dotnet/Apache.Ignite/Sql/IResultSet.cs | 2 +
8 files changed, 292 insertions(+), 47 deletions(-)
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/JavaServer.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/JavaServer.cs
index 97b5592323..ca60a14a03 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/JavaServer.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/JavaServer.cs
@@ -180,8 +180,7 @@ namespace Apache.Ignite.Tests
var cfg = new IgniteClientConfiguration("127.0.0.1:" + port);
using var client = await IgniteClient.StartAsync(cfg);
- var tables = await client.Tables.GetTablesAsync();
- return tables.Count > 0 ? null : new
InvalidOperationException("No tables found on server");
+ return null;
}
catch (Exception e)
{
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqSqlGenerationTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqSqlGenerationTests.cs
index 0a0ba656fd..4582114324 100644
---
a/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqSqlGenerationTests.cs
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqSqlGenerationTests.cs
@@ -207,6 +207,32 @@ public partial class LinqSqlGenerationTests
ex!.Message);
}
+ [Test]
+ public void TestUnion() =>
+ AssertSql(
+ "select (_T0.KEY + ?), _T0.VAL from PUBLIC.tbl1 as _T0 " +
+ "union (select (_T1.KEY + ?), _T1.VAL from PUBLIC.tbl1 as _T1)",
+ q => q.Select(x => new { Key = x.Key + 1, x.Val })
+ .Union(q.Select(x => new { Key = x.Key + 100, x.Val }))
+ .ToList());
+
+ [Test]
+ public void TestIntersect() =>
+ AssertSql(
+ "select (_T0.KEY + ?), concat(_T0.VAL, ?) from PUBLIC.tbl1 as _T0
" +
+ "intersect (select (_T1.KEY + ?), concat(_T1.VAL, ?) from
PUBLIC.tbl1 as _T1)",
+ q => q.Select(x => new { Key = x.Key + 1, Val = x.Val + "_" })
+ .Intersect(q.Select(x => new { Key = x.Key + 100, Val = x.Val
+ "!" }))
+ .ToList());
+
+ [Test]
+ public void TestExcept() =>
+ AssertSql(
+ "select (_T0.KEY + ?) from PUBLIC.tbl1 as _T0 except (select
(_T1.KEY + ?) from PUBLIC.tbl1 as _T1)",
+ q => q.Select(x => x.Key + 1)
+ .Except(q.Select(x => x.Key + 5))
+ .ToList());
+
[OneTimeSetUp]
public async Task OneTimeSetUp()
{
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.GroupBy.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.GroupBy.cs
index c858d20cb0..7ae5615831 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.GroupBy.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.GroupBy.cs
@@ -19,6 +19,7 @@ namespace Apache.Ignite.Tests.Linq;
using System.Collections.Generic;
using System.Linq;
+using Internal.Linq;
using NUnit.Framework;
/// <summary>
@@ -146,4 +147,43 @@ public partial class LinqTests
"order by (_T0.VAL) asc",
query.ToString());
}
+
+ /// <summary>
+ /// Tests grouping combined with join in a reverse order followed by a
projection to an anonymous type with
+ /// custom projected column names.
+ /// <para />
+ /// Covers <see cref="ExpressionWalker.GetProjectedMember"/>.
+ /// </summary>
+ [Test]
+ public void TestGroupByWithReverseJoinAndAnonymousProjectionWithRename()
+ {
+ var query1 = PocoView.AsQueryable();
+ var query2 = PocoIntView.AsQueryable();
+
+ var query = query1.Join(
+ query2,
+ o => o.Key,
+ p => p.Key,
+ (org, person) => new
+ {
+ Cat = org.Val,
+ Price = person.Val
+ })
+ .GroupBy(x => x.Cat)
+ .Select(g => new {Category = g.Key, MaxPrice = g.Max(x =>
x.Price)})
+ .OrderByDescending(x => x.MaxPrice);
+
+ var res = query.ToList();
+
+ Assert.AreEqual("v-9", res[0].Category);
+ Assert.AreEqual(900, res[0].MaxPrice);
+
+ StringAssert.Contains(
+ "select _T0.VAL, max(_T1.VAL) " +
+ "from PUBLIC.TBL1 as _T0 " +
+ "inner join PUBLIC.TBL_INT32 as _T1 on (cast(_T1.KEY as bigint) =
_T0.KEY) " +
+ "group by (_T0.VAL) " +
+ "order by (max(_T1.VAL)) desc",
+ query.ToString());
+ }
}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.UnionIntersectExcept.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.UnionIntersectExcept.cs
new file mode 100644
index 0000000000..f6bc7a9be2
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.UnionIntersectExcept.cs
@@ -0,0 +1,159 @@
+/*
+ * 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.Linq;
+using NUnit.Framework;
+
+/// <summary>
+/// Linq UNION/INTERSECT/EXCEPT tests.
+/// </summary>
+public partial class LinqTests
+{
+ [Test]
+ public void TestUnion()
+ {
+ var subQuery = PocoView.AsQueryable()
+ .Where(x => x.Key < 2)
+ .Select(x => new { Id = x.Key });
+
+ var query = PocoLongView.AsQueryable()
+ .Where(x => x.Key > 8)
+ .Select(x => new { Id = x.Key })
+ .Union(subQuery);
+
+ var res = query.ToList();
+
+ CollectionAssert.AreEquivalent(new[] { 0, 1, 9 }, res.Select(x =>
x.Id));
+
+ StringAssert.Contains(
+ "select _T0.KEY from PUBLIC.TBL_INT64 as _T0 where (_T0.KEY > ?) "
+
+ "union (select _T1.KEY from PUBLIC.TBL1 as _T1 where (_T1.KEY <
?)",
+ query.ToString());
+ }
+
+ [Test]
+ public void TestUnionWithOrderBy()
+ {
+ var subQuery = PocoView.AsQueryable()
+ .Where(x => x.Key < 2)
+ .Select(x => new { Id = x.Key });
+
+ var query = PocoLongView.AsQueryable()
+ .Where(x => x.Key > 8)
+ .Select(x => new { Id = x.Key })
+ .Union(subQuery)
+ .OrderBy(x => x.Id);
+
+ var res = query.ToList();
+
+ Assert.AreEqual(new[] { 0, 1, 9 }, res.Select(x => x.Id));
+
+ StringAssert.Contains(
+ "select * from " +
+ "(select _T0.KEY from PUBLIC.TBL_INT64 as _T0 where (_T0.KEY > ?)
" +
+ "union (select _T1.KEY from PUBLIC.TBL1 as _T1 where (_T1.KEY <
?))) as _T2 " +
+ "order by (_T2.KEY) asc",
+ query.ToString());
+ }
+
+ [Test]
+ public void TestUnionWithCast()
+ {
+ var subQuery = PocoIntView.AsQueryable()
+ .Select(x => new { x.Key })
+ .Where(x => x.Key > 3 && x.Key < 5);
+
+ var query = PocoByteView.AsQueryable()
+ .Where(x => x.Key > 8)
+ .Select(x => new { Key = (int)x.Key })
+ .Union(subQuery);
+
+ var res = query.ToList();
+
+ CollectionAssert.AreEquivalent(new[] { 4, 9 }, res.Select(x => x.Key));
+
+ StringAssert.Contains(
+ "select cast(_T0.KEY as int) " +
+ "from PUBLIC.TBL_INT8 as _T0 " +
+ "where (cast(_T0.KEY as int) > ?) " +
+ "union (select _T1.KEY from PUBLIC.TBL_INT32 as _T1 where
((_T1.KEY > ?) and (_T1.KEY < ?)))",
+ query.ToString());
+ }
+
+ [Test]
+ public void TestIntersect()
+ {
+ var subQuery = PocoView.AsQueryable()
+ .Where(x => x.Key < 5)
+ .Select(x => new { Id = x.Key });
+
+ var query = PocoLongView.AsQueryable()
+ .Where(x => x.Key > 2)
+ .Select(x => new { Id = x.Key })
+ .Intersect(subQuery);
+
+ var res = query.ToList();
+
+ CollectionAssert.AreEquivalent(new[] { 3, 4 }, res.Select(x => x.Id));
+
+ StringAssert.Contains(
+ "select _T0.KEY from PUBLIC.TBL_INT64 as _T0 where (_T0.KEY > ?) "
+
+ "intersect (select _T1.KEY from PUBLIC.TBL1 as _T1 where (_T1.KEY
< ?)",
+ query.ToString());
+ }
+
+ [Test]
+ public void TestIntersectEmpty()
+ {
+ var subQuery = PocoView.AsQueryable()
+ .Where(x => x.Key < 2)
+ .Select(x => new { Id = x.Key });
+
+ var query = PocoLongView.AsQueryable()
+ .Where(x => x.Key > 8)
+ .Select(x => new { Id = x.Key })
+ .Intersect(subQuery);
+
+ var res = query.ToList();
+
+ CollectionAssert.IsEmpty(res);
+ }
+
+ [Test]
+ public void TestExcept()
+ {
+ var subQuery = PocoView.AsQueryable()
+ .Where(x => x.Key < 7)
+ .Select(x => new { Id = x.Key });
+
+ var query = PocoLongView.AsQueryable()
+ .Where(x => x.Key > 5)
+ .Select(x => new { Id = x.Key })
+ .Except(subQuery);
+
+ var res = query.ToList();
+
+ CollectionAssert.AreEquivalent(new[] { 7, 8, 9 }, res.Select(x =>
x.Id));
+
+ StringAssert.Contains(
+ "select _T0.KEY from PUBLIC.TBL_INT64 as _T0 where (_T0.KEY > ?) "
+
+ "except (select _T1.KEY from PUBLIC.TBL1 as _T1 where (_T1.KEY <
?))",
+ query.ToString());
+ }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/IgniteQueryExecutor.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/IgniteQueryExecutor.cs
index f9815f3ac6..fd049619ff 100644
---
a/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/IgniteQueryExecutor.cs
+++
b/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/IgniteQueryExecutor.cs
@@ -28,6 +28,8 @@ using Ignite.Sql;
using Ignite.Transactions;
using Proto.BinaryTuple;
using Remotion.Linq;
+using Remotion.Linq.Clauses;
+using Remotion.Linq.Clauses.Expressions;
using Sql;
using Table.Serialization;
@@ -180,6 +182,15 @@ internal sealed class IgniteQueryExecutor : IQueryExecutor
};
}
+ if (selectorExpression is QuerySourceReferenceExpression
+ {
+ ReferencedQuerySource: IFromClause { FromExpression:
SubQueryExpression subQuery }
+ })
+ {
+ // Select everything from a sub-query - use nested selector.
+ return GetResultSelector<T>(columns,
subQuery.QueryModel.SelectClause.Selector);
+ }
+
return (IReadOnlyList<IColumnMetadata> cols, ref BinaryTupleReader
reader) =>
{
var res = (T)FormatterServices.GetUninitializedObject(typeof(T));
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/IgniteQueryModelVisitor.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/IgniteQueryModelVisitor.cs
index 8a5e3e3e32..c8422091f0 100644
---
a/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/IgniteQueryModelVisitor.cs
+++
b/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/IgniteQueryModelVisitor.cs
@@ -78,7 +78,7 @@ internal sealed class IgniteQueryModelVisitor :
QueryModelVisitorBase
/** <inheritdoc /> */
public override void VisitQueryModel(QueryModel queryModel)
{
- VisitQueryModel(queryModel, false);
+ VisitQueryModel(queryModel, includeAllFields:
queryModel.MainFromClause.FromExpression is SubQueryExpression);
}
/** <inheritdoc /> */
@@ -180,7 +180,21 @@ internal sealed class IgniteQueryModelVisitor :
QueryModelVisitorBase
/** <inheritdoc /> */
public override void VisitMainFromClause(MainFromClause fromClause,
QueryModel queryModel)
{
- base.VisitMainFromClause(fromClause, queryModel);
+ // Special case for UNION subquery.
+ if (fromClause.FromExpression is SubQueryExpression subQuery &&
+ subQuery.QueryModel.ResultOperators.Any(x => x is
UnionResultOperator))
+ {
+ _builder.Append("from (");
+
+ VisitQueryModel(subQuery.QueryModel);
+
+ _builder.TrimEnd()
+ .Append(") as ")
+
.Append(_aliases.GetTableAlias(subQuery.QueryModel.MainFromClause))
+ .Append(' ');
+
+ return;
+ }
// TODO: IGNITE-18137 DML: queryModel.ResultOperators.LastOrDefault()
is UpdateAllResultOperator;
var isUpdateQuery = false;
@@ -274,12 +288,9 @@ internal sealed class IgniteQueryModelVisitor :
QueryModelVisitorBase
{
var parenCount = ProcessResultOperatorsBegin(queryModel);
- if (parenCount >= 0)
- {
- // FIELD1, FIELD2
- BuildSqlExpression(queryModel.SelectClause.Selector, parenCount >
0, includeAllFields);
- _builder.TrimEnd().Append(')', parenCount).Append(' ');
- }
+ // FIELD1, FIELD2
+ BuildSqlExpression(queryModel.SelectClause.Selector, parenCount > 0,
includeAllFields);
+ _builder.TrimEnd().Append(')', parenCount).Append(' ');
}
/** <inheritdoc /> */
@@ -393,67 +404,61 @@ internal sealed class IgniteQueryModelVisitor :
QueryModelVisitorBase
{
ProcessSkipTake(queryModel);
- foreach (var op in queryModel.ResultOperators.Reverse())
+ for (var i = queryModel.ResultOperators.Count - 1; i >= 0; i--)
{
+ var op = queryModel.ResultOperators[i];
string? keyword = null;
Expression? source = null;
- var union = op as UnionResultOperator;
- if (union != null)
+ if (op is UnionResultOperator union)
{
keyword = "union";
source = union.Source2;
}
- var intersect = op as IntersectResultOperator;
- if (intersect != null)
+ if (op is IntersectResultOperator intersect)
{
keyword = "intersect";
source = intersect.Source2;
}
- var except = op as ExceptResultOperator;
- if (except != null)
+ if (op is ExceptResultOperator except)
{
keyword = "except";
source = except.Source2;
}
- if (keyword != null)
+ if (keyword == null)
{
- _builder.Append(keyword).Append(" (");
+ continue;
+ }
- var subQuery = source as SubQueryExpression;
+ _builder.Append(keyword).Append(" (");
- if (subQuery != null)
+ if (source is SubQueryExpression subQuery)
+ {
+ // Subquery union.
+ VisitQueryModel(subQuery.QueryModel);
+ }
+ else
+ {
+ // Direct union, source is IIgniteQueryableInternal
+ if (source is not ConstantExpression innerExpr)
{
- // Subquery union.
- VisitQueryModel(subQuery.QueryModel);
+ throw new NotSupportedException("Unexpected UNION inner
sequence: " + source);
}
- else
- {
- // Direct union, source is IIgniteQueryableInternal
- var innerExpr = source as ConstantExpression;
-
- if (innerExpr == null)
- {
- throw new NotSupportedException("Unexpected UNION
inner sequence: " + source);
- }
- var queryable = innerExpr.Value as
IIgniteQueryableInternal;
-
- if (queryable == null)
- {
- throw new NotSupportedException("Unexpected UNION
inner sequence " +
- "(only results of
cache.ToQueryable() are supported): " +
- innerExpr.Value);
- }
-
- VisitQueryModel(queryable.GetQueryModel());
+ if (innerExpr.Value is not IIgniteQueryableInternal queryable)
+ {
+ throw new NotSupportedException("Unexpected UNION inner
sequence " +
+ "(only results of
cache.ToQueryable() are supported): " +
+ innerExpr.Value);
}
- _builder.TrimEnd().Append(')');
+ VisitQueryModel(queryable.GetQueryModel());
}
+
+ _builder.TrimEnd().Append(')');
}
}
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
index 8815baf650..02e0658d54 100644
---
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
+++
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
@@ -172,7 +172,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
if (fieldInfo.GetCustomAttribute<ColumnAttribute>() is { Name: { }
columnAttributeName })
{
- return new(columnAttributeName, fieldInfo,
HasColumnNameAttribute: true);
+ return new(columnAttributeName, fieldInfo, null,
HasColumnNameAttribute: true);
}
var cleanName = fieldInfo.GetCleanName();
@@ -188,11 +188,13 @@ namespace Apache.Ignite.Internal.Table.Serialization
if (property.GetCustomAttribute<ColumnAttribute>() is { Name:
{ } columnAttributeName2 })
{
// This is a compiler-generated backing field for an
automatic property - get the attribute from the property.
- return new(columnAttributeName2, fieldInfo,
HasColumnNameAttribute: true);
+ return new(columnAttributeName2, fieldInfo, property,
HasColumnNameAttribute: true);
}
+
+ return new(cleanName, fieldInfo, property,
HasColumnNameAttribute: false);
}
- return new(cleanName, fieldInfo, HasColumnNameAttribute: false);
+ return new(cleanName, fieldInfo, null, HasColumnNameAttribute:
false);
}
/// <summary>
@@ -232,8 +234,9 @@ namespace Apache.Ignite.Internal.Table.Serialization
/// </summary>
/// <param name="Name">Column name.</param>
/// <param name="Field">Corresponding field.</param>
+ /// <param name="Property">Corresponding property (when <see
cref="Field"/> is a backing field of a auto property).</param>
/// <param name="HasColumnNameAttribute">Whether corresponding field
or property has <see cref="ColumnAttribute"/>
/// with <see cref="ColumnAttribute.Name"/> set.</param>
- internal record ColumnInfo(string Name, FieldInfo Field, bool
HasColumnNameAttribute);
+ internal record ColumnInfo(string Name, FieldInfo Field, PropertyInfo?
Property, bool HasColumnNameAttribute);
}
}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/IResultSet.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/IResultSet.cs
index 8a61bed18c..89a14b8949 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Sql/IResultSet.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/IResultSet.cs
@@ -19,6 +19,7 @@ namespace Apache.Ignite.Sql
{
using System;
using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
/// <summary>
@@ -42,6 +43,7 @@ namespace Apache.Ignite.Sql
/// <summary>
/// Gets a value indicating whether this result set contains a
collection of rows.
/// </summary>
+ [MemberNotNullWhen(true, nameof(Metadata))]
bool HasRowSet { get; }
/// <summary>