IGNITE-3591 .NET: Fix self-joins in LINQ
Project: http://git-wip-us.apache.org/repos/asf/ignite/repo Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/f96b568e Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/f96b568e Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/f96b568e Branch: refs/heads/ignite-3443 Commit: f96b568e662758a915309fe631c5bec4fd0ccdfe Parents: 2f36ae5 Author: Pavel Tupitsyn <[email protected]> Authored: Wed Jul 27 15:25:59 2016 +0300 Committer: Pavel Tupitsyn <[email protected]> Committed: Wed Jul 27 15:25:59 2016 +0300 ---------------------------------------------------------------------- .../Cache/Query/CacheLinqTest.cs | 18 +++++ .../Apache.Ignite.Linq/Impl/AliasDictionary.cs | 74 ++++++++++++++++---- .../Impl/CacheQueryExpressionVisitor.cs | 4 +- .../Impl/CacheQueryModelVisitor.cs | 6 +- .../Apache.Ignite.Linq/Impl/ExpressionWalker.cs | 34 ++++----- 5 files changed, 96 insertions(+), 40 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ignite/blob/f96b568e/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTest.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTest.cs index 679f03a..70c5fb5 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTest.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTest.cs @@ -537,6 +537,24 @@ namespace Apache.Ignite.Core.Tests.Cache.Query } /// <summary> + /// Tests the join of a table to itself. + /// </summary> + [Test] + public void TestSelfJoin() + { + // Different queryables + var p1 = GetPersonCache().AsCacheQueryable(); + var p2 = GetPersonCache().AsCacheQueryable(); + + var qry = p1.Join(p2, x => x.Value.Age, x => x.Key, (x, y) => x.Key); + Assert.AreEqual(PersonCount, qry.ToArray().Distinct().Count()); + + // Same queryables + var qry2 = p1.Join(p1, x => x.Value.Age, x => x.Key, (x, y) => x.Key); + Assert.AreEqual(PersonCount, qry2.ToArray().Distinct().Count()); + } + + /// <summary> /// Tests the group by. /// </summary> [Test] http://git-wip-us.apache.org/repos/asf/ignite/blob/f96b568e/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs index 10a414c..5fd890d 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs @@ -17,10 +17,13 @@ namespace Apache.Ignite.Linq.Impl { + using System; using System.Collections.Generic; using System.Diagnostics; + using System.Linq.Expressions; using System.Text; using Remotion.Linq.Clauses; + using Remotion.Linq.Clauses.Expressions; /// <summary> /// Alias dictionary. @@ -31,10 +34,10 @@ namespace Apache.Ignite.Linq.Impl private int _aliasIndex; /** */ - private Dictionary<string, string> _aliases = new Dictionary<string, string>(); + private Dictionary<IQuerySource, string> _aliases = new Dictionary<IQuerySource, string>(); /** */ - private readonly Stack<Dictionary<string, string>> _stack = new Stack<Dictionary<string, string>>(); + private readonly Stack<Dictionary<IQuerySource, string>> _stack = new Stack<Dictionary<IQuerySource, string>>(); /// <summary> /// Pushes current aliases to stack. @@ -43,7 +46,7 @@ namespace Apache.Ignite.Linq.Impl { _stack.Push(_aliases); - _aliases = new Dictionary<string, string>(); + _aliases = new Dictionary<IQuerySource, string>(); } /// <summary> @@ -57,27 +60,34 @@ namespace Apache.Ignite.Linq.Impl /// <summary> /// Gets the table alias. /// </summary> - public string GetTableAlias(ICacheQueryableInternal queryable) + public string GetTableAlias(Expression expression) { - Debug.Assert(queryable != null); + Debug.Assert(expression != null); - return GetTableAlias(ExpressionWalker.GetTableNameWithSchema(queryable)); + return GetTableAlias(GetQuerySource(expression)); } - /// <summary> - /// Gets the table alias. - /// </summary> - public string GetTableAlias(string fullName) + public string GetTableAlias(IFromClause fromClause) { - Debug.Assert(!string.IsNullOrEmpty(fullName)); + return GetTableAlias(GetQuerySource(fromClause.FromExpression) ?? fromClause); + } + + public string GetTableAlias(JoinClause joinClause) + { + return GetTableAlias(GetQuerySource(joinClause.InnerSequence) ?? joinClause); + } + + private string GetTableAlias(IQuerySource querySource) + { + Debug.Assert(querySource != null); string alias; - if (!_aliases.TryGetValue(fullName, out alias)) + if (!_aliases.TryGetValue(querySource, out alias)) { alias = "_T" + _aliasIndex++; - _aliases[fullName] = alias; + _aliases[querySource] = alias; } return alias; @@ -94,9 +104,45 @@ namespace Apache.Ignite.Linq.Impl var queryable = ExpressionWalker.GetCacheQueryable(clause); var tableName = ExpressionWalker.GetTableNameWithSchema(queryable); - builder.AppendFormat("{0} as {1}", tableName, GetTableAlias(tableName)); + builder.AppendFormat("{0} as {1}", tableName, GetTableAlias(clause)); return builder; } + + /// <summary> + /// Gets the query source. + /// </summary> + private static IQuerySource GetQuerySource(Expression expression) + { + var subQueryExp = expression as SubQueryExpression; + + if (subQueryExp != null) + return GetQuerySource(subQueryExp.QueryModel.MainFromClause.FromExpression) + ?? subQueryExp.QueryModel.MainFromClause; + + var srcRefExp = expression as QuerySourceReferenceExpression; + + if (srcRefExp != null) + { + var fromSource = srcRefExp.ReferencedQuerySource as IFromClause; + + if (fromSource != null) + return GetQuerySource(fromSource.FromExpression) ?? fromSource; + + var joinSource = srcRefExp.ReferencedQuerySource as JoinClause; + + if (joinSource != null) + return GetQuerySource(joinSource.InnerSequence) ?? joinSource; + + throw new NotSupportedException("Unexpected query source: " + srcRefExp.ReferencedQuerySource); + } + + var memberExpr = expression as MemberExpression; + + if (memberExpr != null) + return GetQuerySource(memberExpr.Expression); + + return null; + } } } http://git-wip-us.apache.org/repos/asf/ignite/blob/f96b568e/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs index eaca07a..2362137 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs @@ -248,7 +248,7 @@ namespace Apache.Ignite.Linq.Impl // In other cases we need both parts of cache entry var format = _useStar ? "{0}.*" : "{0}._key, {0}._val"; - var tableName = Aliases.GetTableAlias(ExpressionWalker.GetCacheQueryable(expression)); + var tableName = Aliases.GetTableAlias(expression); ResultBuilder.AppendFormat(format, tableName); @@ -283,7 +283,7 @@ namespace Apache.Ignite.Linq.Impl { var fieldName = GetFieldName(expression, queryable); - ResultBuilder.AppendFormat("{0}.{1}", Aliases.GetTableAlias(queryable), fieldName); + ResultBuilder.AppendFormat("{0}.{1}", Aliases.GetTableAlias(expression), fieldName); } else AppendParameter(RegisterEvaluatedParameter(expression)); http://git-wip-us.apache.org/repos/asf/ignite/blob/f96b568e/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs index 1888414..a8c4c67 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs @@ -418,8 +418,7 @@ namespace Apache.Ignite.Linq.Impl VisitQueryModel(subQuery.QueryModel, true); - var queryable = ExpressionWalker.GetCacheQueryable(subQuery.QueryModel.MainFromClause); - var alias = _aliases.GetTableAlias(queryable); + var alias = _aliases.GetTableAlias(subQuery.QueryModel.MainFromClause); _builder.AppendFormat(") as {0} on (", alias); } else @@ -437,7 +436,8 @@ namespace Apache.Ignite.Linq.Impl var queryable = ExpressionWalker.GetCacheQueryable(joinClause); var tableName = ExpressionWalker.GetTableNameWithSchema(queryable); - _builder.AppendFormat("inner join {0} as {1} on (", tableName, _aliases.GetTableAlias(tableName)); + var alias = _aliases.GetTableAlias(joinClause); + _builder.AppendFormat("inner join {0} as {1} on (", tableName, alias); } BuildJoinCondition(joinClause.InnerKeySelector, joinClause.OuterKeySelector); http://git-wip-us.apache.org/repos/asf/ignite/blob/f96b568e/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs index 96371cc..bd57e3f 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs @@ -38,26 +38,6 @@ namespace Apache.Ignite.Linq.Impl /// <summary> /// Gets the cache queryable. /// </summary> - public static ICacheQueryableInternal GetCacheQueryable(QuerySourceReferenceExpression expression) - { - Debug.Assert(expression != null); - - var fromSource = expression.ReferencedQuerySource as IFromClause; - - if (fromSource != null) - return GetCacheQueryable(fromSource); - - var joinSource = expression.ReferencedQuerySource as JoinClause; - - if (joinSource != null) - return GetCacheQueryable(joinSource); - - throw new NotSupportedException("Unexpected query source: " + expression.ReferencedQuerySource); - } - - /// <summary> - /// Gets the cache queryable. - /// </summary> public static ICacheQueryableInternal GetCacheQueryable(IFromClause fromClause) { return GetCacheQueryable(fromClause.FromExpression); @@ -84,7 +64,19 @@ namespace Apache.Ignite.Linq.Impl var srcRefExp = expression as QuerySourceReferenceExpression; if (srcRefExp != null) - return GetCacheQueryable(srcRefExp); + { + var fromSource = srcRefExp.ReferencedQuerySource as IFromClause; + + if (fromSource != null) + return GetCacheQueryable(fromSource); + + var joinSource = srcRefExp.ReferencedQuerySource as JoinClause; + + if (joinSource != null) + return GetCacheQueryable(joinSource); + + throw new NotSupportedException("Unexpected query source: " + srcRefExp.ReferencedQuerySource); + } var memberExpr = expression as MemberExpression;
