From 62285e4e18caaf6102db3163d6495f983a4d84e7 Mon Sep 17 00:00:00 2001 From: maumar Date: Wed, 5 Mar 2025 15:12:37 -0800 Subject: [PATCH 1/3] Fix to #21006 - Support a default value for non-nullable properties Only for scalar properties when projecting Json-mapped entity. Only need to change code for Cosmos - relational already works in the desired way after the change to streaming (properties that are not encountered maintain their default value) We still throw exception if JSON contains explicit null where non-nullable scalar is expected. Fixes #21006 --- ...ionBindingRemovingExpressionVisitorBase.cs | 31 +- .../Query/AdHocCosmosTestHelpers.cs | 43 ++ .../Query/AdHocJsonQueryCosmosTest.cs | 659 ++++++++++++++++++ .../AdHocMiscellaneousQueryCosmosTest.cs | 139 ++++ .../InMemoryComplianceTest.cs | 1 + ...cs => AdHocJsonQueryRelationalTestBase.cs} | 46 +- .../Query/AdHocJsonQueryTestBase.cs | 419 +++++++++++ .../Query/AdHocJsonQuerySqlServerTestBase.cs | 179 ++++- .../Query/AdHocJsonQuerySqliteTest.cs | 68 +- 9 files changed, 1573 insertions(+), 12 deletions(-) create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs rename test/EFCore.Relational.Specification.Tests/Query/{AdHocJsonQueryTestBase.cs => AdHocJsonQueryRelationalTestBase.cs} (97%) create mode 100644 test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index d349d73e609..6e434aee428 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -691,7 +691,11 @@ private Expression CreateGetValueExpression( && !property.IsShadowProperty()) { var readExpression = CreateGetValueExpression( - jTokenExpression, storeName, type.MakeNullable(), property.GetTypeMapping()); + jTokenExpression, + storeName, + type.MakeNullable(), + property.GetTypeMapping(), + isNonNullableScalar: false); var nonNullReadExpression = readExpression; if (nonNullReadExpression.Type != type) @@ -712,7 +716,14 @@ private Expression CreateGetValueExpression( } return Convert( - CreateGetValueExpression(jTokenExpression, storeName, type.MakeNullable(), property.GetTypeMapping()), + CreateGetValueExpression( + jTokenExpression, + storeName, + type.MakeNullable(), + property.GetTypeMapping(), + // special case keys - we check them for null to see if the entity needs to be materialized, so we want to keep the null, rather than non-nullable default + // returning defaults is supposed to help with evolving the schema - so this doesn't concern keys anyway (they shouldn't evolve) + isNonNullableScalar: !property.IsNullable && !property.IsKey()), type); } @@ -720,7 +731,8 @@ private Expression CreateGetValueExpression( Expression jTokenExpression, string storeName, Type type, - CoreTypeMapping typeMapping = null) + CoreTypeMapping typeMapping = null, + bool isNonNullableScalar = false) { Check.DebugAssert(type.IsNullableType(), "Must read nullable type from JObject."); @@ -763,6 +775,7 @@ var body Constant(CosmosClientWrapper.Serializer)), converter.ConvertFromProviderExpression.Body); + var originalBodyType = body.Type; if (body.Type != type) { body = Convert(body, type); @@ -783,7 +796,11 @@ var body } else { - replaceExpression = Default(type); + replaceExpression = isNonNullableScalar + ? Expression.Convert( + Default(originalBodyType), + type) + : Default(type); } body = Condition( @@ -799,7 +816,11 @@ var body } else { - valueExpression = ConvertJTokenToType(jTokenExpression, typeMapping?.ClrType.MakeNullable() ?? type); + valueExpression = ConvertJTokenToType( + jTokenExpression, + (isNonNullableScalar + ? typeMapping?.ClrType + : typeMapping?.ClrType.MakeNullable()) ?? type); if (valueExpression.Type != type) { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs new file mode 100644 index 00000000000..e0a942e4915 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.Azure.Cosmos; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocCosmosTestHelpers +{ + public static async Task CreateCustomEntityHelperAsync( + Container container, + string json, + CancellationToken cancellationToken) + { + var document = JObject.Parse(json); + + var stream = new MemoryStream(); + await using var __ = stream.ConfigureAwait(false); + var writer = new StreamWriter(stream, new UTF8Encoding(), bufferSize: 1024, leaveOpen: false); + await using var ___ = writer.ConfigureAwait(false); + using var jsonWriter = new JsonTextWriter(writer); + + CosmosClientWrapper.Serializer.Serialize(jsonWriter, document); + await jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false); + + var response = await container.CreateItemStreamAsync( + stream, + PartitionKey.None, + requestOptions: null, + cancellationToken) + .ConfigureAwait(false); + + + if (response.StatusCode != HttpStatusCode.Created) + { + throw new InvalidOperationException($"Failed to create entitty (status code: {response.StatusCode}) for json: {json}"); + } + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs new file mode 100644 index 00000000000..979ad3d8ba0 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs @@ -0,0 +1,659 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocJsonQueryCosmosTest : AdHocJsonQueryTestBase +{ + public override async Task Project_root_with_missing_scalars(bool async) + { + if (async) + { + await base.Project_root_with_missing_scalars(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] < 4) +"""); + } + } + + [ConditionalTheory(Skip = "issue #35702")] + public override async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + if (async) + { + await base.Project_top_level_json_entity_with_missing_scalars(async); + + AssertSql(); + } + } + + public override async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + if (async) + { + await AssertTranslationFailed( + () => base.Project_nested_json_entity_with_missing_scalars(async)); + + AssertSql(); + } + } + + [ConditionalTheory(Skip = "issue #34067")] + public override async Task Project_top_level_entity_with_null_value_required_scalars(bool async) + { + if (async) + { + await base.Project_top_level_entity_with_null_value_required_scalars(async); + + AssertSql( + """ +SELECT c["Id"], c +FROM root c +WHERE (c["Id"] = 4) +"""); + } + } + + public override async Task Project_root_entity_with_missing_required_navigation(bool async) + { + if (async) + { + await base.Project_root_entity_with_missing_required_navigation(async); + + AssertSql( + """ +ReadItem(?, ?) +"""); + } + } + + public override async Task Project_missing_required_navigation(bool async) + { + if (async) + { + await base.Project_missing_required_navigation(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] = 5) +"""); + } + } + + public override async Task Project_root_entity_with_null_required_navigation(bool async) + { + if (async) + { + await base.Project_root_entity_with_null_required_navigation(async); + + AssertSql( + """ +ReadItem(?, ?) +"""); + } + } + + public override async Task Project_null_required_navigation(bool async) + { + if (async) + { + await base.Project_null_required_navigation(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] = 6) +"""); + } + } + + public override async Task Project_missing_required_scalar(bool async) + { + if (async) + { + await base.Project_missing_required_scalar(async); + + AssertSql( + """ +SELECT c["Id"], c["RequiredReference"]["Number"] +FROM root c +WHERE (c["Id"] = 2) +"""); + } + } + + public override async Task Project_null_required_scalar(bool async) + { + if (async) + { + await base.Project_null_required_scalar(async); + + AssertSql( + """ +SELECT c["Id"], c["RequiredReference"]["Number"] +FROM root c +WHERE (c["Id"] = 4) +"""); + } + } + + protected override void OnModelCreating21006(ModelBuilder modelBuilder) + { + base.OnModelCreating21006(modelBuilder); + + modelBuilder.Entity().ToContainer("Entities"); + } + + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(StoreName, containerId: "Entities"); + + var missingTopLevel = +$$""" +{ + "Id": 2, + "$type": "Entity", + "Name": "e2", + "id": "2", + "Collection": [ + { + "Text": "e2 c1", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 nrr" + } + }, + { + "Text": "e2 c2", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 nrr" + } + } + ], + "OptionalReference": { + "Text": "e2 or", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or nrr" + } + }, + "RequiredReference": { + "Text": "e2 rr", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr nrr" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingTopLevel, + CancellationToken.None); + + var missingNested = +$$""" +{ + "Id": 3, + "$type": "Entity", + "Name": "e3", + "id": "3", + "Collection": [ + { + "Number": 7.0, + "Text": "e3 c1", + "NestedCollection": [ + { + "Text": "e3 c1 c1" + }, + { + "Text": "e3 c1 c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 c1 nor" + }, + "NestedRequiredReference": { + "Text": "e3 c1 nrr" + } + }, + { + "Number": 7.0, + "Text": "e3 c2", + "NestedCollection": [ + { + "Text": "e3 c2 c1" + }, + { + "Text": "e3 c2 c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 c2 nor" + }, + "NestedRequiredReference": { + "Text": "e3 c2 nrr" + } + } + ], + "OptionalReference": { + "Number": 7.0, + "Text": "e3 or", + "NestedCollection": [ + { + "Text": "e3 or c1" + }, + { + "Text": "e3 or c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 or nor" + }, + "NestedRequiredReference": { + "Text": "e3 or nrr" + } + }, + "RequiredReference": { + "Number": 7.0, + "Text": "e3 rr", + "NestedCollection": [ + { + "Text": "e3 rr c1" + }, + { + "Text": "e3 rr c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 rr nor" + }, + "NestedRequiredReference": { + "Text": "e3 rr nrr" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingNested, + CancellationToken.None); + + var nullTopLevel = +$$""" +{ + "Id": 4, + "$type": "Entity", + "Name": "e4", + "id": "4", + "Collection": [ + { + "Number": null, + "Text": "e4 c1", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 nrr" + } + }, + { + "Number": null, + "Text": "e4 c2", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 nrr" + } + } + ], + "OptionalReference": { + "Number": null, + "Text": "e4 or", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or nrr" + } + }, + "RequiredReference": { + "Number": null, + "Text": "e4 rr", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr nrr" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + nullTopLevel, + CancellationToken.None); + + var missingRequiredNav = +$$""" +{ + "Id": 5, + "$type": "Entity", + "Name": "e5", + "id": "5", + "Collection": [ + { + "Number": 7.0, + "Text": "e5 c1", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c1 nor" + }, + }, + { + "Number": 7.0, + "Text": "e5 c2", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c2 nor" + }, + } + ], + "OptionalReference": { + "Number": 7.0, + "Text": "e5 or", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 or nor" + }, + }, + "RequiredReference": { + "Number": 7.0, + "Text": "e5 rr", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 rr nor" + }, + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingRequiredNav, + CancellationToken.None); + + var nullRequiredNav = +$$""" +{ + "Id": 6, + "$type": "Entity", + "Name": "e6", + "id": "6", + "Collection": [ + { + "Number": 7.0, + "Text": "e6 c1", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c1 nor" + }, + "NestedRequiredReference": null + }, + { + "Number": 7.0, + "Text": "e6 c2", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c2 nor" + }, + "NestedRequiredReference": null + } + ], + "OptionalReference": { + "Number": 7.0, + "Text": "e6 or", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 or nor" + }, + "NestedRequiredReference": null + }, + "RequiredReference": { + "Number": 7.0, + "Text": "e6 rr", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 rr nor" + }, + "NestedRequiredReference": null + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + nullRequiredNav, + CancellationToken.None); + } + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + private void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); + + protected static async Task AssertTranslationFailed(Func query) + => Assert.Contains( + CoreStrings.TranslationFailed("")[48..], + (await Assert.ThrowsAsync(query)) + .Message); + + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder.ConfigureWarnings(b => b.Ignore(CosmosEventId.NoPartitionKeyDefined)); + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs index e2dd2b90904..9b7ee298cc0 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; namespace Microsoft.EntityFrameworkCore.Query; @@ -9,6 +10,144 @@ namespace Microsoft.EntityFrameworkCore.Query; public class AdHocMiscellaneousQueryCosmosTest : NonSharedModelTestBase { + #region 21006 + + [ConditionalFact] + public virtual async Task Project_all_types_entity_with_missing_scalars() + { + var contextFactory = await InitializeAsync( + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set(); + + var result = await query.ToListAsync(); + } + + public void OnModelCreating21006(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.ToContainer("Entities"); + b.Property(x => x.TestDecimal).HasPrecision(18, 3); + b.OwnsOne(x => x.Reference, bb => + { + bb.Property(x => x.TestDecimal).HasPrecision(18, 3); + bb.Property(x => x.TestEnumWithIntConverter).HasConversion(); + }); + }); + } + + protected async Task Seed21006(JsonContext21006 context) + { + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(StoreName, containerId: "Entities"); + + var missingTopLevel = +$$""" +{ + "Id": 1, + "$type": "Entity", + "id": "1", + "Reference": { + "Text": "e2 or" + }, +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingTopLevel, + CancellationToken.None); + } + + protected class JsonContext21006(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + + public short TestInt16 { get; set; } + public int TestInt32 { get; set; } + public long TestInt64 { get; set; } + public double TestDouble { get; set; } + public decimal TestDecimal { get; set; } + public DateTime TestDateTime { get; set; } + public DateTimeOffset TestDateTimeOffset { get; set; } + public TimeSpan TestTimeSpan { get; set; } + public DateOnly TestDateOnly { get; set; } + public TimeOnly TestTimeOnly { get; set; } + public float TestSingle { get; set; } + public bool TestBoolean { get; set; } + public byte TestByte { get; set; } + + public byte[] TestByteArray { get; set; } + public Guid TestGuid { get; set; } + public ushort TestUnsignedInt16 { get; set; } + public uint TestUnsignedInt32 { get; set; } + public ulong TestUnsignedInt64 { get; set; } + public char TestCharacter { get; set; } + public sbyte TestSignedByte { get; set; } + public int? TestNullableInt32 { get; set; } + public JsonEnum TestEnum { get; set; } + public byte[] TestByteCollection { get; set; } + public IList TestUnsignedInt16Collection { get; set; } + public uint[] TestUnsignedInt32Collection { get; set; } + public sbyte[] TestSignedByteCollection { get; set; } + public JsonEntity Reference { get; set; } + } + + public class JsonEntity + { + public string Text { get; set; } + + public short TestInt16 { get; set; } + public int TestInt32 { get; set; } + public long TestInt64 { get; set; } + public double TestDouble { get; set; } + public decimal TestDecimal { get; set; } + public DateTime TestDateTime { get; set; } + public DateTimeOffset TestDateTimeOffset { get; set; } + public TimeSpan TestTimeSpan { get; set; } + public DateOnly TestDateOnly { get; set; } + public TimeOnly TestTimeOnly { get; set; } + public float TestSingle { get; set; } + public bool TestBoolean { get; set; } + public byte TestByte { get; set; } + public byte[] TestByteArray { get; set; } + public Guid TestGuid { get; set; } + public ushort TestUnsignedInt16 { get; set; } + public uint TestUnsignedInt32 { get; set; } + public ulong TestUnsignedInt64 { get; set; } + public char TestCharacter { get; set; } + public sbyte TestSignedByte { get; set; } + public int? TestNullableInt32 { get; set; } + public JsonEnum TestEnum { get; set; } + public JsonEnum TestEnumWithIntConverter { get; set; } + + public byte[] TestByteCollection { get; set; } + public IList TestUnsignedInt16Collection { get; set; } + public uint[] TestUnsignedInt32Collection { get; set; } + + public sbyte[] TestSignedByteCollection { get; set; } + } + + public enum JsonEnum + { + One = -1, + Two = 2, + Three = -3 + } + } + + #endregion + #region 34911 [ConditionalFact] diff --git a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs index a6707db2bce..1a6e951b5de 100644 --- a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs @@ -27,6 +27,7 @@ public class InMemoryComplianceTest : ComplianceTestBase typeof(NonSharedModelBulkUpdatesTestBase), typeof(NorthwindBulkUpdatesTestBase<>), typeof(JsonQueryTestBase<>), + typeof(AdHocJsonQueryTestBase), // TODO: implement later once things are baked typeof(ComplexRelationshipsInProjectionNoTrackingQueryTestBase<>), diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs similarity index 97% rename from test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs rename to test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs index ff85628c439..4bf85dc526d 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs @@ -7,15 +7,50 @@ namespace Microsoft.EntityFrameworkCore.Query; #nullable disable -public abstract class AdHocJsonQueryTestBase : NonSharedModelTestBase +public abstract class AdHocJsonQueryRelationalTestBase : AdHocJsonQueryTestBase { - protected override string StoreName - => "AdHocJsonQueryTest"; + #region 21006 - protected virtual void ConfigureWarnings(WarningsConfigurationBuilder builder) + public override async Task Project_missing_required_navigation(bool async) { + var message = (await Assert.ThrowsAsync( + () => base.Project_missing_required_navigation(async))).Message; + + Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(typeof(Context21006.JsonEntityNested).Name), message); + } + + public override async Task Project_null_required_navigation(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Project_null_required_navigation(async))).Message; + + Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(typeof(Context21006.JsonEntityNested).Name), message); } + public override async Task Project_top_level_entity_with_null_value_required_scalars(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Project_top_level_entity_with_null_value_required_scalars(async))).Message; + + Assert.Equal("Cannot get the value of a token type 'Null' as a number.", message); + } + + protected override void OnModelCreating21006(ModelBuilder modelBuilder) + { + base.OnModelCreating21006(modelBuilder); + + modelBuilder.Entity( + b => + { + b.ToTable("Entities"); + b.OwnsOne(x => x.OptionalReference).ToJson(); + b.OwnsOne(x => x.RequiredReference).ToJson(); + b.OwnsMany(x => x.Collection).ToJson(); + }); + } + + #endregion + #region 32310 [ConditionalTheory] @@ -44,7 +79,8 @@ protected virtual async Task Seed32310(DbContext context) { var user = new Pub32310 { - Name = "FBI", Visits = new Visits32310 { LocationTag = "tag", DaysVisited = [new DateOnly(2023, 1, 1)] } + Name = "FBI", + Visits = new Visits32310 { LocationTag = "tag", DaysVisited = [new DateOnly(2023, 1, 1)] } }; context.Add(user); diff --git a/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs new file mode 100644 index 00000000000..820b7acc872 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs @@ -0,0 +1,419 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public abstract class AdHocJsonQueryTestBase : NonSharedModelTestBase +{ + protected override string StoreName + => "AdHocJsonQueryTests"; + + protected virtual void ClearLog() + => ListLoggerFactory.Clear(); + + protected virtual void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + } + + #region 21006 + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_root_with_missing_scalars(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id < 4); + + var result = async + ? await query.ToListAsync() + : query.ToList()!; + + var topLevel = result.Single(x => x.Id == 2); + var nested = result.Single(x => x.Id == 3); + + Assert.Equal(default, topLevel.OptionalReference.Number); + Assert.Equal(default, topLevel.RequiredReference.Number); + Assert.True(topLevel.Collection.All(x => x.Number == default)); + + Assert.Equal(default, nested.RequiredReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.RequiredReference.NestedOptionalReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedOptionalReference.DoB); + Assert.True(nested.Collection.SelectMany(x => x.NestedCollection).All(x => x.DoB == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id < 4).Select(x => new + { + x.Id, + x.OptionalReference, + x.RequiredReference, + x.Collection + }).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var topLevel = result.Single(x => x.Id == 2); + var nested = result.Single(x => x.Id == 3); + + Assert.Equal(default, topLevel.OptionalReference.Number); + Assert.Equal(default, topLevel.RequiredReference.Number); + Assert.True(topLevel.Collection.All(x => x.Number == default)); + + Assert.Equal(default, nested.RequiredReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.RequiredReference.NestedOptionalReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedOptionalReference.DoB); + Assert.True(nested.Collection.SelectMany(x => x.NestedCollection).All(x => x.DoB == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id < 4).Select(x => new + { + x.Id, + x.OptionalReference.NestedOptionalReference, + x.RequiredReference.NestedRequiredReference, + x.Collection[0].NestedCollection + }).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var topLevel = result.Single(x => x.Id == 2); + var nested = result.Single(x => x.Id == 3); + + Assert.Equal(default, nested.NestedOptionalReference.DoB); + Assert.Equal(default, nested.NestedRequiredReference.DoB); + Assert.True(nested.NestedCollection.All(x => x.DoB == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_top_level_entity_with_null_value_required_scalars(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 4).Select(x => new + { + x.Id, + x.RequiredReference, + }).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var nullScalars = result.Single(); + + Assert.Equal(default, nullScalars.RequiredReference.Number); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_root_entity_with_missing_required_navigation(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 5).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var missingRequiredNav = result.Single(); + + Assert.Equal(default, missingRequiredNav.RequiredReference.NestedRequiredReference); + Assert.Equal(default, missingRequiredNav.OptionalReference.NestedRequiredReference); + Assert.True(missingRequiredNav.Collection.All(x => x.NestedRequiredReference == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_missing_required_navigation(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 5).Select(x => x.RequiredReference.NestedRequiredReference).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var missingRequiredNav = result.Single(); + + Assert.Equal(default, missingRequiredNav); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_root_entity_with_null_required_navigation(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 6).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var nullRequiredNav = result.Single(); + + Assert.Equal(default, nullRequiredNav.RequiredReference.NestedRequiredReference); + Assert.Equal(default, nullRequiredNav.OptionalReference.NestedRequiredReference); + Assert.True(nullRequiredNav.Collection.All(x => x.NestedRequiredReference == default)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_null_required_navigation(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(x => x.Id == 6).Select(x => x.RequiredReference).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var nullRequiredNav = result.Single(); + + Assert.Equal(default, nullRequiredNav.NestedRequiredReference); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_missing_required_scalar(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set() + .Where(x => x.Id == 2) + .Select(x => new + { + x.Id, + Number = (double?)x.RequiredReference.Number + }); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Null(result.Single().Number); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Project_null_required_scalar(bool async) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set() + .Where(x => x.Id == 4) + .Select(x => new + { + x.Id, + Number = (double?)x.RequiredReference.Number, + }); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Null(result.Single().Number); + } + + protected virtual void OnModelCreating21006(ModelBuilder modelBuilder) + => modelBuilder.Entity( + b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.OptionalReference, bb => + { + bb.OwnsOne(x => x.NestedOptionalReference); + bb.OwnsOne(x => x.NestedRequiredReference); + bb.Navigation(x => x.NestedRequiredReference).IsRequired(); + bb.OwnsMany(x => x.NestedCollection); + }); + b.OwnsOne( + x => x.RequiredReference, bb => + { + bb.OwnsOne(x => x.NestedOptionalReference); + bb.OwnsOne(x => x.NestedRequiredReference); + bb.Navigation(x => x.NestedRequiredReference).IsRequired(); + bb.OwnsMany(x => x.NestedCollection); + }); + b.Navigation(x => x.RequiredReference).IsRequired(); + b.OwnsMany( + x => x.Collection, bb => + { + bb.OwnsOne(x => x.NestedOptionalReference); + bb.OwnsOne(x => x.NestedRequiredReference); + bb.Navigation(x => x.NestedRequiredReference).IsRequired(); + bb.OwnsMany(x => x.NestedCollection); + }); + }); + + protected virtual async Task Seed21006(Context21006 context) + { + // everything + var e1 = new Context21006.Entity + { + Id = 1, + Name = "e1", + OptionalReference = new Context21006.JsonEntity + { + Number = 7, + Text = "e1 or", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or nrr" }, + NestedCollection = new List + { + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or c1" }, + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or c2" }, + } + }, + + RequiredReference = new Context21006.JsonEntity + { + Number = 7, + Text = "e1 rr", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr nrr" }, + NestedCollection = new List + { + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr c1" }, + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr c2" }, + } + }, + Collection = new List + { + new Context21006.JsonEntity + { + Number = 7, + Text = "e1 c1", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 nrr" }, + NestedCollection = new List + { + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 c1" }, + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 c2" }, + } + }, + new Context21006.JsonEntity + { + Number = 7, + Text = "e1 c2", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 nrr" }, + NestedCollection = new List + { + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 c1" }, + new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 c2" }, + } + }, + } + }; + + context.Add(e1); + await context.SaveChangesAsync(); + } + + protected class Context21006(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + public string Name { get; set; } + public JsonEntity OptionalReference { get; set; } + public JsonEntity RequiredReference { get; set; } + public List Collection { get; set; } + } + + public class JsonEntity + { + public string Text { get; set; } + public double Number { get; set; } + + public JsonEntityNested NestedOptionalReference { get; set; } + public JsonEntityNested NestedRequiredReference { get; set; } + public List NestedCollection { get; set; } + } + + public class JsonEntityNested + { + public DateTime DoB { get; set; } + public string Text { get; set; } + } + } + + #endregion +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs index 6cc90cd4c02..6733cf07709 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Query; #nullable disable -public abstract class AdHocJsonQuerySqlServerTestBase : AdHocJsonQueryTestBase +public abstract class AdHocJsonQuerySqlServerTestBase : AdHocJsonQueryRelationalTestBase { protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; @@ -21,6 +21,183 @@ protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) builder.Log(CoreEventId.StringEnumValueInJson, SqlServerEventId.JsonTypeExperimental); } + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); + + public override async Task Project_root_with_missing_scalars(bool async) + { + await base.Project_root_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + await base.Project_top_level_json_entity_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[OptionalReference], [e].[RequiredReference], [e].[Collection] +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + await base.Project_nested_json_entity_with_missing_scalars(async); + + AssertSql( +""" +SELECT [e].[Id], JSON_QUERY([e].[OptionalReference], '$.NestedOptionalReference'), JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), JSON_QUERY([e].[Collection], '$[0].NestedCollection') +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_root_entity_with_missing_required_navigation(bool async) + { + await base.Project_root_entity_with_missing_required_navigation(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] = 5 +"""); + } + + + public override async Task Project_missing_required_navigation(bool async) + { + await base.Project_missing_required_navigation(async); + + AssertSql( + """ +SELECT JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), [e].[Id] +FROM [Entities] AS [e] +WHERE [e].[Id] = 5 +"""); + } + + public override async Task Project_root_entity_with_null_required_navigation(bool async) + { + await base.Project_root_entity_with_null_required_navigation(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] = 6 +"""); + } + + public override async Task Project_null_required_navigation(bool async) + { + await base.Project_null_required_navigation(async); + + AssertSql( + """ +SELECT [e].[RequiredReference], [e].[Id] +FROM [Entities] AS [e] +WHERE [e].[Id] = 6 +"""); + } + + public override async Task Project_missing_required_scalar(bool async) + { + await base.Project_missing_required_scalar(async); + + AssertSql( + """ +SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 2 +"""); + } + + public override async Task Project_null_required_scalar(bool async) + { + await base.Project_null_required_scalar(async); + + AssertSql( + """ +SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 4 +"""); + } + + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + // missing scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Text":"e2 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nrr"}},{"Text":"e2 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nrr"}}]', +N'{"Text":"e2 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nrr"}}', +N'{"Text":"e2 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nrr"}}', +2, +N'e2') +"""); + + // missing scalar on nested level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Number":7,"Text":"e3 c1","NestedCollection":[{"Text":"e3 c1 c1"},{"Text":"e3 c1 c2"}],"NestedOptionalReference":{"Text":"e3 c1 nor"},"NestedRequiredReference":{"Text":"e3 c1 nrr"}},{"Number":7,"Text":"e3 c2","NestedCollection":[{"Text":"e3 c2 c1"},{"Text":"e3 c2 c2"}],"NestedOptionalReference":{"Text":"e3 c2 nor"},"NestedRequiredReference":{"Text":"e3 c2 nrr"}}]', +N'{"Number":7,"Text":"e3 or","NestedCollection":[{"Text":"e3 or c1"},{"Text":"e3 or c2"}],"NestedOptionalReference":{"Text":"e3 or nor"},"NestedRequiredReference":{"Text":"e3 or nrr"}}', +N'{"Number":7,"Text":"e3 rr","NestedCollection":[{"Text":"e3 rr c1"},{"Text":"e3 rr c2"}],"NestedOptionalReference":{"Text":"e3 rr nor"},"NestedRequiredReference":{"Text":"e3 rr nrr"}}', +3, +N'e3') +"""); + + // null scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Number":null,"Text":"e4 c1","NestedCollection":[{"Text":"e4 c1 c1"},{"Text":"e4 c1 c2"}],"NestedOptionalReference":{"Text":"e4 c1 nor"},"NestedRequiredReference":{"Text":"e4 c1 nrr"}},{"Number":null,"Text":"e4 c2","NestedCollection":[{"Text":"e4 c2 c1"},{"Text":"e4 c2 c2"}],"NestedOptionalReference":{"Text":"e4 c2 nor"},"NestedRequiredReference":{"Text":"e4 c2 nrr"}}]', +N'{"Number":null,"Text":"e4 or","NestedCollection":[{"Text":"e4 or c1"},{"Text":"e4 or c2"}],"NestedOptionalReference":{"Text":"e4 or nor"},"NestedRequiredReference":{"Text":"e4 or nrr"}}', +N'{"Number":null,"Text":"e4 rr","NestedCollection":[{"Text":"e4 rr c1"},{"Text":"e4 rr c2"}],"NestedOptionalReference":{"Text":"e4 rr nor"},"NestedRequiredReference":{"Text":"e4 rr nrr"}}', +4, +N'e4') +"""); + + // missing required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Number":7,"Text":"e5 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 nor"}},{"Number":7,"Text":"e5 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 nor"}}]', +N'{"Number":7,"Text":"e5 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 or nor"}}', +N'{"Number":7,"Text":"e5 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 rr nor"}}', +5, +N'e5') +"""); + + // null required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +N'[{"Number":7,"Text":"e6 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 nor"},"NestedRequiredReference":null},{"Number":7,"Text":"e6 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 nor"},"NestedRequiredReference":null}]', +N'{"Number":7,"Text":"e6 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 or nor"},"NestedRequiredReference":null}', +N'{"Number":7,"Text":"e6 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 rr nor"},"NestedRequiredReference":null}', +6, +N'e6') +"""); + } + protected override async Task Seed29219(DbContext ctx) { var entity1 = new MyEntity29219 diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs index 55f245a3f94..da40ab1a726 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs @@ -1,15 +1,81 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + namespace Microsoft.EntityFrameworkCore.Query; #nullable disable -public class AdHocJsonQuerySqliteTest : AdHocJsonQueryTestBase +public class AdHocJsonQuerySqliteTest : AdHocJsonQueryRelationalTestBase { protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + // missing scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Text":"e2 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nrr"}},{"Text":"e2 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nrr"}}]', +'{"Text":"e2 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nrr"}}', +'{"Text":"e2 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nrr"}}', +2, +'e2') +"""); + + // missing scalar on nested level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":7,"Text":"e3 c1","NestedCollection":[{"Text":"e3 c1 c1"},{"Text":"e3 c1 c2"}],"NestedOptionalReference":{"Text":"e3 c1 nor"},"NestedRequiredReference":{"Text":"e3 c1 nrr"}},{"Number":7,"Text":"e3 c2","NestedCollection":[{"Text":"e3 c2 c1"},{"Text":"e3 c2 c2"}],"NestedOptionalReference":{"Text":"e3 c2 nor"},"NestedRequiredReference":{"Text":"e3 c2 nrr"}}]', +'{"Number":7,"Text":"e3 or","NestedCollection":[{"Text":"e3 or c1"},{"Text":"e3 or c2"}],"NestedOptionalReference":{"Text":"e3 or nor"},"NestedRequiredReference":{"Text":"e3 or nrr"}}', +'{"Number":7,"Text":"e3 rr","NestedCollection":[{"Text":"e3 rr c1"},{"Text":"e3 rr c2"}],"NestedOptionalReference":{"Text":"e3 rr nor"},"NestedRequiredReference":{"Text":"e3 rr nrr"}}', +3, +'e3') +"""); + + // null scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":null,"Text":"e4 c1","NestedCollection":[{"Text":"e4 c1 c1"},{"Text":"e4 c1 c2"}],"NestedOptionalReference":{"Text":"e4 c1 nor"},"NestedRequiredReference":{"Text":"e4 c1 nrr"}},{"Number":null,"Text":"e4 c2","NestedCollection":[{"Text":"e4 c2 c1"},{"Text":"e4 c2 c2"}],"NestedOptionalReference":{"Text":"e4 c2 nor"},"NestedRequiredReference":{"Text":"e4 c2 nrr"}}]', +'{"Number":null,"Text":"e4 or","NestedCollection":[{"Text":"e4 or c1"},{"Text":"e4 or c2"}],"NestedOptionalReference":{"Text":"e4 or nor"},"NestedRequiredReference":{"Text":"e4 or nrr"}}', +'{"Number":null,"Text":"e4 rr","NestedCollection":[{"Text":"e4 rr c1"},{"Text":"e4 rr c2"}],"NestedOptionalReference":{"Text":"e4 rr nor"},"NestedRequiredReference":{"Text":"e4 rr nrr"}}', +4, +'e4') +"""); + + // missing required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":7,"Text":"e5 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 nor"}},{"Number":7,"Text":"e5 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 nor"}}]', +'{"Number":7,"Text":"e5 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 or nor"}}', +'{"Number":7,"Text":"e5 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 rr nor"}}', +5, +'e5') +"""); + + // null required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":7,"Text":"e6 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 nor"},"NestedRequiredReference":null},{"Number":7,"Text":"e6 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 nor"},"NestedRequiredReference":null}]', +'{"Number":7,"Text":"e6 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 or nor"},"NestedRequiredReference":null}', +'{"Number":7,"Text":"e6 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 rr nor"},"NestedRequiredReference":null}', +6, +'e6') +"""); + } + protected override async Task Seed29219(DbContext ctx) { var entity1 = new MyEntity29219 From c9cfc9446b4b796dfd4b22cf63eb4eae5ba83dad Mon Sep 17 00:00:00 2001 From: Maurycy Markowski Date: Sat, 8 Mar 2025 00:33:07 -0800 Subject: [PATCH 2/3] Update test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Query/AdHocCosmosTestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs index e0a942e4915..19a5b39eccc 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs @@ -37,7 +37,7 @@ public static async Task CreateCustomEntityHelperAsync( if (response.StatusCode != HttpStatusCode.Created) { - throw new InvalidOperationException($"Failed to create entitty (status code: {response.StatusCode}) for json: {json}"); + throw new InvalidOperationException($"Failed to create entity (status code: {response.StatusCode}) for json: {json}"); } } } From 4dd71e966a78e0efa6e0420c4a0481d59c61033b Mon Sep 17 00:00:00 2001 From: Maurycy Markowski Date: Sat, 8 Mar 2025 00:33:21 -0800 Subject: [PATCH 3/3] Update test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Query/AdHocMiscellaneousQueryCosmosTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs index 9b7ee298cc0..9365b6dfc7e 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocMiscellaneousQueryCosmosTest.cs @@ -55,7 +55,7 @@ protected async Task Seed21006(JsonContext21006 context) "id": "1", "Reference": { "Text": "e2 or" - }, + } } """;