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..8c2b894c563 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs @@ -0,0 +1,629 @@ +// 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) +"""); + } + } + + 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..cee32064c72 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs @@ -0,0 +1,367 @@ +// 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); + } + + 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..2999f81d5a3 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,159 @@ 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 +"""); + } + + 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