From: <pa...@us...> - 2011-04-15 04:10:59
|
Revision: 5705 http://nhibernate.svn.sourceforge.net/nhibernate/?rev=5705&view=rev Author: patearl Date: 2011-04-15 04:10:51 +0000 (Fri, 15 Apr 2011) Log Message: ----------- Harald Mueller's great contribution that fixes joins for member expression chains, especially where || operators are involved. This is basically the original patch. (NH-2583) Modified Paths: -------------- trunk/nhibernate/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs trunk/nhibernate/src/NHibernate/NHibernate.csproj trunk/nhibernate/src/NHibernate.Test/NHibernate.Test.csproj Added Paths: ----------- trunk/nhibernate/src/NHibernate/Linq/Clauses/NhJoinClause.cs trunk/nhibernate/src/NHibernate/Linq/ReWriters/AddJoinsReWriter.cs trunk/nhibernate/src/NHibernate/Linq/Visitors/AbstractJoinDetector.cs trunk/nhibernate/src/NHibernate/Linq/Visitors/SelectAndOrderByJoinDetector.cs trunk/nhibernate/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/ trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/AbstractMassTestingFixture.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/Domain.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/ManualTestFixture.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/Mappings.hbm.xml trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/MassTestingNotAndDeMorganFixture.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/MassTestingOneOrTreeFixture.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/MassTestingOrderByFixture.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/MassTestingSelectFixture.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/MassTestingThreeOrTreesSideBySideFixture.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/MassTestingTwoOrTreesSideBySideFixture.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/SelfJoinTestFixture.cs Added: trunk/nhibernate/src/NHibernate/Linq/Clauses/NhJoinClause.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Linq/Clauses/NhJoinClause.cs (rev 0) +++ trunk/nhibernate/src/NHibernate/Linq/Clauses/NhJoinClause.cs 2011-04-15 04:10:51 UTC (rev 5705) @@ -0,0 +1,23 @@ +using System.Linq.Expressions; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses; + +namespace NHibernate.Linq.Visitors +{ + /// <summary> + /// All joins are created as outer joins. An optimization in <see cref="WhereJoinDetector"/> finds + /// joins that may be inner joined and calls <see cref="MakeInner"/> on them. + /// <see cref="QueryModelVisitor"/>'s <see cref="QueryModelVisitor.VisitAdditionalFromClause"/> will + /// then emit the correct HQL join. + /// </summary> + public class NhJoinClause : AdditionalFromClause + { + public bool IsInner { get; private set; } + public void MakeInner() { + IsInner = true; + } + public NhJoinClause(string itemName, System.Type itemType, Expression fromExpression) : base(itemName, itemType, fromExpression) { + IsInner = false; + } + } +} Added: trunk/nhibernate/src/NHibernate/Linq/ReWriters/AddJoinsReWriter.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Linq/ReWriters/AddJoinsReWriter.cs (rev 0) +++ trunk/nhibernate/src/NHibernate/Linq/ReWriters/AddJoinsReWriter.cs 2011-04-15 04:10:51 UTC (rev 5705) @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using NHibernate.Linq.Visitors; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.Expressions; + +namespace NHibernate.Linq.ReWriters +{ + internal interface IIsEntityDecider + { + bool IsEntity(System.Type type); + } + + public class AddJoinsReWriter : QueryModelVisitorBase, IIsEntityDecider + { + private readonly Dictionary<string, NhJoinClause> _joins = new Dictionary<string, NhJoinClause>(); + private readonly Dictionary<MemberExpression, QuerySourceReferenceExpression> _expressionMap = new Dictionary<MemberExpression, QuerySourceReferenceExpression>(); + private readonly NameGenerator _nameGenerator; + private readonly ISessionFactory _sessionFactory; + + private AddJoinsReWriter(NameGenerator nameGenerator, ISessionFactory sessionFactory) + { + _nameGenerator = nameGenerator; + _sessionFactory = sessionFactory; + } + + public static void ReWrite(QueryModel queryModel, ISessionFactory sessionFactory) + { + new AddJoinsReWriter(new NameGenerator(queryModel), sessionFactory).ReWrite(queryModel); + } + + private void ReWrite(QueryModel queryModel) + { + VisitQueryModel(queryModel); + + if (_joins.Count > 0) + { + MemberExpressionSwapper swap = new MemberExpressionSwapper(_expressionMap); + queryModel.TransformExpressions(swap.VisitExpression); + + foreach (var join in _joins.Values) + { + queryModel.BodyClauses.Add(join); + } + } + } + + public override void VisitSelectClause(SelectClause selectClause, QueryModel queryModel) + { + new SelectAndOrderByJoinDetector(_nameGenerator, this, _joins, _expressionMap).VisitExpression(selectClause.Selector); + } + + public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index) + { + WhereJoinDetector.Find(whereClause.Predicate,_nameGenerator, + this, + _joins, + _expressionMap); + } + + public override void VisitOrderByClause(OrderByClause orderByClause, QueryModel queryModel, int index) + { + var joinDetector = new SelectAndOrderByJoinDetector(_nameGenerator, this, _joins, _expressionMap); + foreach (Ordering ordering in orderByClause.Orderings) + { + joinDetector.VisitExpression(ordering.Expression); + } + } + + public bool IsEntity(System.Type type) + { + return _sessionFactory.GetClassMetadata(type) != null; + } + } + + public class MemberExpressionSwapper : NhExpressionTreeVisitor + { + private readonly Dictionary<MemberExpression, QuerySourceReferenceExpression> _expressionMap; + + public MemberExpressionSwapper(Dictionary<MemberExpression, QuerySourceReferenceExpression> expressionMap) + { + _expressionMap = expressionMap; + } + + protected override Expression VisitMemberExpression(MemberExpression expression) + { + if (expression == null) + { + return null; + } + + QuerySourceReferenceExpression replacement; + + if (_expressionMap.TryGetValue(expression, out replacement)) + { + return replacement; + } + else + { + return base.VisitMemberExpression(expression); + } + } + } +} Added: trunk/nhibernate/src/NHibernate/Linq/Visitors/AbstractJoinDetector.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Linq/Visitors/AbstractJoinDetector.cs (rev 0) +++ trunk/nhibernate/src/NHibernate/Linq/Visitors/AbstractJoinDetector.cs 2011-04-15 04:10:51 UTC (rev 5705) @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using NHibernate.Linq.ReWriters; +using Remotion.Linq.Clauses.Expressions; + +namespace NHibernate.Linq.Visitors +{ + public abstract class AbstractJoinDetector : NhExpressionTreeVisitor + { + private readonly NameGenerator _nameGenerator; + internal readonly IIsEntityDecider _isEntityDecider; + protected readonly Dictionary<string, NhJoinClause> _joins; + protected readonly Dictionary<MemberExpression, QuerySourceReferenceExpression> _expressionMap; + + internal AbstractJoinDetector(NameGenerator nameGenerator, IIsEntityDecider isEntityDecider, Dictionary<string, NhJoinClause> joins, Dictionary<MemberExpression, QuerySourceReferenceExpression> expressionMap) + { + _nameGenerator = nameGenerator; + _expressionMap = expressionMap; + _joins = joins; + _isEntityDecider = isEntityDecider; + } + + protected internal Expression AddJoin(MemberExpression expression) + { + string key = ExpressionKeyVisitor.Visit(expression, null); + NhJoinClause join; + + if (!_joins.TryGetValue(key, out join)) + { + join = new NhJoinClause(_nameGenerator.GetNewName(), expression.Type, expression); + _joins.Add(key, join); + } + + QuerySourceReferenceExpression newExpr = new QuerySourceReferenceExpression(join); + + if (!_expressionMap.ContainsKey(expression)) + _expressionMap.Add(expression, newExpr); + + return newExpr; + } + + protected void MakeInnerIfJoined(string memberExpressionKey) + { + // memberExpressionKey is not joined if it occurs only at tails of expressions, e.g. + // a.B == null, a.B != null, a.B == c.D etc. + if (_joins.ContainsKey(memberExpressionKey)) + { + _joins[memberExpressionKey].MakeInner(); + } + } + } +} Modified: trunk/nhibernate/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs 2011-04-14 21:27:16 UTC (rev 5704) +++ trunk/nhibernate/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs 2011-04-15 04:10:51 UTC (rev 5705) @@ -41,8 +41,8 @@ // Flatten pointless subqueries QueryReferenceExpressionFlattener.ReWrite(queryModel); - // Add left joins for references - AddLeftJoinsReWriter.ReWrite(queryModel, parameters.SessionFactory); + // Add joins for references + AddJoinsReWriter.ReWrite(queryModel, parameters.SessionFactory); // Move OrderBy clauses to end MoveOrderByToEndRewriter.ReWrite(queryModel); @@ -202,12 +202,20 @@ public override void VisitAdditionalFromClause(AdditionalFromClause fromClause, QueryModel queryModel, int index) { - if (fromClause is LeftJoinClause) + if (fromClause is NhJoinClause) { - // It's a left join - _hqlTree.AddFromClause(_hqlTree.TreeBuilder.LeftJoin( - HqlGeneratorExpressionTreeVisitor.Visit(fromClause.FromExpression, VisitorParameters).AsExpression(), - _hqlTree.TreeBuilder.Alias(fromClause.ItemName))); + if (((NhJoinClause)fromClause).IsInner) + { + _hqlTree.AddFromClause(_hqlTree.TreeBuilder.Join( + HqlGeneratorExpressionTreeVisitor.Visit(fromClause.FromExpression, VisitorParameters).AsExpression(), + _hqlTree.TreeBuilder.Alias(fromClause.ItemName))); + } + else + { + _hqlTree.AddFromClause(_hqlTree.TreeBuilder.LeftJoin( + HqlGeneratorExpressionTreeVisitor.Visit(fromClause.FromExpression, VisitorParameters).AsExpression(), + _hqlTree.TreeBuilder.Alias(fromClause.ItemName))); + } } else if (fromClause.FromExpression is MemberExpression) { Added: trunk/nhibernate/src/NHibernate/Linq/Visitors/SelectAndOrderByJoinDetector.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Linq/Visitors/SelectAndOrderByJoinDetector.cs (rev 0) +++ trunk/nhibernate/src/NHibernate/Linq/Visitors/SelectAndOrderByJoinDetector.cs 2011-04-15 04:10:51 UTC (rev 5705) @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using NHibernate.Linq.ReWriters; +using Remotion.Linq.Clauses.Expressions; + +namespace NHibernate.Linq.Visitors +{ + public class SelectAndOrderByJoinDetector : AbstractJoinDetector + { + internal SelectAndOrderByJoinDetector(NameGenerator nameGenerator, IIsEntityDecider isEntityDecider, Dictionary<string, NhJoinClause> joins, Dictionary<MemberExpression, QuerySourceReferenceExpression> expressionMap) + : base(nameGenerator, isEntityDecider, joins, expressionMap) + { + } + + protected override Expression VisitMemberExpression(MemberExpression expression) + { + if (expression.Type.IsNonPrimitive() && _isEntityDecider.IsEntity(expression.Type)) { + return AddJoin(expression); + } + + return base.VisitMemberExpression(expression); + } + } +} Added: trunk/nhibernate/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs (rev 0) +++ trunk/nhibernate/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs 2011-04-15 04:10:51 UTC (rev 5705) @@ -0,0 +1,340 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using NHibernate.Linq.ReWriters; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Utilities; + +namespace NHibernate.Linq.Visitors +{ + /// <summary> + /// The WhereJoinDetector creates the joins for the where clause, including + /// optimizations for inner joins. The algorithms are explained in a text + /// attached to JIRA entry NH-2583. + /// </summary> + internal class WhereJoinDetector : AbstractJoinDetector + { + // Possible results of a condition when emptily outer joined. + private const int T = 1; + private const int N = 2; + private const int TN = T | N; + private const int F = 4; + private const int TF = T | F; + private const int NF = N | F; + private const int TNF = T | N | F; + + // Composition rules for possible results when emptily outer joined + // for &&, ||, !. + private static readonly int[,] AND = new int[8, 8]; + private static readonly int[,] OR = new int[8, 8]; + private static readonly int[] NOT = new int[8]; + + /// <summary> + /// Setup of <see cref="AND"/>, <see cref="OR"/>, <see cref="NOT"/>. + /// </summary> + static WhereJoinDetector() + { + // Setup of simple values according to SQL 3-valued logic. + NOT[T] = F; + NOT[N] = N; + NOT[F] = T; + + foreach (var p in new[] { T, N, F }) + { + OR[p, p] = AND[p, p] = p; + AND[p, F] = AND[F, p] = F; + OR[p, T] = OR[T, p] = T; + } + AND[T, N] = AND[N, T] = N; + OR[F, N] = OR[N, F] = N; + + // Setup of compound values. Idea: Split each + // compound value to simple values, compute results + // for simple values and or them together. + var allValues = new[] { T, N, TN, F, TF, NF, TNF }; + + // How compound values are split up into simple values. + var split = new int[8][]; + split[T] = new[] { T }; + split[N] = new[] { N }; + split[TN] = new[] { T, N }; + split[F] = new[] { F }; + split[TF] = new[] { T, F }; + split[NF] = new[] { N, F }; + split[TNF] = new[] { T, N, F }; + + foreach (var p in allValues) + { + int[] splitP = split[p]; + // We only need to compute NOT for compound values. + if (splitP.Length > 1) + { + int notResult = 0; + foreach (var p0 in splitP) + { + notResult |= NOT[p0]; + } + NOT[p] = notResult; + } + foreach (var q in allValues) + { + int[] splitQ = split[q]; + // We must compute AND and OR if both values are compound, + // *but also* if one is compound and the other is simple + // (e.g. T and TNF). + if (splitP.Length > 1 || splitQ.Length > 1) + { + int andResult = 0; + int orResult = 0; + foreach (var p0 in splitP) + { + foreach (var q0 in splitQ) + { + andResult |= AND[p0, q0]; + orResult |= OR[p0, q0]; + } + } + AND[p, q] = andResult; + OR[p, q] = orResult; + } + } + } + } + + // The following is used for all *condition* traversal (but not *expressions* that are not conditions). + // This is the "mapping" described in the text at NH-2583. + private Dictionary<string, int> _memberExpressionMapping = new Dictionary<string, int>(); + + // The following two are used for member expressions traversal. + private HashSet<string> _collectedPathMemberExpressionsInExpression = new HashSet<string>(); + private int _memberExpressionDepth = 0; + + internal + WhereJoinDetector(NameGenerator nameGenerator, IIsEntityDecider isEntityDecider, Dictionary<string, NhJoinClause> joins, Dictionary<MemberExpression, QuerySourceReferenceExpression> expressionMap) + : base(nameGenerator, isEntityDecider, joins, expressionMap) + { + } + + protected override Expression VisitBinaryExpression(BinaryExpression expression) + { + ArgumentUtility.CheckNotNull("expression", expression); + + Expression baseResult = expression; + if (expression.NodeType == ExpressionType.AndAlso && expression.Type == typeof(bool)) + { + // Case (a) from the text at NH-2583. + var newLeft = VisitExpression(expression.Left); + + var leftMapping = _memberExpressionMapping; + + var newRight = VisitExpression(expression.Right); + + var rightMapping = _memberExpressionMapping; + + _memberExpressionMapping = BinaryMapping(leftMapping, rightMapping, AND); + + // The following is copy-pasted from Relinq's visitor, as I had to split the code above. + var newConversion = (LambdaExpression)VisitExpression(expression.Conversion); + if (newLeft != expression.Left || newRight != expression.Right || newConversion != expression.Conversion) + baseResult = Expression.MakeBinary(expression.NodeType, newLeft, newRight, expression.IsLiftedToNull, expression.Method, newConversion); + } + else if (expression.NodeType == ExpressionType.OrElse && expression.Type == typeof(bool)) + { + // Case (b) + var newLeft = VisitExpression(expression.Left); + + var leftMapping = _memberExpressionMapping; + + var newRight = VisitExpression(expression.Right); + + var rightMapping = _memberExpressionMapping; + + _memberExpressionMapping = BinaryMapping(leftMapping, rightMapping, OR); + + // Again, the following is copy-pasted from Relinq's visitor, as I had to split the code above. + var newConversion = (LambdaExpression)VisitExpression(expression.Conversion); + if (newLeft != expression.Left || newRight != expression.Right || newConversion != expression.Conversion) + baseResult = Expression.MakeBinary(expression.NodeType, newLeft, newRight, expression.IsLiftedToNull, expression.Method, newConversion); + } + else if (expression.Type == typeof(bool) + && (expression.NodeType == ExpressionType.Equal && !IsNullConstantExpression(expression.Right) && !IsNullConstantExpression(expression.Left) + || expression.NodeType == ExpressionType.NotEqual && !IsNullConstantExpression(expression.Right) && !IsNullConstantExpression(expression.Left) + || expression.NodeType == ExpressionType.LessThan + || expression.NodeType == ExpressionType.LessThanOrEqual + || expression.NodeType == ExpressionType.GreaterThan + || expression.NodeType == ExpressionType.GreaterThanOrEqual)) + { + // Cases (e), (f).2, (g).2 + _collectedPathMemberExpressionsInExpression = new HashSet<string>(); + + baseResult = base.VisitBinaryExpression(expression); + + _memberExpressionMapping = FixedMapping(_collectedPathMemberExpressionsInExpression, N); + } + else if (expression.Type == typeof(bool) + && expression.NodeType == ExpressionType.NotEqual) + { + // Case (h) + _collectedPathMemberExpressionsInExpression = new HashSet<string>(); + + baseResult = base.VisitBinaryExpression(expression); + + _memberExpressionMapping = FixedMapping(_collectedPathMemberExpressionsInExpression, F); + } + else if (expression.Type == typeof(bool) + && expression.NodeType == ExpressionType.Equal) + { + // Case (i) + _collectedPathMemberExpressionsInExpression = new HashSet<string>(); + + baseResult = base.VisitBinaryExpression(expression); + + _memberExpressionMapping = FixedMapping(_collectedPathMemberExpressionsInExpression, T); + } + else // +, * etc. + { + // Case (j) + _collectedPathMemberExpressionsInExpression = new HashSet<string>(); + + baseResult = base.VisitBinaryExpression(expression); + + _memberExpressionMapping = FixedMapping(_collectedPathMemberExpressionsInExpression, TNF); + } + return baseResult; + } + + private static Dictionary<string, int> FixedMapping(IEnumerable<string> collectedPathMemberExpressionsInExpression, int value) + { + var memberExpressionMapping = new Dictionary<string, int>(); + foreach (var me in collectedPathMemberExpressionsInExpression) + { + memberExpressionMapping.Add(me, value); + } + return memberExpressionMapping; + } + + private static Dictionary<string, int> BinaryMapping(Dictionary<string, int> leftMapping, Dictionary<string, int> rightMapping, int[,] op) + { + var result = new Dictionary<string, int>(); + // Compute mapping for all member expressions in leftMapping. If the member expression is missing + // in rightMapping, use TNF as a "pessimistic approximation" instead (inside the ?: operator). See + // the text for an explanation of this. + foreach (var lhs in leftMapping) + { + result.Add(lhs.Key, op[lhs.Value, rightMapping.ContainsKey(lhs.Key) ? rightMapping[lhs.Key] : TNF]); + } + // Compute mapping for all member expressions *only* in rightMapping (we did the common ones above). + // Again, use TNF as pessimistic approximation to result of left subcondition. + foreach (var rhs in rightMapping) + { + if (!leftMapping.ContainsKey(rhs.Key)) + { + result[rhs.Key] = op[rhs.Value, TNF]; + } + } + return result; + } + + private static bool IsNullConstantExpression(Expression expression) + { + if (expression is ConstantExpression) + { + var constant = (ConstantExpression)expression; + return constant.Value == null; + } + else + { + return false; + } + } + + protected override Expression VisitUnaryExpression(UnaryExpression expression) + { + ArgumentUtility.CheckNotNull("expression", expression); + + Expression baseResult; + if (expression.NodeType == ExpressionType.Not && expression.Type == typeof(bool)) + { + // Case (c) from text at NH-2583. + baseResult = VisitExpression(expression.Operand); + + var opMapping = _memberExpressionMapping; + _memberExpressionMapping = new Dictionary<string, int>(); + + foreach (var m in opMapping) + { + _memberExpressionMapping.Add(m.Key, NOT[m.Value]); + } + } + else + { + baseResult = base.VisitUnaryExpression(expression); + } + return baseResult; + } + + //protected override Expression VisitConditionalExpression(ConditionalExpression expression) + //{ + // ArgumentUtility.CheckNotNull("expression", expression); + + // VisitExpression(expression.Test); + // // If the ?: returns bool, it is (most probably ...) a condition which may require outer joins. + // // TODO: Check check whether HQL accepts ?: conditions; if not, should be rewritten it as (a && b || !a && c). + // if (expression.Type == typeof(bool)) + // { + // ... + // } + // return expression; + //} + + protected override Expression VisitMemberExpression(MemberExpression expression) + { + ArgumentUtility.CheckNotNull("expression", expression); + + Expression newExpression; + try + { + _memberExpressionDepth++; + newExpression = base.VisitExpression(expression.Expression); + } + finally + { + _memberExpressionDepth--; + } + bool isEntity = _isEntityDecider.IsEntity(expression.Type); + + if (isEntity) + { + // See (h) why we do not check for _memberExpressionDepth here! + _collectedPathMemberExpressionsInExpression.Add(ExpressionKeyVisitor.Visit(expression, null)); + } + + if (_memberExpressionDepth > 0 && isEntity) + { + return AddJoin(expression); + } + else + { + if (newExpression != expression.Expression) + return Expression.MakeMemberAccess(newExpression, expression.Member); + return expression; + } + } + + internal static void Find(Expression expression, NameGenerator nameGenerator, IIsEntityDecider isEntityDecider, Dictionary<string, NhJoinClause> joins, Dictionary<MemberExpression, QuerySourceReferenceExpression> expressionMap) + { + WhereJoinDetector f = new WhereJoinDetector(nameGenerator, isEntityDecider, joins, expressionMap); + + f.VisitExpression(expression); + + foreach (var mapping in f._memberExpressionMapping) + { + // If outer join can never produce true, we can safely inner join. + if ((mapping.Value & T) == 0) + { + f.MakeInnerIfJoined(mapping.Key); + } + } + } + + } +} Modified: trunk/nhibernate/src/NHibernate/NHibernate.csproj =================================================================== --- trunk/nhibernate/src/NHibernate/NHibernate.csproj 2011-04-14 21:27:16 UTC (rev 5704) +++ trunk/nhibernate/src/NHibernate/NHibernate.csproj 2011-04-15 04:10:51 UTC (rev 5705) @@ -266,12 +266,17 @@ <Compile Include="ISessionFactory.cs" /> <Compile Include="ITransaction.cs" /> <Compile Include="LazyInitializationException.cs" /> + <Compile Include="Linq\Clauses\NhJoinClause.cs" /> <Compile Include="Linq\Functions\DictionaryGenerator.cs" /> + <Compile Include="Linq\ReWriters\AddJoinsReWriter.cs" /> <Compile Include="Linq\ReWriters\MoveOrderByToEndRewriter.cs" /> <Compile Include="Linq\ReWriters\ResultOperatorRewriter.cs" /> <Compile Include="Linq\ReWriters\ResultOperatorRewriterResult.cs" /> + <Compile Include="Linq\Visitors\AbstractJoinDetector.cs" /> <Compile Include="Linq\Visitors\ExpressionTreeVisitor.cs" /> <Compile Include="Linq\Visitors\ResultOperatorProcessors\ProcessAggregateFromSeed.cs" /> + <Compile Include="Linq\Visitors\SelectAndOrderByJoinDetector.cs" /> + <Compile Include="Linq\Visitors\WhereJoinDetector.cs" /> <Compile Include="Loader\Loader.cs" /> <Compile Include="Loader\OuterJoinLoader.cs" /> <Compile Include="LockMode.cs" /> @@ -892,7 +897,6 @@ <Compile Include="Linq\Expressions\NhMinExpression.cs" /> <Compile Include="Linq\Expressions\NhSumExpression.cs" /> <Compile Include="Impl\ExpressionQueryImpl.cs" /> - <Compile Include="Linq\ReWriters\AddLeftJoinsReWriter.cs" /> <Compile Include="Linq\Visitors\IHqlExpressionVisitor.cs" /> <Compile Include="Linq\GroupJoin\IsAggregatingResults.cs" /> <Compile Include="Linq\GroupJoin\GroupJoinAggregateDetectionVisitor.cs" /> @@ -908,7 +912,6 @@ <Compile Include="Linq\Functions\StringGenerator.cs" /> <Compile Include="Linq\Functions\QueryableGenerator.cs" /> <Compile Include="Linq\ReWriters\RemoveUnnecessaryBodyOperators.cs" /> - <Compile Include="Linq\Clauses\LeftJoinClause.cs" /> <Compile Include="Linq\IntermediateHqlTree.cs" /> <Compile Include="Linq\Visitors\ResultOperatorProcessors\ProcessCast.cs" /> <Compile Include="Linq\Visitors\ResultOperatorProcessors\ProcessOfType.cs" /> @@ -922,7 +925,6 @@ <Compile Include="Linq\Visitors\ResultOperatorProcessors\ResultOperatorProcessorBase.cs" /> <Compile Include="Linq\Visitors\ResultOperatorProcessors\ProcessResultOperatorReturn.cs" /> <Compile Include="Linq\Visitors\ResultOperatorProcessors\IResultOperatorProcessor.cs" /> - <Compile Include="Linq\Visitors\LeftJoinDetector.cs" /> <Compile Include="Linq\Visitors\NameGenerator.cs" /> <Compile Include="Linq\Visitors\ResultOperatorProcessors\ProcessAggregate.cs" /> <Compile Include="Linq\Visitors\ResultOperatorProcessors\ProcessAll.cs" /> Added: trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/AbstractMassTestingFixture.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/AbstractMassTestingFixture.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/AbstractMassTestingFixture.cs 2011-04-15 04:10:51 UTC (rev 5705) @@ -0,0 +1,254 @@ +using NHibernate.Linq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace NHibernate.Test.NHSpecificTest.NH2583 +{ + public abstract class AbstractMassTestingFixture : BugTestCase + { + private class ValueTuple<T1, T2, T3, T4, T5, T6, T7> + { + public T1 Item1; + public T2 Item2; + public T3 Item3; + public T4 Item4; + public T5 Item5; + public T6 Item6; + public T7 Item7; + } + + private static IEnumerable<ValueTuple<T1, T2, T3, T4, T5, T6, T7>> GetAllTestCases<T1, T2, T3, T4, T5, T6, T7>() + { + foreach (T1 v1 in Enum.GetValues(typeof(T1))) + { + foreach (T2 v2 in Enum.GetValues(typeof(T2))) + { + foreach (T3 v3 in Enum.GetValues(typeof(T3))) + { + foreach (T4 v4 in Enum.GetValues(typeof(T4))) + { + foreach (T5 v5 in Enum.GetValues(typeof(T5))) + { + foreach (T6 v6 in Enum.GetValues(typeof(T6))) + { + foreach (T7 v7 in Enum.GetValues(typeof(T7))) + { + yield return + new ValueTuple<T1, T2, T3, T4, T5, T6, T7> { Item1 = v1, Item2 = v2, Item3 = v3, Item4 = v4, Item5 = v5, Item6 = v6, Item7 = v7 }; + } + } + } + } + } + } + } + } + + public class SetterTuple<T1, T2, T3, T4, T5, T6, T7> + { + private readonly Action<MyBO, ISession, T1> _set1; + private readonly Action<MyBO, ISession, T2> _set2; + private readonly Action<MyBO, ISession, T3> _set3; + private readonly Action<MyBO, ISession, T4> _set4; + private readonly Action<MyBO, ISession, T5> _set5; + private readonly Action<MyBO, ISession, T6> _set6; + private readonly Action<MyBO, ISession, T7> _set7; + + public SetterTuple(Action<MyBO, ISession, T1> set1, + Action<MyBO, ISession, T2> set2, + Action<MyBO, ISession, T3> set3, + Action<MyBO, ISession, T4> set4, + Action<MyBO, ISession, T5> set5, + Action<MyBO, ISession, T6> set6, + Action<MyBO, ISession, T7> set7) + { + _set1 = set1; + _set2 = set2; + _set3 = set3; + _set4 = set4; + _set5 = set5; + _set6 = set6; + _set7 = set7; + } + + public void Set(MyBO bo, ISession s, T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, T7 item7) + { + if (_set1 != null) { _set1(bo, s, item1); } + if (_set2 != null) { _set2(bo, s, item2); } + if (_set3 != null) { _set3(bo, s, item3); } + if (_set4 != null) { _set4(bo, s, item4); } + if (_set5 != null) { _set5(bo, s, item5); } + if (_set6 != null) { _set6(bo, s, item6); } + if (_set7 != null) { _set7(bo, s, item7); } + } + } + + + protected void RunTest<T1, T2, T3, T4, T5, T6, T7>(Expression<Func<MyBO, bool>> condition, SetterTuple<T1, T2, T3, T4, T5, T6, T7> setters) + { + if (condition == null) + { + throw new ArgumentNullException("condition"); + } + if (setters == null) + { + throw new ArgumentNullException("setters"); + } + IEnumerable<int> expectedIds; + + // Setup + using (var session = OpenSession()) + { + using (session.BeginTransaction()) + { + using (var tx = session.BeginTransaction()) + { + expectedIds = CreateObjects(session, setters, condition.Compile()); + tx.Commit(); + } + } + } + + try + { + // Test + using (var session = OpenSession()) + { + using (session.BeginTransaction()) + { + TestAndAssert(condition, session, expectedIds); + } + } + + } + finally + { + // Teardown + using (var session = OpenSession()) + { + using (var tx = session.BeginTransaction()) + { + DeleteAll<MyBO>(session); + DeleteAll<MyRef1>(session); + DeleteAll<MyRef2>(session); + DeleteAll<MyRef3>(session); + tx.Commit(); + } + } + } + } + + protected abstract void TestAndAssert(Expression<Func<MyBO, bool>> condition, ISession session, IEnumerable<int> expectedIds); + + protected static SetterTuple<T1, T2, T3, T4, T5, T6, T7> Setters<T1, T2, T3, T4, T5, T6, T7>(Action<MyBO, ISession, T1> set1, + Action<MyBO, ISession, T2> set2, + Action<MyBO, ISession, T3> set3, + Action<MyBO, ISession, T4> set4, + Action<MyBO, ISession, T5> set5, + Action<MyBO, ISession, T6> set6, + Action<MyBO, ISession, T7> set7) + { + return new SetterTuple<T1, T2, T3, T4, T5, T6, T7>(set1, set2, set3, set4, set5, set6, set7); + } + + protected static SetterTuple<T1, T2, T3, T4, T5, T6, Ignore> Setters<T1, T2, T3, T4, T5, T6>(Action<MyBO, ISession, T1> set1, + Action<MyBO, ISession, T2> set2, + Action<MyBO, ISession, T3> set3, + Action<MyBO, ISession, T4> set4, + Action<MyBO, ISession, T5> set5, + Action<MyBO, ISession, T6> set6) + { + return new SetterTuple<T1, T2, T3, T4, T5, T6, Ignore>(set1, set2, set3, set4, set5, set6, null); + } + + protected static SetterTuple<T1, T2, T3, T4, T5, Ignore, Ignore> Setters<T1, T2, T3, T4, T5>(Action<MyBO, ISession, T1> set1, + Action<MyBO, ISession, T2> set2, + Action<MyBO, ISession, T3> set3, + Action<MyBO, ISession, T4> set4, + Action<MyBO, ISession, T5> set5) + { + return new SetterTuple<T1, T2, T3, T4, T5, Ignore, Ignore>(set1, set2, set3, set4, set5, null, null); + } + + protected static SetterTuple<T1, T2, T3, T4, Ignore, Ignore, Ignore> Setters<T1, T2, T3, T4>(Action<MyBO, ISession, T1> set1, + Action<MyBO, ISession, T2> set2, + Action<MyBO, ISession, T3> set3, + Action<MyBO, ISession, T4> set4) + { + return new SetterTuple<T1, T2, T3, T4, Ignore, Ignore, Ignore>(set1, set2, set3, set4, null, null, null); + } + + protected static SetterTuple<T1, T2, T3, Ignore, Ignore, Ignore, Ignore> Setters<T1, T2, T3>(Action<MyBO, ISession, T1> set1, + Action<MyBO, ISession, T2> set2, + Action<MyBO, ISession, T3> set3) + { + return new SetterTuple<T1, T2, T3, Ignore, Ignore, Ignore, Ignore>(set1, set2, set3,null, null, null, null); + } + + protected static SetterTuple<T1, T2, Ignore, Ignore, Ignore, Ignore, Ignore> Setters<T1, T2>(Action<MyBO, ISession, T1> set1, + Action<MyBO, ISession, T2> set2) + { + return new SetterTuple<T1, T2, Ignore, Ignore, Ignore, Ignore, Ignore>(set1, set2, null, null, null, null, null); + } + + protected static SetterTuple<T1, Ignore, Ignore, Ignore, Ignore, Ignore, Ignore> Setters<T1>(Action<MyBO, ISession, T1> set1) + { + return new SetterTuple<T1, Ignore, Ignore, Ignore, Ignore, Ignore, Ignore>(set1, null, null, null, null, null, null); + } + + private static void DeleteAll<T>(ISession session) + { + foreach (var bo in session.Query<T>()) + { + session.Delete(bo); + } + } + + private static IEnumerable<int> CreateObjects<T1, T2, T3, T4, T5, T6, T7>(ISession session, SetterTuple<T1, T2, T3, T4, T5, T6, T7> setters, Func<MyBO, bool> condition) + { + var expectedIds = new List<int>(); + bool thereAreSomeWithTrue = false; + bool thereAreSomeWithFalse = false; + foreach (var q in GetAllTestCases<T1, T2, T3, T4, T5, T6, T7>()) + { + MyBO bo = new MyBO(); + setters.Set(bo, session, q.Item1, q.Item2, q.Item3, q.Item4, q.Item5, q.Item6, q.Item7); + try + { + if (condition(bo)) + { + expectedIds.Add(bo.Id); + thereAreSomeWithTrue = true; + } + else + { + thereAreSomeWithFalse = true; + } + session.Save(bo); + } + catch (NullReferenceException) + { + // ignore - we only check consistency with Linq2Objects in non-failing cases; + // emulating the outer-join logic for exceptional cases in Lin2Objects is IMO very hard. + } + } + if (!thereAreSomeWithTrue) + { + throw new ArgumentException("Condition is false for all - not a good test", "condition"); + } + if (!thereAreSomeWithFalse) + { + throw new ArgumentException("Condition is true for all - not a good test", "condition"); + } + return expectedIds; + } + + protected static void AreEqual(IEnumerable<int> expectedIds, IEnumerable<int> actualList) + { + Assert.That(() => actualList.ToList(), Is.EquivalentTo(expectedIds)); + } + } +} Added: trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/Domain.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/Domain.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/Domain.cs 2011-04-15 04:10:51 UTC (rev 5705) @@ -0,0 +1,261 @@ +namespace NHibernate.Test.NHSpecificTest.NH2583 +{ + public class MyRef1 + { + private static int _idCt = 1000; + private int _id; + + public MyRef1() + { + _id = ++_idCt; + } + + public virtual int Id { + get { return _id; } + set { _id = value; } + } + + public virtual int? I1 { get; set; } + public virtual int I2 { get; set; } + public virtual int I3 { get; set; } + + public virtual MyRef2 BO2 { get; set; } + public virtual MyRef3 BO3 { get; set; } + + public virtual MyRef2 GetOrCreateBO2(ISession s) + { + if (BO2 == null) + { + BO2 = new MyRef2(); + s.Save(BO2); + } + return BO2; + } + + public virtual MyRef3 GetOrCreateBO3(ISession s) + { + if (BO3 == null) + { + BO3 = new MyRef3(); + s.Save(BO3); + } + return BO3; + } + } + + public class MyRef2 + { + private static int _idCt = 1000; + private int _id; + + public MyRef2() + { + _id = ++_idCt; + } + + public virtual int Id { + get { return _id; } + set { _id = value; } + } + + public virtual int? J1 { get; set; } + public virtual int J2 { get; set; } + public virtual int J3 { get; set; } + } + + public class MyRef3 + { + private static int _idCt = 3000; + private int _id; + + public MyRef3() + { + _id = ++_idCt; + } + + public virtual int Id { + get { return _id; } + set { _id = value; } + } + + public virtual int L1 { get; set; } + } + + public enum Ignore { Ignore } + public enum TK { Zero, One } + public enum TBO1_I { Null, Zero, One } + public enum TBO2_J { Null, Zero, One } + public enum TBO1_BO2_J { Null, BO1, Zero, One } + public enum TBO1_BO3_L { Null, BO1, Zero, One } + + public class MyBO + { + private static int _idCt = 0; + private int _id; + + public MyBO() + { + _id = ++_idCt; + } + + public virtual int Id { + get { return _id; } + set { _id = value; } + } + + public virtual string Name { get; set; } + public virtual MyBO LeftSon { get; set; } + public virtual MyBO RightSon { get; set; } + public virtual MyRef1 BO1 { get; set; } + public virtual MyRef1 OtherBO1 { get; set; } + public virtual MyRef2 BO2 { get; set; } + public virtual int? K1 { get; set; } + public virtual int K2 { get; set; } + public virtual int K3 { get; set; } + + private MyRef1 GetOrCreateBO1(ISession s) + { + if (BO1 == null) + { + BO1 = new MyRef1(); + s.Save(BO1); + } + return BO1; + } + + private MyRef2 GetOrCreateBO2(ISession s) + { + if (BO2 == null) + { + BO2 = new MyRef2(); + s.Save(BO2); + } + return BO2; + } + + public static void SetK1(MyBO bo, ISession s, TK value) + { + bo.K1 = value == TK.One ? 1 : 0; + } + + public static void SetK2(MyBO bo, ISession s, TK value) + { + bo.K2 = value == TK.One ? 1 : 0; + } + + public static void SetK3(MyBO bo, ISession s, TK value) + { + bo.K3 = value == TK.One ? 1 : 0; + } + + private static void SetBO1_I(MyBO bo, ISession s, TBO1_I value, System.Action<MyRef1, int> set) + { + switch (value) + { + case TBO1_I.Null: + bo.BO1 = null; + break; + case TBO1_I.One: + set(bo.GetOrCreateBO1(s), 1); + break; + case TBO1_I.Zero: + set(bo.GetOrCreateBO1(s), 0); + break; + } + } + + public static void SetBO1_I1(MyBO bo, ISession s, TBO1_I value) + { + SetBO1_I(bo, s, value, (b, i) => b.I1 = i); + } + + public static void SetBO1_I2(MyBO bo, ISession s, TBO1_I value) + { + SetBO1_I(bo, s, value, (b, i) => b.I2 = i); + } + + public static void SetBO1_I3(MyBO bo, ISession s, TBO1_I value) + { + SetBO1_I(bo, s, value, (b, i) => b.I3 = i); + } + + private static void SetBO2_J(MyBO bo, ISession s, TBO2_J value, System.Action<MyRef2, int> set) + { + switch (value) + { + case TBO2_J.Null: + bo.BO2 = null; + break; + case TBO2_J.One: + set(bo.GetOrCreateBO2(s), 1); + break; + case TBO2_J.Zero: + set(bo.GetOrCreateBO2(s), 0); + break; + } + } + + public static void SetBO2_J1(MyBO bo, ISession s, TBO2_J value) + { + SetBO2_J(bo, s, value, (b, i) => b.J1 = i); + } + + public static void SetBO2_J2(MyBO bo, ISession s, TBO2_J value) + { + SetBO2_J(bo, s, value, (b, i) => b.J2 = i); + } + + public static void SetBO2_J3(MyBO bo, ISession s, TBO2_J value) + { + SetBO2_J(bo, s, value, (b, i) => b.J3 = i); + } + + private static void SetBO1_BO2_J(MyBO bo, ISession s, TBO1_BO2_J value, System.Action<MyRef2, int> set) + { + switch (value) + { + case TBO1_BO2_J.Null: + bo.BO1 = null; + break; + case TBO1_BO2_J.BO1: + bo.GetOrCreateBO1(s).BO2 = null; + break; + case TBO1_BO2_J.Zero: + set(bo.GetOrCreateBO1(s).GetOrCreateBO2(s), 0); + break; + case TBO1_BO2_J.One: + set(bo.GetOrCreateBO1(s).GetOrCreateBO2(s), 1); + break; + } + } + + public static void SetBO1_BO2_J1(MyBO bo, ISession s, TBO1_BO2_J value) + { + SetBO1_BO2_J(bo, s, value, (b, i) => b.J1 = i); + } + + public static void Set_BO1_BO2_J2(MyBO bo, ISession s, TBO1_BO2_J value) + { + SetBO1_BO2_J(bo, s, value, (b, i) => b.J2 = i); + } + + public static void SetBO1_BO3_L1(MyBO bo, ISession s, TBO1_BO3_L value) + { + switch (value) + { + case TBO1_BO3_L.Null: + bo.BO1 = null; + break; + case TBO1_BO3_L.BO1: + bo.GetOrCreateBO1(s).BO3 = null; + break; + case TBO1_BO3_L.Zero: + bo.GetOrCreateBO1(s).GetOrCreateBO3(s).L1 = 0; + break; + case TBO1_BO3_L.One: + bo.GetOrCreateBO1(s).GetOrCreateBO3(s).L1 = 1; + break; + } + } + } +} Added: trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/ManualTestFixture.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/ManualTestFixture.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH2583/ManualTestFixture.cs 2011-04-15 04:10:51 UTC (rev 5705) @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using NHibernate.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH2583 +{ + public class ManualTestFixture : BugTestCase + { + /// <summary> + /// This setup is used in most tests in here - but not all; and we might want to + /// twist it for special tests. Therefore, the OnSetUp mechanism is not used. + /// </summary> + [Test] + private void StandardSetUp() + { + using (var session = OpenSession()) + { + using (var tx = session.BeginTransaction()) + { + var i1001 = new MyRef1 { Id = 1001, I1 = null, I2 = 101 }; + var i1101 = new MyRef1 { Id = 1101, I1 = null, I2 = 111 }; + var i1002 = new MyRef1 { Id = 1002, I1 = 12, I2 = 102 }; + var i1003 = new MyRef1 { Id = 1003, I1 = 13, I2 = 103 }; + session.Save(i1001); + session.Save(i1101); + session.Save(i1002); + session.Save(i1003); + + var j2001 = new MyRef2 { Id = 2001, J1 = null, J2 = 201 }; + var j2002 = new MyRef2 { Id = 2002, J1 = 22, J2 = 202 }; + var j2003 = new MyRef2 { Id = 2003, J1 = null, J2 = 203 }; + session.Save(j2001); + session.Save(j2002); + session.Save(j2003); + + var b10 = new MyBO { Id = 10, Name = "1:1001,o1:NULL,2:NULL", BO1 = i1001, OtherBO1 = null, BO2 = null }; + var b11 = new MyBO { Id = 11, Name = "1:1001,o1:1101,2:NULL", BO1 = i1001, OtherBO1 = i1101, BO2 = null }; + var b20 = new MyBO { Id = 20, Name = "1:1002,o1:NULL,2:2002", BO1 = i1002, OtherBO1 = null, BO2 = j2002 }; + var b30 = new MyBO { Id = 30, Name = "1:NULL,o1:NULL,2:2003", BO1 = null, OtherBO1 = null, BO2 = j2003 }; + session.Save(b10); + session.Save(b11); + session.Save(b20); + session.Save(b30); + tx.Commit(); + } + } + } + + public void OrWithTrueShouldBeEqualToTrue() + { + StandardSetUp(); + + int compareCt; + using (var session = OpenSession()) + { + var result = session.Query<MyBO>(); + // 3.1.0/2011-03-19: OK - select mybo0_.Id as Id0_, mybo0_.Name as Name0_, mybo0_.BO1Key as BO3_0_, mybo0_.OtherBO1Key as OtherBO4_0_, mybo0_.BO2Key as BO5_0_ from MyBO mybo0_ + var resultList = result.ToList(); + compareCt = resultList.Count; + Assert.IsTrue(compareCt > 0); + } + using (var session = OpenSession()) + { + var result = session.Query<MyBO>() + .Where(bo => true); + // 3.1.0/2011-03-19: OK - exec sp_executesql N'select mybo0_.Id as Id0_, mybo0_.Name as Name0_, mybo0_.BO1Key as BO3_0_, mybo0_.OtherBO1Key as OtherBO4_0_, mybo0_.BO2Key as BO5_0_ from MyBO mybo0_ where @p0=1',N'@p0 bit',@p0=1 + var resultList = result.ToList(); + Assert.AreEqual(compareCt, resultList.Count); + } + using (var session = OpenSession()) + { + var result = session.Query<MyBO>() + .Where(bo => + bo.BO1 != null && bo.BO1.I2 == 101 + || true + ); + // 3.1.0/2011-03-19: WRONG - exec sp_executesql N'select mybo0_.Id as Id0_, mybo0_.Name as Name0_, mybo0_.BO1Key as BO3_0_, mybo0_.OtherBO1Key as OtherBO4_0_, mybo0_.BO2Key as BO5_0_ from MyBO mybo0_, MyRef1 myref1x1_ where mybo0_.BO1Key=myref1x1_.Id and ((mybo0_.BO1Key is not null) and myref1x1_.I2=@p0 or @p1=1)',N'@p0 int,@p1 bit',@p0=101,@p1=1 + var resultList = result.ToList(); + Assert.AreEqual(compareCt, resultList.Count); + } + using (var session = OpenSession()) + { + var result = session.Query<MyBO>() + .Where(bo => + bo.BO1 != null && bo.BO1.I2 == 101 + || bo.Id == bo.Id + 0 + ); + // 3.1.0/2011-03-19: WRONG - exec sp_executesql N'select mybo0_.Id as Id0_, mybo0_.Name as Name0_, mybo0_.BO1Key as BO3_0_, mybo0_.OtherBO1Key as OtherBO4_0_, mybo0_.BO2Key as BO5_0_ from MyBO mybo0_, MyRef1 myref1x1_ where mybo0_.BO1Key=myref1x1_.Id and ((mybo0_.BO1Key is not null) and myref1x1_.I2=@p0 or mybo0_.Id=mybo0_.Id+@p1)',N'@p0 int,@p1 int',@p0=101,@p1=0 + var resultList = result.ToList(); + Assert.AreEqual(compareCt, resultList.Count); + } + } + + [Test] + public void OrAndNavigationsShouldUseOuterJoins() + { + StandardSetUp(); + + using (var session = OpenSession()) + { + var result = session.Query<MyBO>() + .Where(bo => + bo.BO1 != null && bo.BO1.I2 == 101 + // || bo.BO2 != null && bo.BO2.J2 == 203 - is added below! + ); + var resultList = result.ToList(); + Assert.IsTrue(resultList.Count > 0); + } + using (var session = OpenSession()) + { + var result = session.Query<MyBO>() + .Where(bo => + bo.BO1 != null && bo.BO1.I2 == 101 + || bo.BO2 != null && bo.BO2.J2 == 203 + ); + var resultList = result.ToList(); + Assert.IsTrue(resultList.Count > 0); + } + using (var session = OpenSession()) + { + var result = session.Query<MyBO>() + .Where(bo => + bo.BO1.I2 == 101 && bo.BO1 != null + || bo.BO2.J2 == 203 && bo.BO2 != null + ); + var resultList = result.ToList(); + Assert.IsTrue(resultList.Count > 0); + } + } + + [Test] + public void OrShouldBeCompatibleWithAdditionForNullReferences() + { + // This tests against the "outer join anomaly" of (||-3) semantics. + StandardSetUp(); + + using (var session = OpenSession()) + { + List<MyBO> leftResult; + List<MyBO> rightResult; + List<MyBO> orResult; + TestCoreOrShouldBeCompatibleWithSum(session, + bo => bo.BO1.I2 == null, + bo => bo.BO2.J2 == null, + bo => bo.BO1.I2 == null || bo.BO2.J2 == null, out leftResult, out rightResult, out orResult); + //Assert.AreEqual(0, leftResult.Count); + //Assert.AreEqual(0, rightResult.Count); + //Assert.AreEqual(0, orResult.Count); + + } + } + + private static void TestCoreOrShouldBeCompatibleWithSum(ISession session, + Expression<Func<MyBO, bool>> left, + Expression<Func<MyBO, bool>> right, + Expression<Func<MyBO, bool>> both, + out List<MyBO> leftResult, + out List<MyBO> rightResult, + out List<MyBO> orResult) + { + leftResult = session.Query<MyBO>() + .Where(left + ).ToList(); + + rightResult = session.Query<MyBO>() + .Where(right + ).ToList(); + + orResult = session.Query<MyBO>() + .Where(both + ).ToList(); + Assert.IsTrue(orResult.Count <= leftResult.Count + rightResult.Count); + } + + [Test] + public void OrShouldBeCompatibleWithAdditionForNonNullReferences() + { + // This tests one case of existing references - so that we do not fall + // into trap of testing only empty results that might be empty for other reasons. + StandardSetUp(); + + using (var session = OpenSession()) + { + List<MyBO> leftResult; + List<MyBO> rightResult; + List<MyBO> orResult; + TestCoreOrShouldBeCompatibleWithSum(session, + bo => bo.BO1.I1 == null, + bo => bo.BO2.J1 == null, + bo => bo.BO1.I1 == null || bo.BO2.J1 == null, out leftResult, out rightResult, out orResult); + Assert.That(() => leftResult.Count, Is.GreaterThan(0)); + Assert.That(() => rightResult.Count, Is.GreaterThan(0)); + Assert.That(() => orResult.Count, Is.GreaterThan(0)); + } + } + + [Test, Ignore("Pure Outer Join semantics has projection anomaly!")] + public void ProjectionDoesNotChangeResult() + { + // This tests against the "projection anomaly" of (||-4) semantics. + using (var session = OpenSession()) + { + using (var tx = session.BeginTransaction()) + { + var i1001 = new MyRef1 { Id = 1001, I1 = null, I2 = 101 }; + var i1101 = new MyRef1 { Id = 1101, I1 = null, I2 = 111 }; + session.Save(i1001); + session.Save(i1101); + + var b1 = new MyBO { Id = 1, Name = "1:1001", BO1 = i1001 }; + var b2 = new MyBO { Id = 2, Name = "2:1101", BO1 = i1101 }; + var b3 = new MyBO { Id = 3, Name = "3:1101", BO1 = i1101 }; + var b4 = new MyBO { Id = 4, Name = "4:NULL", BO1 = null }; + session.Save(b1); + session.Save(b2); + session.Save(b3); + session.Save(b4); + tx.Commit(); + }... [truncated message content] |