From 7fc176f0e051165f3c29580cbf72dc1f5a5cfb5d Mon Sep 17 00:00:00 2001 From: PhuocOng <122703392+PhuocOng@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:04:33 -0700 Subject: [PATCH] fix(postgrest): resolve nullable Value comparisons to mapped columns --- .../Postgrest.Tests/Linq/LinqQueryTests.cs | 28 +++++++++++++++++ .../Postgrest.Tests/Linq/WhereClauseTests.cs | 30 +++++++++++++++++++ .../Postgrest/Linq/WhereExpressionVisitor.cs | 10 +++---- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/packages/Postgrest/Postgrest.Tests/Linq/LinqQueryTests.cs b/packages/Postgrest/Postgrest.Tests/Linq/LinqQueryTests.cs index 2b5a881f..d4323f2c 100644 --- a/packages/Postgrest/Postgrest.Tests/Linq/LinqQueryTests.cs +++ b/packages/Postgrest/Postgrest.Tests/Linq/LinqQueryTests.cs @@ -49,6 +49,34 @@ public async Task Where_ShouldSelectRowsMatchingTheTranslatedPredicate() .Models.Should().OnlyContain(m => m.Status == MovieStatus.OnDisplay); } + [TestMethod] + public async Task Where_ShouldSelectOnlyMatchingRows_GivenNullableValueComparisons() + { + var client = LocalStack.Client(); + var marker = $"nullable-value-{Guid.NewGuid()}"; + await client.Table().Insert(new List + { + new() { StringValue = marker, IntValue = null }, + new() { StringValue = marker, IntValue = 1 }, + new() { StringValue = marker, IntValue = 5 } + }); + + try + { + var matching = await client.Table() + .Where(x => x.StringValue == marker && x.IntValue!.Value > 3).Get(); + matching.Models.Should().ContainSingle().Which.IntValue.Should().Be(5); + + var converted = await client.Table() + .Where(x => x.StringValue == marker && (long) x.IntValue!.Value > 3L).Get(); + converted.Models.Should().ContainSingle().Which.Id.Should().Be(matching.Model!.Id); + } + finally + { + await client.Table().Where(x => x.StringValue == marker).Delete(); + } + } + [TestMethod] public async Task Not_ShouldExcludeMatchingRows_GivenTheExpressionOverload() { diff --git a/packages/Postgrest/Postgrest.Tests/Linq/WhereClauseTests.cs b/packages/Postgrest/Postgrest.Tests/Linq/WhereClauseTests.cs index 962c25f5..c3197417 100644 --- a/packages/Postgrest/Postgrest.Tests/Linq/WhereClauseTests.cs +++ b/packages/Postgrest/Postgrest.Tests/Linq/WhereClauseTests.cs @@ -171,6 +171,36 @@ public void Where_ShouldNestBooleanMember_GivenAnAndPredicate() .GenerateUrl().Should().Be($"{BaseUrl}/kitchen_sink?and=(bool_value.eq.True%2cint_value.gt.3)"); } + [TestMethod] + public void Where_ShouldUseColumnName_GivenNullableValueComparison() + { + this.client.Table().Where(x => x.IntValue!.Value > 3) + .GenerateUrl().Should().Be($"{BaseUrl}/kitchen_sink?int_value=gt.3"); + } + + [TestMethod] + public void Where_ShouldUseColumnName_GivenConvertedNullableValueComparison() + { + this.client.Table().Where(x => (long) x.IntValue!.Value > 3L) + .GenerateUrl().Should().Be($"{BaseUrl}/kitchen_sink?int_value=gt.3"); + } + + [TestMethod] + public void Where_ShouldUsePrimaryKeyName_GivenNullableValueComparison() + { + var identifier = new Guid("f3ff356d-5803-43a7-b125-ba10cf10fdcd"); + this.client.Table().Where(x => x.Id!.Value == identifier) + .GenerateUrl().Should().Be($"{BaseUrl}/kitchen_sink?id=eq.{identifier}"); + } + + [TestMethod] + public void Where_ShouldEvaluateCapturedNullableValue_GivenColumnComparison() + { + int? threshold = 3; + this.client.Table().Where(x => x.IntValue > threshold.Value) + .GenerateUrl().Should().Be($"{BaseUrl}/kitchen_sink?int_value=gt.3"); + } + [TestMethod] public void Where_ShouldThrowDescriptive_GivenTwoColumnsCompared() { diff --git a/packages/Postgrest/Postgrest/Linq/WhereExpressionVisitor.cs b/packages/Postgrest/Postgrest/Linq/WhereExpressionVisitor.cs index ca9c74ce..bc23a702 100644 --- a/packages/Postgrest/Postgrest/Linq/WhereExpressionVisitor.cs +++ b/packages/Postgrest/Postgrest/Linq/WhereExpressionVisitor.cs @@ -120,13 +120,13 @@ protected override Expression VisitBinary(BinaryExpression node) Type? columnType = null; if (node.Left is MemberExpression leftMember) { - column = this.GetColumnFromMemberExpression(leftMember); + column = this.ResolveColumn(leftMember); columnType = leftMember.Type; } //To handle properly if it's a Convert ExpressionType generally with nullable properties else if (node.Left is UnaryExpression leftUnary && leftUnary.NodeType == ExpressionType.Convert && leftUnary.Operand is MemberExpression leftOperandMember) { - column = this.GetColumnFromMemberExpression(leftOperandMember); + column = this.ResolveColumn(leftOperandMember); columnType = leftOperandMember.Type; } @@ -445,16 +445,16 @@ private bool ContainsParameter(Expression expression) private class ParameterFinder : ExpressionVisitor { - private readonly ParameterExpression? _target; + private readonly ParameterExpression? target; public ParameterFinder(ParameterExpression? target) => - this._target = target; + this.target = target; public bool Found { get; private set; } protected override Expression VisitParameter(ParameterExpression node) { - if (this._target == null || node == this._target) + if (this.target == null || node == this.target) this.Found = true; return base.VisitParameter(node);