#2921·LiteDB

LINQ silently mistranslates unary minus and bitwise complement: `-x.Balance > 100m` becomes `$.Balance > @p0`

Author: JKamskerCreated Sep 18, 2026Updated Sep 18, 2026

Version dev at 7d2a16c4. #2905 rewrites the LINQ translator but deliberately preserves this behaviour, so it is present there too.

Describe the bug The LINQ translator ignores unary arithmetic operators. -x.Balance is translated as if it were x.Balance, so the query runs without any error and returns the wrong documents. Bitwise complement (~) is mistranslated as a logical NOT.

Code to reproduce

csharp
public class Account { public int Id { get; set; } public decimal Balance { get; set; } public int Flags { get; set; } }

using var db = new LiteDatabase(":memory:");
var col = db.GetCollection<Account>("accounts");
col.Insert(new Account { Id = 1, Balance = 120.5m });
col.Insert(new Account { Id = 2, Balance = -120.5m });

var ids = col.Find(x => -x.Balance > 100m).Select(a => a.Id).ToArray();
// actual:   [1]
// expected: [2]   (what LINQ-to-objects returns)

What BsonMapper.Global.GetExpression(...) produces:

Lambda Source Problem
x => -x.Balance > 100m ($.Balance>@p0) minus dropped, wrong rows
x => -x.Flags < 0 ($.Flags<@p0) minus dropped, wrong rows
x => ~x.Flags == 0 (($.Flags=false)=@p0) complement treated as logical NOT
x => +x.Balance > 100m ($.Balance>@p0) harmless, unary plus is the identity

A negated constant is fine (x => x.Balance == -50m binds -50), because the compiler folds it. Only a negated member or sub-expression is affected.

Expected behavior Either translate the operator (-x.Balance could become (0-$.Balance) or ($.Balance*-1)), or throw NotSupportedException like other unsupported LINQ constructs. Silently returning different rows is the one outcome that should not happen.

Cause LinqExpressionVisitor.VisitUnary handles Not, Convert and ArrayLength and sends everything else to base.VisitUnary(node), which visits the operand and discards the operator (LiteDB/Client/Mapper/Linq/LinqExpressionVisitor.cs, around line 343). ExpressionType.Negate, NegateChecked and UnaryPlus all take that path. For ~, the C# compiler emits ExpressionType.Not on an integer operand, and the Not branch assumes a boolean. In #2905 the equivalent fall-through is the last line of LinqExpressionTranslator.TranslateUnary.

Workaround Write the subtraction explicitly: x => (0 - x.Balance) > 100m translates to ((@p0-$.Balance)>@p1) and returns the right rows.

Found while auditing #2905; it is not caused by that PR.