Skip to content

Commit 6f943a4

Browse files
committed
Add support for HasValue
1 parent eec2400 commit 6f943a4

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

src/PgKeyValueDB/SqlExpressionVisitor.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,36 @@ private string BuildJsonPath(MemberInfo member, string parentPath = "value")
248248

249249
protected override Expression VisitMember(MemberExpression node)
250250
{
251+
// Handle HasValue property on nullable types
252+
if (node.Member.Name == "HasValue" && node.Expression is MemberExpression nullableMember)
253+
{
254+
// Check if this is accessing a closure field
255+
if (nullableMember.Expression?.NodeType == ExpressionType.Constant)
256+
{
257+
var value = Expression.Lambda(node).Compile().DynamicInvoke();
258+
AddParameter(value, node.Type);
259+
return node;
260+
}
261+
262+
// For nullable properties, HasValue means the JSON field is not null
263+
if (nullableMember.Expression is MemberExpression nestedMember)
264+
{
265+
var parentPath = BuildNestedJsonPath(nestedMember);
266+
var jsonPath = BuildJsonPath(nullableMember.Member, parentPath);
267+
// Remove the cast since we're checking for null
268+
var pathWithoutCast = jsonPath.Substring(1, jsonPath.LastIndexOf(')') - 1);
269+
whereClause.Append($"({pathWithoutCast}) is not null");
270+
}
271+
else if (nullableMember.Expression?.NodeType == ExpressionType.Parameter)
272+
{
273+
var jsonPath = BuildJsonPath(nullableMember.Member);
274+
// Remove the cast since we're checking for null
275+
var pathWithoutCast = jsonPath.Substring(1, jsonPath.LastIndexOf(')') - 1);
276+
whereClause.Append($"({pathWithoutCast}) is not null");
277+
}
278+
return node;
279+
}
280+
251281
// Handle closure/captured variables
252282
if (node.Expression?.NodeType == ExpressionType.Constant)
253283
{

test/PgKeyValueDB.Tests/PgKeyValueDBTest.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,4 +755,50 @@ public async Task FilterByIsNullOrWhiteSpaceTest()
755755
Assert.AreEqual(1, usersWithValidDisplayName.Count);
756756
Assert.AreEqual("Alice", usersWithValidDisplayName[0].Name);
757757
}
758+
759+
[TestMethod]
760+
public async Task FilterByHasValueTest()
761+
{
762+
var key1 = nameof(FilterByHasValueTest) + "1";
763+
var key2 = nameof(FilterByHasValueTest) + "2";
764+
var key3 = nameof(FilterByHasValueTest) + "3";
765+
var pid = nameof(FilterByHasValueTest);
766+
767+
var user1 = new UserProfile
768+
{
769+
Name = "Alice",
770+
IsVerified = true
771+
};
772+
773+
var user2 = new UserProfile
774+
{
775+
Name = "Bob",
776+
IsVerified = false
777+
};
778+
779+
var user3 = new UserProfile
780+
{
781+
Name = "Charlie",
782+
IsVerified = null // null value
783+
};
784+
785+
await kv.UpsertAsync(key1, user1, pid);
786+
await kv.UpsertAsync(key2, user2, pid);
787+
await kv.UpsertAsync(key3, user3, pid);
788+
789+
// This query should filter users where IsVerified has a value (not null)
790+
Expression<Func<UserProfile, bool>> expr = u => u.IsVerified.HasValue;
791+
var usersWithVerificationStatus = await kv.GetListAsync(pid, expr).ToListAsync();
792+
793+
Assert.AreEqual(2, usersWithVerificationStatus.Count);
794+
Assert.IsTrue(usersWithVerificationStatus.Any(u => u.Name == "Alice"));
795+
Assert.IsTrue(usersWithVerificationStatus.Any(u => u.Name == "Bob"));
796+
797+
// Test the opposite - users where IsVerified is null
798+
Expression<Func<UserProfile, bool>> exprNoValue = u => !u.IsVerified.HasValue;
799+
var usersWithoutVerificationStatus = await kv.GetListAsync(pid, exprNoValue).ToListAsync();
800+
801+
Assert.AreEqual(1, usersWithoutVerificationStatus.Count);
802+
Assert.AreEqual("Charlie", usersWithoutVerificationStatus[0].Name);
803+
}
758804
}

0 commit comments

Comments
 (0)