diff --git a/Orm/Xtensive.Orm.Tests/Linq/SubqueryFilterCheckerTest.cs b/Orm/Xtensive.Orm.Tests/Linq/SubqueryFilterCheckerTest.cs
new file mode 100644
index 000000000..f63eba85d
--- /dev/null
+++ b/Orm/Xtensive.Orm.Tests/Linq/SubqueryFilterCheckerTest.cs
@@ -0,0 +1,72 @@
+// Copyright (C) 2026 Xtensive LLC.
+// This code is distributed under MIT license terms.
+// See the License.txt file in the project root for more information.
+
+using System.Linq;
+using NUnit.Framework;
+using Xtensive.Orm.Configuration;
+using Xtensive.Orm.Tests.Linq.SubqueryFilterCheckerTestModel;
+
+namespace Xtensive.Orm.Tests.Linq.SubqueryFilterCheckerTestModel
+{
+ [HierarchyRoot]
+ public class Promotion : Entity
+ {
+ [Field, Key]
+ public long Id { get; private set; }
+
+ [Field(Length = 64)]
+ public string CampainName { get; set; }
+ }
+}
+
+namespace Xtensive.Orm.Tests.Linq
+{
+ ///
+ /// Regression test for the always-on correctness fix in
+ /// SubqueryFilterRemover.SubqueryFilterChecker.
+ ///
+ /// The legacy checker unconditionally called Pop() on its internal
+ /// meaningfulLefts/meaningfulRights stacks on every null-constant
+ /// comparison it saw. When the checker visited a FilterProvider whose
+ /// predicate started with a bare null-check (e.g. x.Field == null
+ /// inside g.Count(...) after a GroupBy on a scalar key) nothing
+ /// had been pushed onto the stacks first, so Pop() threw
+ /// InvalidOperationException("Stack empty.") during LINQ translation.
+ ///
+ ///
+ [TestFixture]
+ [Category("Linq")]
+ public sealed class SubqueryFilterCheckerTest : AutoBuildTest
+ {
+ protected override DomainConfiguration BuildConfiguration()
+ {
+ var config = base.BuildConfiguration();
+ config.Types.Register(typeof(Promotion));
+ return config;
+ }
+
+ ///
+ /// Minimal reproducer for the "Stack empty." crash:
+ /// GroupBy(pk).Select(g => g.Count(x => x.NullableField == null)).
+ /// When the grouping key is the entity Id and the Select projects a
+ /// single scalar aggregate, Translator.Queryable.VisitAggregate
+ /// takes the "use grouping AggregateProvider" optimization path and
+ /// runs over
+ /// an origin data source whose top FilterProvider carries the bare
+ /// null-check predicate. With nothing pushed onto the correlation stacks
+ /// yet, the legacy checker did Pop() on empty and crashed.
+ ///
+ [Test]
+ public void GroupByCountWithBareNullPredicate_DoesNotThrow()
+ {
+ using var session = Domain.OpenSession();
+ using var tx = session.OpenTransaction();
+
+ Assert.DoesNotThrow(() => session.Query.All()
+ .GroupBy(p => p.Id)
+ .Select(g => g.Count(x => x.CampainName == null))
+ .ToArray());
+ }
+ }
+}
diff --git a/Orm/Xtensive.Orm/Orm/Linq/Rewriters/SubqueryFilterRemover.cs b/Orm/Xtensive.Orm/Orm/Linq/Rewriters/SubqueryFilterRemover.cs
index 6a7243a4b..f82a98b2e 100644
--- a/Orm/Xtensive.Orm/Orm/Linq/Rewriters/SubqueryFilterRemover.cs
+++ b/Orm/Xtensive.Orm/Orm/Linq/Rewriters/SubqueryFilterRemover.cs
@@ -57,12 +57,20 @@ protected override BinaryExpression VisitBinary(BinaryExpression b)
}
else if (@continue && rightConstant!=null) {
var rightIsNullValue = rightConstant.Value==null;
- if (!rightIsNullValue)
+ if (!rightIsNullValue) {
@continue = false;
+ }
else {
+ // A bare null-check such as `x.Field == null` may appear in a
+ // FilterProvider predicate that the checker reaches before any
+ // correlation equality has pushed anything onto the stacks
+ // (e.g. `GroupBy(k).Select(g => g.Count(x => x.Field == null))`).
+ // The legacy implementation called Pop() on an empty stack here
+ // and crashed with InvalidOperationException("Stack empty.").
+ // Treat it as "not a subquery-correlation filter" instead.
var leftIsParameter = leftAccess.Object.NodeType==ExpressionType.Parameter;
- var onStackValue = (leftIsParameter) ? meaningfulLefts.Pop() : meaningfulRights.Pop();
- @continue = onStackValue == leftAccess.Object;
+ var stack = leftIsParameter ? meaningfulLefts : meaningfulRights;
+ @continue = stack.TryPop(out var top) && top == leftAccess.Object;
}
}
else {