From d0841f7befe7f9c1be7246d736a16f5de0558939 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 19 May 2022 09:54:43 +0200 Subject: [PATCH] Translate List.Count inside json (#2376) Fixes #2374 (cherry picked from commit e946af86d35e191891d767865b21672bbae476dd) --- .../Internal/NpgsqlArrayTranslator.cs | 3 +- .../Internal/NpgsqlJsonPocoTranslator.cs | 34 ++++++++----- .../Query/JsonDomQueryTest.cs | 8 ++-- .../Query/JsonPocoQueryTest.cs | 48 ++++++++++++++++--- 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs index 4f8dcf174..2c13cdaae 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs @@ -143,8 +143,7 @@ public NpgsqlArrayTranslator( // The array/list CLR type may be mapped to a non-array database type (e.g. byte[] to bytea, or just // value converters) - we don't want to translate for those cases. static bool IsMappedToNonArray(SqlExpression arrayOrList) - => arrayOrList.TypeMapping is RelationalTypeMapping typeMapping && - typeMapping is not (NpgsqlArrayTypeMapping or NpgsqlJsonTypeMapping); + => arrayOrList.TypeMapping is RelationalTypeMapping and not (NpgsqlArrayTypeMapping or NpgsqlJsonTypeMapping); SqlExpression? TranslateCommon(SqlExpression arrayOrList, IReadOnlyList arguments) { diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs index 042168617..65534015c 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs @@ -33,17 +33,29 @@ public NpgsqlJsonPocoTranslator( _stringTypeMapping = typeMappingSource.FindMapping(typeof(string), model)!; } - public virtual SqlExpression? Translate(SqlExpression? instance, + public virtual SqlExpression? Translate( + SqlExpression? instance, MemberInfo member, Type returnType, IDiagnosticsLogger logger) - => instance?.TypeMapping is NpgsqlJsonTypeMapping || instance is PostgresJsonTraversalExpression - ? TranslateMemberAccess( - instance, - _sqlExpressionFactory.Constant( - member.GetCustomAttribute()?.Name ?? member.Name), - returnType) - : null; + { + if (instance?.TypeMapping is not NpgsqlJsonTypeMapping && instance is not PostgresJsonTraversalExpression) + { + return null; + } + + if (member.Name == nameof(List.Count) + && member.DeclaringType?.IsGenericType == true + && member.DeclaringType.GetGenericTypeDefinition() == typeof(List<>)) + { + return TranslateArrayLength(instance); + } + + return TranslateMemberAccess( + instance, + _sqlExpressionFactory.Constant(member.GetCustomAttribute()?.Name ?? member.Name), + returnType); + } public virtual SqlExpression? TranslateMemberAccess( SqlExpression instance, SqlExpression member, Type returnType) @@ -51,8 +63,7 @@ public NpgsqlJsonPocoTranslator( // The first time we see a JSON traversal it's on a column - create a JsonTraversalExpression. // Traversals on top of that get appended into the same expression. - if (instance is ColumnExpression columnExpression && - columnExpression.TypeMapping is NpgsqlJsonTypeMapping) + if (instance is ColumnExpression { TypeMapping: NpgsqlJsonTypeMapping } columnExpression) { return ConvertFromText( _sqlExpressionFactory.JsonTraversal( @@ -76,8 +87,7 @@ public NpgsqlJsonPocoTranslator( public virtual SqlExpression? TranslateArrayLength(SqlExpression expression) { - if (expression is ColumnExpression columnExpression && - columnExpression.TypeMapping is NpgsqlJsonTypeMapping mapping) + if (expression is ColumnExpression { TypeMapping: NpgsqlJsonTypeMapping mapping }) { return _sqlExpressionFactory.Function( mapping.IsJsonb ? "jsonb_array_length" : "json_array_length", diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs index 150e9c241..90c5c7b4e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs @@ -105,7 +105,7 @@ public void Parameter_element() WHERE j.""Id"" = @__p_0 LIMIT 1", // - @"@__expected_0='{""ID"": ""00000000-0000-0000-0000-000000000000"", ""Age"": 25, ""Name"": ""Joe"", ""IsVip"": false, ""Orders"": [{""Price"": 99.5, ""ShippingAddress"": ""Some address 1""}, {""Price"": 23, ""ShippingAddress"": ""Some address 2""}], ""Statistics"": {""Nested"": {""IntArray"": [3, 4], ""SomeProperty"": 10, ""SomeNullableInt"": 20, ""SomeNullableGuid"": ""d5f2685d-e5c4-47e5-97aa-d0266154eb2d""}, ""Visits"": 4, ""Purchases"": 3}, ""VariousTypes"": {""Bool"": ""false"", ""Int16"": 8, ""Int32"": 8, ""Int64"": 8, ""String"": ""foo"", ""Decimal"": 10, ""DateTime"": ""2020-01-01T10:30:45"", ""DateTimeOffset"": ""2020-01-01T10:30:45+02:00""}}' (DbType = Object) + @"@__expected_0='{""ID"": ""00000000-0000-0000-0000-000000000000"", ""Age"": 25, ""Name"": ""Joe"", ""IsVip"": false, ""Orders"": [{""Price"": 99.5, ""ShippingAddress"": ""Some address 1""}, {""Price"": 23, ""ShippingAddress"": ""Some address 2""}], ""Statistics"": {""Nested"": {""IntList"": [3, 4], ""IntArray"": [3, 4], ""SomeProperty"": 10, ""SomeNullableInt"": 20, ""SomeNullableGuid"": ""d5f2685d-e5c4-47e5-97aa-d0266154eb2d""}, ""Visits"": 4, ""Purchases"": 3}, ""VariousTypes"": {""Bool"": ""false"", ""Int16"": 8, ""Int32"": 8, ""Int64"": 8, ""String"": ""foo"", ""Decimal"": 10, ""DateTime"": ""2020-01-01T10:30:45"", ""DateTimeOffset"": ""2020-01-01T10:30:45+02:00""}}' (DbType = Object) SELECT j.""Id"", j.""CustomerDocument"", j.""CustomerElement"" FROM ""JsonbEntities"" AS j @@ -522,7 +522,8 @@ static JsonDocument CreateCustomer1() => JsonDocument.Parse(@" ""SomeProperty"": 10, ""SomeNullableInt"": 20, ""SomeNullableGuid"": ""d5f2685d-e5c4-47e5-97aa-d0266154eb2d"", - ""IntArray"": [3, 4] + ""IntArray"": [3, 4], + ""IntList"": [3, 4] } }, ""Orders"": @@ -564,7 +565,8 @@ static JsonDocument CreateCustomer2() => JsonDocument.Parse(@" ""SomeProperty"": 20, ""SomeNullableInt"": null, ""SomeNullableGuid"": null, - ""IntArray"": [5, 6] + ""IntArray"": [5, 6, 7], + ""IntArray"": [5, 6, 7] } }, ""Orders"": diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs index 72b041101..1ba31a641 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text.Json; @@ -309,13 +310,13 @@ WHERE CAST(j.""Customer""#>>ARRAY['Statistics','Nested','IntArray',@__i_0]::TEXT public void Array_Length() { using var ctx = CreateContext(); - var x = ctx.JsonbEntities.Single(e => e.Customer.Orders.Length == 2); + var x = ctx.JsonbEntities.Single(e => e.Customer.Statistics.Nested.IntArray.Length == 2); Assert.Equal("Joe", x.Customer.Name); AssertSql( @"SELECT j.""Id"", j.""Customer"", j.""ToplevelArray"" FROM ""JsonbEntities"" AS j -WHERE jsonb_array_length(j.""Customer""->'Orders') = 2 +WHERE jsonb_array_length(j.""Customer""#>'{Statistics,Nested,IntArray}') = 2 LIMIT 2"); } @@ -323,13 +324,45 @@ WHERE jsonb_array_length(j.""Customer""->'Orders') = 2 public void Array_Length_json() { using var ctx = CreateContext(); - var x = ctx.JsonEntities.Single(e => e.Customer.Orders.Length == 2); + var x = ctx.JsonEntities.Single(e => e.Customer.Statistics.Nested.IntArray.Length == 2); Assert.Equal("Joe", x.Customer.Name); AssertSql( @"SELECT j.""Id"", j.""Customer"", j.""ToplevelArray"" FROM ""JsonEntities"" AS j -WHERE json_array_length(j.""Customer""->'Orders') = 2 +WHERE json_array_length(j.""Customer""#>'{Statistics,Nested,IntArray}') = 2 +LIMIT 2"); + } + + [Fact] + public void List_Count() + { + using var ctx = CreateContext(); + + var x = ctx.JsonbEntities.Single(e => e.Customer.Statistics.Nested.IntList.Count == 2); + + Assert.Equal("Joe", x.Customer.Name); + + AssertSql( + @"SELECT j.""Id"", j.""Customer"", j.""ToplevelArray"" +FROM ""JsonbEntities"" AS j +WHERE jsonb_array_length(j.""Customer""#>'{Statistics,Nested,IntList}') = 2 +LIMIT 2"); + } + + [Fact] + public void List_Count_json() + { + using var ctx = CreateContext(); + + var x = ctx.JsonEntities.Single(e => e.Customer.Statistics.Nested.IntList.Count == 2); + + Assert.Equal("Joe", x.Customer.Name); + + AssertSql( + @"SELECT j.""Id"", j.""Customer"", j.""ToplevelArray"" +FROM ""JsonEntities"" AS j +WHERE json_array_length(j.""Customer""#>'{Statistics,Nested,IntList}') = 2 LIMIT 2"); } @@ -585,7 +618,8 @@ public static void Seed(JsonPocoQueryContext context) SomeProperty = 10, SomeNullableInt = 20, SomeNullableGuid = Guid.Parse("d5f2685d-e5c4-47e5-97aa-d0266154eb2d"), - IntArray = new[] { 3, 4 } + IntArray = new[] { 3, 4 }, + IntList = new() { 3, 4 } } }, Orders = new[] @@ -629,7 +663,8 @@ public static void Seed(JsonPocoQueryContext context) SomeProperty = 20, SomeNullableInt = null, SomeNullableGuid = null, - IntArray = new[] { 5, 6 } + IntArray = new[] { 5, 6, 7 }, + IntList = new() { 5, 6, 7 } } }, Orders = new[] @@ -709,6 +744,7 @@ public class NestedStatistics public int SomeProperty { get; set; } public int? SomeNullableInt { get; set; } public int[] IntArray { get; set; } + public List IntList { get; set; } public Guid? SomeNullableGuid { get; set; } }