From 77bd25bdc25b3043dc70865ba838380e30abb89d Mon Sep 17 00:00:00 2001 From: maumar Date: Tue, 19 Sep 2023 13:27:48 -0700 Subject: [PATCH] [release/8.0] Respect the difference between null/empty for owned collections mapped to JSON Fixes #29348 Fixes #31731 Collaboration between @maumar and @ajcvickers ### Description A empty collection of related entities is different from a null collection in both the EF model and JSON. This change ensures we preserve the difference. ### Customer impact JSON collections that should be non-null and empty as instead null leading to app crashes. ### How found Customer reported and internal testing. ### Regression Yes. ### Testing Added test coverage. ### Risk Low. --- ...ocessingExpressionVisitor.ClientMethods.cs | 3 + ...sitor.ShaperProcessingExpressionVisitor.cs | 91 ++++- .../Update/ModificationCommand.cs | 33 +- .../Query/JsonQueryAdHocTestBase.cs | 2 +- .../Update/JsonUpdateTestBase.cs | 292 +++++++++++++- .../Update/JsonUpdateSqlServerTest.cs | 361 +++++++++++++++++- .../Update/JsonUpdateSqliteTest.cs | 336 +++++++++++++++- 7 files changed, 1068 insertions(+), 50 deletions(-) diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs index 6ed06607b35..fcc0192adda 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs @@ -992,6 +992,7 @@ private static void IncludeJsonEntityCollection innerShaper, + Action getOrCreateCollectionObject, Action fixup, bool trackingQuery) where TIncludingEntity : class @@ -1011,6 +1012,8 @@ private static void IncludeJsonEntityCollection(); var innerFixupMap = new Dictionary(); + var trackingInnerFixupMap = new Dictionary(); foreach (var ownedNavigation in entityType.GetNavigations().Where( n => n.TargetEntityType.IsMappedToJson() && n.ForeignKey.IsOwnership && n == n.ForeignKey.PrincipalToDependent)) { @@ -1303,21 +1307,30 @@ private Expression CreateJsonShapers( var shaperEntityParameter = Parameter(ownedNavigation.DeclaringEntityType.ClrType); var shaperCollectionParameter = Parameter(ownedNavigation.ClrType); var expressions = new List(); + var expressionsForTracking = new List(); if (!ownedNavigation.IsShadowProperty()) { expressions.Add( shaperEntityParameter.MakeMemberAccess(ownedNavigation.GetMemberInfo(forMaterialization: true, forSet: true)) .Assign(shaperCollectionParameter)); + + expressionsForTracking.Add( + IfThen( + OrElse( + ReferenceEqual(Constant(null), shaperCollectionParameter), + IsFalse( + Call( + typeof(EnumerableExtensions).GetMethod(nameof(EnumerableExtensions.Any))!, + shaperCollectionParameter))), + shaperEntityParameter + .MakeMemberAccess(ownedNavigation.GetMemberInfo(forMaterialization: true, forSet: true)) + .Assign(shaperCollectionParameter))); } if (ownedNavigation.Inverse is INavigation inverseNavigation && !inverseNavigation.IsShadowProperty()) { - //for (var i = 0; i < prm.Count; i++) - //{ - // prm[i].Parent = instance - //} var innerFixupCollectionElementParameter = Parameter(inverseNavigation.DeclaringEntityType.ClrType); var innerFixupParentParameter = Parameter(inverseNavigation.TargetEntityType.ClrType); @@ -1347,6 +1360,14 @@ private Expression CreateJsonShapers( shaperCollectionParameter); innerFixupMap[navigationJsonPropertyName] = fixup; + + var trackedFixup = Lambda( + Block(typeof(void), expressionsForTracking), + shaperEntityParameter, + shaperCollectionParameter); + + innerFixupMap[navigationJsonPropertyName] = fixup; + trackingInnerFixupMap[navigationJsonPropertyName] = trackedFixup; } else { @@ -1366,6 +1387,7 @@ private Expression CreateJsonShapers( jsonReaderDataShaperLambdaParameter, innerShapersMap, innerFixupMap, + trackingInnerFixupMap, _queryLogger).Rewrite(entityShaperMaterializer); var entityShaperMaterializerVariable = Variable( @@ -1416,6 +1438,9 @@ private Expression CreateJsonShapers( jsonReaderDataParameter, includingEntityExpression, shaperLambda, + GetOrCreateCollectionObjectLambda( + navigation.DeclaringEntityType.ClrType, + navigation), fixup, Constant(_isTracking)); @@ -1484,6 +1509,7 @@ private sealed class JsonEntityMaterializerRewriter : ExpressionVisitor private readonly ParameterExpression _jsonReaderDataParameter; private readonly IDictionary _innerShapersMap; private readonly IDictionary _innerFixupMap; + private readonly IDictionary _trackingInnerFixupMap; private readonly IDiagnosticsLogger _queryLogger; private static readonly PropertyInfo JsonEncodedTextEncodedUtf8BytesProperty @@ -1499,6 +1525,7 @@ public JsonEntityMaterializerRewriter( ParameterExpression jsonReaderDataParameter, IDictionary innerShapersMap, IDictionary innerFixupMap, + IDictionary trackingInnerFixupMap, IDiagnosticsLogger queryLogger) { _entityType = entityType; @@ -1506,6 +1533,7 @@ public JsonEntityMaterializerRewriter( _jsonReaderDataParameter = jsonReaderDataParameter; _innerShapersMap = innerShapersMap; _innerFixupMap = innerFixupMap; + _trackingInnerFixupMap = trackingInnerFixupMap; _queryLogger = queryLogger; } @@ -1662,10 +1690,26 @@ protected override Expression VisitSwitch(SwitchExpression switchExpression) finalBlockExpressions.Add(propertyAssignmentReplacer.Visit(jsonEntityTypeInitializerBlockExpression)); } - // fixup is only needed for non-tracking queries, in case of tracking - ChangeTracker does the job - if (!_isTracking) + // Fixup is only needed for non-tracking queries, in case of tracking - ChangeTracker does the job + // or for empty/null collections of a tracking queries. + if (_isTracking) + { + ProcessFixup(_trackingInnerFixupMap); + } + else + { + ProcessFixup(_innerFixupMap); + } + + finalBlockExpressions.Add(jsonEntityTypeVariable); + + return Block( + finalBlockVariables, + finalBlockExpressions); + + void ProcessFixup(IDictionary fixupMap) { - foreach (var fixup in _innerFixupMap) + foreach (var fixup in fixupMap) { var navigationEntityParameter = _navigationVariableMap[fixup.Key]; @@ -1674,25 +1718,15 @@ protected override Expression VisitSwitch(SwitchExpression switchExpression) // but in this case fixups are standalone, so the null safety must be added by us directly finalBlockExpressions.Add( IfThen( - AndAlso( - NotEqual( - jsonEntityTypeVariable, - Constant(null, jsonEntityTypeVariable.Type)), - NotEqual( - navigationEntityParameter, - Constant(null, navigationEntityParameter.Type))), + NotEqual( + jsonEntityTypeVariable, + Constant(null, jsonEntityTypeVariable.Type)), Invoke( fixup.Value, jsonEntityTypeVariable, _navigationVariableMap[fixup.Key]))); } } - - finalBlockExpressions.Add(jsonEntityTypeVariable); - - return Block( - finalBlockVariables, - finalBlockExpressions); } return base.VisitSwitch(switchExpression); @@ -2369,6 +2403,23 @@ private static Expression AssignReferenceNavigation( INavigationBase navigation) => entity.MakeMemberAccess(navigation.GetMemberInfo(forMaterialization: true, forSet: true)).Assign(relatedEntity); + private static Expression GetOrCreateCollectionObjectLambda( + Type entityType, + INavigationBase navigation) + { + var prm = Parameter(entityType); + + return Lambda( + Block( + typeof(void), + Call( + Constant(navigation.GetCollectionAccessor()), + CollectionAccessorGetOrCreateMethodInfo, + prm, + Constant(true))), + prm); + } + private static Expression AddToCollectionNavigation( ParameterExpression entity, ParameterExpression relatedEntity, diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index 9338966f332..5b60ebd6e40 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -258,7 +258,8 @@ private List GenerateColumnModifications() if (_entries.Count > 1 || _entries is [var singleEntry] && (singleEntry.SharedIdentityEntry is not null - || singleEntry.EntityType.GetComplexProperties().Any())) + || singleEntry.EntityType.GetComplexProperties().Any() + || singleEntry.EntityType.GetNavigations().Any(e => e.IsCollection && e.TargetEntityType.IsMappedToJson()))) { Check.DebugAssert(StoreStoredProcedure is null, "Multiple entries/shared identity not supported with stored procedures"); @@ -293,9 +294,13 @@ private List GenerateColumnModifications() HandleSharedColumns(entry.EntityType, entry, tableMapping, deleting, sharedTableColumnMap); - if (!jsonEntry && entry.EntityType.IsMappedToJson()) + if (!jsonEntry) { - jsonEntry = true; + if (entry.EntityType.IsMappedToJson() + || entry.EntityType.GetNavigations().Any(e => e.IsCollection && e.TargetEntityType.IsMappedToJson())) + { + jsonEntry = true; + } } } } @@ -684,6 +689,28 @@ void HandleJson(List columnModifications) jsonColumnsUpdateMap[jsonColumn] = jsonPartialUpdateInfo; } + foreach (var entry in _entries.Where(e => !e.EntityType.IsMappedToJson())) + { + foreach (var jsonCollectionNavigation in entry.EntityType.GetNavigations() + .Where(n => n.IsCollection + && n.TargetEntityType.IsMappedToJson() + && (entry.GetCurrentValue(n) as IEnumerable)?.Any() == false)) + { + var jsonCollectionEntityType = jsonCollectionNavigation.TargetEntityType; + var jsonCollectionColumn = + GetTableMapping(jsonCollectionEntityType)!.Table.FindColumn( + jsonCollectionEntityType.GetContainerColumnName()!)!; + + if (!jsonColumnsUpdateMap.ContainsKey(jsonCollectionColumn)) + { + var jsonPartialUpdateInfo = new JsonPartialUpdateInfo(); + jsonPartialUpdateInfo.Path.Insert(0, new JsonPartialUpdatePathEntry("$", null, entry, jsonCollectionNavigation)); + jsonPartialUpdateInfo.PropertyValue = entry.GetCurrentValue(jsonCollectionNavigation); + jsonColumnsUpdateMap[jsonCollectionColumn] = jsonPartialUpdateInfo; + } + } + } + foreach (var (jsonColumn, updateInfo) in jsonColumnsUpdateMap) { var finalUpdatePathElement = updateInfo.Path.Last(); diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs index 4716d795a18..8f3c7ce3c20 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs @@ -823,7 +823,7 @@ public class MyJsonEntityLazyLoadingProxies } #endregion - + #region NotICollection [ConditionalTheory] diff --git a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs index 43288cd58ac..110aab7b945 100644 --- a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs @@ -61,12 +61,10 @@ public virtual Task Add_entity_with_json() var newEntity = query.Where(e => e.Id == 2).Single(); Assert.Equal("NewEntity", newEntity.Name); - // TODO: #29348 - collection should be empty here - Assert.Null(newEntity.OwnedCollectionRoot); + Assert.Empty(newEntity.OwnedCollectionRoot); Assert.Equal("RootName", newEntity.OwnedReferenceRoot.Name); Assert.Equal(42, newEntity.OwnedReferenceRoot.Number); - // TODO: #29348 - collection should be empty here - Assert.Null(newEntity.OwnedReferenceRoot.OwnedCollectionBranch); + Assert.Empty(newEntity.OwnedReferenceRoot.OwnedCollectionBranch); Assert.Equal(new DateTime(2010, 10, 10), newEntity.OwnedReferenceRoot.OwnedReferenceBranch.Date); Assert.Equal(JsonEnum.Three, newEntity.OwnedReferenceRoot.OwnedReferenceBranch.Enum); Assert.Equal(42.42m, newEntity.OwnedReferenceRoot.OwnedReferenceBranch.Fraction); @@ -183,8 +181,7 @@ public virtual Task Add_json_reference_root() var updatedReference = updatedEntity.OwnedReferenceRoot; Assert.Equal("RootName", updatedReference.Name); Assert.Equal(42, updatedReference.Number); - // TODO: #29348 - collection should be empty here - Assert.Null(updatedReference.OwnedCollectionBranch); + Assert.Empty(updatedReference.OwnedCollectionBranch); Assert.Equal(new DateTime(2010, 10, 10), updatedReference.OwnedReferenceBranch.Date); Assert.Equal(JsonEnum.Three, updatedReference.OwnedReferenceBranch.Enum); Assert.Equal(42.42m, updatedReference.OwnedReferenceBranch.Fraction); @@ -265,8 +262,7 @@ public virtual Task Add_element_to_json_collection_root() Assert.Equal(3, updatedCollection.Count); Assert.Equal("new Name", updatedCollection[2].Name); Assert.Equal(142, updatedCollection[2].Number); - // TODO: #29348 - collection should be empty here - Assert.Null(updatedCollection[2].OwnedCollectionBranch); + Assert.Empty(updatedCollection[2].OwnedCollectionBranch); Assert.Equal(new DateTime(2010, 10, 10), updatedCollection[2].OwnedReferenceBranch.Date); Assert.Equal(JsonEnum.Three, updatedCollection[2].OwnedReferenceBranch.Enum); Assert.Equal(42.42m, updatedCollection[2].OwnedReferenceBranch.Fraction); @@ -3084,6 +3080,286 @@ public virtual async Task SaveChanges_throws_when_required_primitive_collection_ (await Assert.ThrowsAsync(async () => await context.SaveChangesAsync())).Message); }); + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + [InlineData(null)] + public virtual Task Add_and_update_top_level_optional_owned_collection_to_JSON(bool? value) + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var newEntity = new JsonEntityBasic + { + Id = 2, + Name = "NewEntity", + OwnedCollectionRoot = + value.HasValue + ? value.Value + ? new List { new() } + : new List() + : null + }; + + context.Add(newEntity); + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => + { + var newEntity = await context.JsonEntitiesBasic.SingleAsync(e => e.Id == 2); + + if (value.HasValue) + { + if (value.Value) + { + Assert.Single(newEntity.OwnedCollectionRoot!); + newEntity.OwnedCollectionRoot = null; + } + else + { + Assert.Empty(newEntity.OwnedCollectionRoot!); + newEntity.OwnedCollectionRoot.Add(new JsonOwnedRoot()); + } + } + else + { + Assert.Null(newEntity.OwnedCollectionRoot); + newEntity.OwnedCollectionRoot = new List(); + + // Because just setting the navigation to an empty collection currently doesn't mark it as modified. + context.Entry(newEntity).State = EntityState.Modified; + } + await context.SaveChangesAsync(); + + var saved = context.Database.SqlQueryRaw("select OwnedCollectionRoot from JsonEntitiesBasic where Id = 2").ToList(); + }, + async context => + { + var newEntity = await context.JsonEntitiesBasic.SingleAsync(e => e.Id == 2); + + if (value.HasValue) + { + if (value.Value) + { + Assert.Null(newEntity.OwnedCollectionRoot); + } + else + { + Assert.Single(newEntity.OwnedCollectionRoot!); + } + } + else + { + Assert.Empty(newEntity.OwnedCollectionRoot); + } + }); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + [InlineData(null)] + public virtual Task Add_and_update_nested_optional_owned_collection_to_JSON(bool? value) + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var newEntity = new JsonEntityBasic + { + Id = 2, + Name = "NewEntity", + OwnedReferenceRoot = new JsonOwnedRoot() + { + OwnedCollectionBranch = + value.HasValue + ? value.Value + ? new List { new() } + : new List() + : null + } + }; + + context.Add(newEntity); + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => + { + var newEntity = await context.JsonEntitiesBasic.SingleAsync(e => e.Id == 2); + + if (value.HasValue) + { + if (value.Value) + { + Assert.Single(newEntity.OwnedReferenceRoot.OwnedCollectionBranch!); + newEntity.OwnedReferenceRoot.OwnedCollectionBranch = null; + } + else + { + Assert.Empty(newEntity.OwnedReferenceRoot.OwnedCollectionBranch!); + newEntity.OwnedReferenceRoot.OwnedCollectionBranch.Add(new JsonOwnedBranch()); + } + } + else + { + Assert.Null(newEntity.OwnedReferenceRoot.OwnedCollectionBranch); + newEntity.OwnedReferenceRoot.OwnedCollectionBranch = new List(); + + // Because just setting the navigation to an empty collection currently doesn't mark it as modified. + context.Entry(newEntity).Reference(e => e.OwnedReferenceRoot).TargetEntry!.State = EntityState.Modified; + } + await context.SaveChangesAsync(); + }, + async context => + { + var newEntity = await context.JsonEntitiesBasic.SingleAsync(e => e.Id == 2); + + if (value.HasValue) + { + if (value.Value) + { + Assert.Null(newEntity.OwnedReferenceRoot.OwnedCollectionBranch); + } + else + { + Assert.Single(newEntity.OwnedReferenceRoot.OwnedCollectionBranch!); + } + } + else + { + Assert.Empty(newEntity.OwnedReferenceRoot.OwnedCollectionBranch); + } + }); + + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + [InlineData(null)] + public virtual Task Add_and_update_nested_optional_primitive_collection(bool? value) + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var newEntity = new JsonEntityAllTypes + { + Id = 7624, + TestDefaultStringCollection = Array.Empty(), + TestMaxLengthStringCollection = new List(), + TestBooleanCollection = Array.Empty(), + TestCharacterCollection = new ObservableCollection(), + TestDateTimeCollection = new List(), + TestDateTimeOffsetCollection = Array.Empty(), + TestDoubleCollection = Array.Empty(), + TestDecimalCollection = Array.Empty(), + TestGuidCollection = new List(), + TestInt16Collection = Array.Empty(), + TestInt32Collection = Array.Empty(), + TestInt64Collection = new List(), + TestSignedByteCollection = Array.Empty(), + TestSingleCollection = new List(), + TestTimeSpanCollection = Array.Empty(), + TestUnsignedInt16Collection = new List(), + TestUnsignedInt32Collection = Array.Empty(), + TestUnsignedInt64Collection = new ObservableCollection(), + TestNullableInt32Collection = new ObservableCollection(), + TestEnumCollection = Array.Empty(), + TestEnumWithIntConverterCollection = Array.Empty(), + TestNullableEnumCollection = new Collection(), + TestNullableEnumWithIntConverterCollection = new Collection(), + TestNullableEnumWithConverterThatHandlesNullsCollection = Array.Empty(), + Collection = new List + { + new() + { + TestDefaultStringCollection = Array.Empty(), + TestMaxLengthStringCollection = new List(), + TestBooleanCollection = Array.Empty(), + TestDateTimeCollection = new List(), + TestDateTimeOffsetCollection = Array.Empty(), + TestDoubleCollection = Array.Empty(), + TestDecimalCollection = Array.Empty(), + TestGuidCollection = new List(), + TestInt16Collection = Array.Empty(), + TestInt32Collection = Array.Empty(), + TestInt64Collection = new List(), + TestSignedByteCollection = Array.Empty(), + TestSingleCollection = new List(), + TestTimeSpanCollection = Array.Empty(), + TestDateOnlyCollection = Array.Empty(), + TestTimeOnlyCollection = Array.Empty(), + TestUnsignedInt16Collection = new List(), + TestUnsignedInt32Collection = Array.Empty(), + TestUnsignedInt64Collection = new ObservableCollection(), + TestNullableInt32Collection = new ObservableCollection(), + TestEnumCollection = Array.Empty(), + TestEnumWithIntConverterCollection = Array.Empty(), + TestNullableEnumCollection = new Collection(), + TestNullableEnumWithIntConverterCollection = new Collection(), + TestNullableEnumWithConverterThatHandlesNullsCollection = Array.Empty(), + TestCharacterCollection = + value.HasValue + ? value.Value + ? new ObservableCollection { 'A' } + : new ObservableCollection() + : null + } + } + }; + + context.Add(newEntity); + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => + { + var newEntity = await context.Set().SingleAsync(e => e.Id == 7624); + + if (value.HasValue) + { + if (value.Value) + { + Assert.Single(newEntity.Collection!.Single().TestCharacterCollection!); + newEntity.Collection!.Single().TestCharacterCollection = null; + } + else + { + Assert.Empty(newEntity.Collection!.Single().TestCharacterCollection!); + newEntity.Collection!.Single().TestCharacterCollection.Add('Z'); + } + } + else + { + Assert.Null(newEntity.Collection!.Single().TestCharacterCollection); + newEntity.Collection!.Single().TestCharacterCollection = new ObservableCollection(); + } + + await context.SaveChangesAsync(); + }, + async context => + { + var newEntity = await context.Set().SingleAsync(e => e.Id == 7624); + + if (value.HasValue) + { + if (value.Value) + { + Assert.Null(newEntity.Collection!.Single().TestCharacterCollection); + } + else + { + Assert.Single(newEntity.Collection!.Single().TestCharacterCollection!); + } + } + else + { + Assert.Empty(newEntity.Collection!.Single().TestCharacterCollection); + } + }); + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs index 37ef39ba623..3054f0bbe3a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs @@ -127,19 +127,20 @@ public override async Task Add_entity_with_json() await base.Add_entity_with_json(); AssertSql( - """ +""" @p0='{"Name":"RootName","Names":null,"Number":42,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":2,"Enums":null,"Fraction":42.42,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}}' (Nullable = false) (Size = 352) -@p1='2' -@p2=NULL (DbType = Int32) -@p3='NewEntity' (Size = 4000) +@p1='[]' (Nullable = false) (Size = 2) +@p2='2' +@p3=NULL (DbType = Int32) +@p4='NewEntity' (Size = 4000) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; -INSERT INTO [JsonEntitiesBasic] ([OwnedReferenceRoot], [Id], [EntityBasicId], [Name]) -VALUES (@p0, @p1, @p2, @p3); +INSERT INTO [JsonEntitiesBasic] ([OwnedReferenceRoot], [OwnedCollectionRoot], [Id], [EntityBasicId], [Name]) +VALUES (@p0, @p1, @p2, @p3, @p4); """, - // - """ +// +""" SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] FROM [JsonEntitiesBasic] AS [j] """); @@ -150,7 +151,7 @@ public override async Task Add_entity_with_json_null_navigations() await base.Add_entity_with_json_null_navigations(); AssertSql( - """ +""" @p0='{"Name":"RootName","Names":null,"Number":42,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":2,"Enums":null,"Fraction":42.42,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":null}}' (Nullable = false) (Size = 330) @p1='2' @p2=NULL (DbType = Int32) @@ -161,8 +162,8 @@ public override async Task Add_entity_with_json_null_navigations() INSERT INTO [JsonEntitiesBasic] ([OwnedReferenceRoot], [Id], [EntityBasicId], [Name]) VALUES (@p0, @p1, @p2, @p3); """, - // - """ +// +""" SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] FROM [JsonEntitiesBasic] AS [j] """); @@ -2140,6 +2141,344 @@ FROM [JsonEntitiesAllTypes] AS [j] """); } + public override async Task Add_and_update_top_level_optional_owned_collection_to_JSON(bool? value) + { + await base.Add_and_update_top_level_optional_owned_collection_to_JSON(value); + + switch (value) + { + case true: + AssertSql( + """ +@p0='[{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":null}]' (Nullable = false) (Size = 111) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [JsonEntitiesBasic] ([OwnedCollectionRoot], [Id], [EntityBasicId], [Name]) +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +""", + // + """ +@p0=NULL (Nullable = false) +@p1='2' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1; +""", + // + """ +select OwnedCollectionRoot from JsonEntitiesBasic where Id = 2 +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +"""); + break; + case false: + AssertSql( + """ +@p0='[]' (Nullable = false) (Size = 2) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [JsonEntitiesBasic] ([OwnedCollectionRoot], [Id], [EntityBasicId], [Name]) +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +""", + // + """ +@p0='[{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":null}]' (Nullable = false) (Size = 111) +@p1='2' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1; +""", + + + // + """ +select OwnedCollectionRoot from JsonEntitiesBasic where Id = 2 +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +"""); + break; + default: + AssertSql( + """ +@p0='2' +@p1=NULL (DbType = Int32) +@p2='NewEntity' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [JsonEntitiesBasic] ([Id], [EntityBasicId], [Name]) +VALUES (@p0, @p1, @p2); +""", + + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +""", + // + """ +@p0='[]' (Nullable = false) (Size = 2) +@p3='2' +@p1=NULL (DbType = Int32) +@p2='NewEntity' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = @p0, [EntityBasicId] = @p1, [Name] = @p2 +OUTPUT 1 +WHERE [Id] = @p3; +""", + // + """ +select OwnedCollectionRoot from JsonEntitiesBasic where Id = 2 +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +"""); + break; + } + } + + public override async Task Add_and_update_nested_optional_owned_collection_to_JSON(bool? value) + { + await base.Add_and_update_nested_optional_owned_collection_to_JSON(value); + + switch (value) + { + case true: + AssertSql( + """ +@p0='{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":[{"Date":"0001-01-01T00:00:00","Enum":0,"Enums":null,"Fraction":0,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":null,"OwnedReferenceLeaf":null}],"OwnedReferenceBranch":null}' (Nullable = false) (Size = 266) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [JsonEntitiesBasic] ([OwnedReferenceRoot], [Id], [EntityBasicId], [Name]) +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +""", + // + """ +@p0=NULL (Nullable = false) +@p1='2' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = JSON_MODIFY([OwnedReferenceRoot], 'strict $.OwnedCollectionBranch', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1; +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +"""); + break; + case false: + AssertSql( + """ +@p0='{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":null}' (Nullable = false) (Size = 107) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [JsonEntitiesBasic] ([OwnedReferenceRoot], [Id], [EntityBasicId], [Name]) +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +""", + // + """ +@p0='[{"Date":"0001-01-01T00:00:00","Enum":0,"Enums":null,"Fraction":0,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":null,"OwnedReferenceLeaf":null}]' (Nullable = false) (Size = 161) +@p1='2' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = JSON_MODIFY([OwnedReferenceRoot], 'strict $.OwnedCollectionBranch', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1; +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +"""); + break; + default: + AssertSql( + """ +@p0='{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":null}' (Nullable = false) (Size = 109) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [JsonEntitiesBasic] ([OwnedReferenceRoot], [Id], [EntityBasicId], [Name]) +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +""", + // + """ +@p0='{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":null}' (Nullable = false) (Size = 107) +@p1='2' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1; +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 2 +"""); + break; + } + } + + public override async Task Add_and_update_nested_optional_primitive_collection(bool? value) + { + await base.Add_and_update_nested_optional_primitive_collection(value); + + string characterCollection = value switch + { + true => "[\"A\"]", + false => "[]", + _ => "null" + }; + + string parameterSize = value switch + { + true => "1537", + false => "1534", + _ => "1536" + }; + + string updateParameter = value switch + { + true => "NULL", + false => "'[\"Z\"]'", + _ => "'[]'" + }; + + AssertSql( + @"@p0='[{""TestBoolean"":false,""TestBooleanCollection"":[],""TestByte"":0,""TestByteCollection"":null,""TestCharacter"":""\u0000"",""TestCharacterCollection"":" + characterCollection + @",""TestDateOnly"":""0001-01-01"",""TestDateOnlyCollection"":[],""TestDateTime"":""0001-01-01T00:00:00"",""TestDateTimeCollection"":[],""TestDateTimeOffset"":""0001-01-01T00:00:00+00:00"",""TestDateTimeOffsetCollection"":[],""TestDecimal"":0,""TestDecimalCollection"":[],""TestDefaultString"":null,""TestDefaultStringCollection"":[],""TestDouble"":0,""TestDoubleCollection"":[],""TestEnum"":0,""TestEnumCollection"":[],""TestEnumWithIntConverter"":0,""TestEnumWithIntConverterCollection"":[],""TestGuid"":""00000000-0000-0000-0000-000000000000"",""TestGuidCollection"":[],""TestInt16"":0,""TestInt16Collection"":[],""TestInt32"":0,""TestInt32Collection"":[],""TestInt64"":0,""TestInt64Collection"":[],""TestMaxLengthString"":null,""TestMaxLengthStringCollection"":[],""TestNullableEnum"":null,""TestNullableEnumCollection"":[],""TestNullableEnumWithConverterThatHandlesNulls"":null,""TestNullableEnumWithConverterThatHandlesNullsCollection"":[],""TestNullableEnumWithIntConverter"":null,""TestNullableEnumWithIntConverterCollection"":[],""TestNullableInt32"":null,""TestNullableInt32Collection"":[],""TestSignedByte"":0,""TestSignedByteCollection"":[],""TestSingle"":0,""TestSingleCollection"":[],""TestTimeOnly"":""00:00:00.0000000"",""TestTimeOnlyCollection"":[],""TestTimeSpan"":""0:00:00"",""TestTimeSpanCollection"":[],""TestUnsignedInt16"":0,""TestUnsignedInt16Collection"":[],""TestUnsignedInt32"":0,""TestUnsignedInt32Collection"":[],""TestUnsignedInt64"":0,""TestUnsignedInt64Collection"":[]}]' (Nullable = false) (Size = " + parameterSize + @") +@p1='7624' +@p2='[]' (Size = 4000) +@p3=NULL (Size = 8000) (DbType = Binary) +@p4='[]' (Size = 4000) +@p5='[]' (Size = 4000) +@p6='[]' (Size = 4000) +@p7='[]' (Size = 4000) +@p8='[]' (Size = 4000) +@p9='[]' (Size = 4000) +@p10='[]' (Size = 4000) +@p11='[]' (Size = 4000) +@p12='[]' (Nullable = false) (Size = 4000) +@p13='[]' (Size = 4000) +@p14='[]' (Size = 4000) +@p15='[]' (Size = 4000) +@p16='[]' (Size = 4000) +@p17='[]' (Size = 4000) +@p18='[]' (Size = 4000) +@p19='[]' (Size = 4000) +@p20='[]' (Size = 4000) +@p21='[]' (Size = 4000) +@p22='[]' (Size = 4000) +@p23='[]' (Size = 4000) +@p24='[]' (Size = 4000) +@p25='[]' (Size = 4000) +@p26='[]' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [JsonEntitiesAllTypes] ([Collection], [Id], [TestBooleanCollection], [TestByteCollection], [TestCharacterCollection], [TestDateTimeCollection], [TestDateTimeOffsetCollection], [TestDecimalCollection], [TestDefaultStringCollection], [TestDoubleCollection], [TestEnumCollection], [TestEnumWithIntConverterCollection], [TestGuidCollection], [TestInt16Collection], [TestInt32Collection], [TestInt64Collection], [TestMaxLengthStringCollection], [TestNullableEnumCollection], [TestNullableEnumWithConverterThatHandlesNullsCollection], [TestNullableEnumWithIntConverterCollection], [TestNullableInt32Collection], [TestSignedByteCollection], [TestSingleCollection], [TestTimeSpanCollection], [TestUnsignedInt16Collection], [TestUnsignedInt32Collection], [TestUnsignedInt64Collection]) +VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, @p16, @p17, @p18, @p19, @p20, @p21, @p22, @p23, @p24, @p25, @p26);", + // + """ +SELECT TOP(2) [j].[Id], [j].[TestBooleanCollection], [j].[TestByteCollection], [j].[TestCharacterCollection], [j].[TestDateTimeCollection], [j].[TestDateTimeOffsetCollection], [j].[TestDecimalCollection], [j].[TestDefaultStringCollection], [j].[TestDoubleCollection], [j].[TestEnumCollection], [j].[TestEnumWithIntConverterCollection], [j].[TestGuidCollection], [j].[TestInt16Collection], [j].[TestInt32Collection], [j].[TestInt64Collection], [j].[TestMaxLengthStringCollection], [j].[TestNullableEnumCollection], [j].[TestNullableEnumWithConverterThatHandlesNullsCollection], [j].[TestNullableEnumWithIntConverterCollection], [j].[TestNullableInt32Collection], [j].[TestSignedByteCollection], [j].[TestSingleCollection], [j].[TestTimeSpanCollection], [j].[TestUnsignedInt16Collection], [j].[TestUnsignedInt32Collection], [j].[TestUnsignedInt64Collection], [j].[Collection], [j].[Reference] +FROM [JsonEntitiesAllTypes] AS [j] +WHERE [j].[Id] = 7624 +""", + // + +"@p0=" + updateParameter + @" (Nullable = false) (Size = 4000) +@p1='7624' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesAllTypes] SET [Collection] = JSON_MODIFY([Collection], 'strict $[0].TestCharacterCollection', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1;", + // + """ +SELECT TOP(2) [j].[Id], [j].[TestBooleanCollection], [j].[TestByteCollection], [j].[TestCharacterCollection], [j].[TestDateTimeCollection], [j].[TestDateTimeOffsetCollection], [j].[TestDecimalCollection], [j].[TestDefaultStringCollection], [j].[TestDoubleCollection], [j].[TestEnumCollection], [j].[TestEnumWithIntConverterCollection], [j].[TestGuidCollection], [j].[TestInt16Collection], [j].[TestInt32Collection], [j].[TestInt64Collection], [j].[TestMaxLengthStringCollection], [j].[TestNullableEnumCollection], [j].[TestNullableEnumWithConverterThatHandlesNullsCollection], [j].[TestNullableEnumWithIntConverterCollection], [j].[TestNullableInt32Collection], [j].[TestSignedByteCollection], [j].[TestSingleCollection], [j].[TestTimeSpanCollection], [j].[TestUnsignedInt16Collection], [j].[TestUnsignedInt32Collection], [j].[TestUnsignedInt64Collection], [j].[Collection], [j].[Reference] +FROM [JsonEntitiesAllTypes] AS [j] +WHERE [j].[Id] = 7624 +"""); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Update/JsonUpdateSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Update/JsonUpdateSqliteTest.cs index d940acd9c54..7534891a03d 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Update/JsonUpdateSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Update/JsonUpdateSqliteTest.cs @@ -122,17 +122,18 @@ public override async Task Add_entity_with_json() await base.Add_entity_with_json(); AssertSql( - """ +""" @p0='{"Name":"RootName","Names":null,"Number":42,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2010-10-10 00:00:00","Enum":2,"Enums":null,"Fraction":"42.42","NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}}' (Nullable = false) (Size = 354) -@p1='2' -@p2=NULL (DbType = Int32) -@p3='NewEntity' (Size = 9) +@p1='[]' (Nullable = false) (Size = 2) +@p2='2' +@p3=NULL (DbType = Int32) +@p4='NewEntity' (Size = 9) -INSERT INTO "JsonEntitiesBasic" ("OwnedReferenceRoot", "Id", "EntityBasicId", "Name") -VALUES (@p0, @p1, @p2, @p3); +INSERT INTO "JsonEntitiesBasic" ("OwnedReferenceRoot", "OwnedCollectionRoot", "Id", "EntityBasicId", "Name") +VALUES (@p0, @p1, @p2, @p3, @p4); """, // - """ +""" SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" FROM "JsonEntitiesBasic" AS "j" """); @@ -2038,6 +2039,327 @@ LIMIT 2 """); } + public override async Task Add_and_update_top_level_optional_owned_collection_to_JSON(bool? value) + { + await base.Add_and_update_top_level_optional_owned_collection_to_JSON(value); + + switch (value) + { + case true: + AssertSql( + """ +@p0='[{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":null}]' (Nullable = false) (Size = 111) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 9) + +INSERT INTO "JsonEntitiesBasic" ("OwnedCollectionRoot", "Id", "EntityBasicId", "Name") +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +""", + // + """ +@p0=NULL (Nullable = false) +@p1='2' + +UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = @p0 +WHERE "Id" = @p1 +RETURNING 1; +""", + // + """ +select OwnedCollectionRoot from JsonEntitiesBasic where Id = 2 +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +"""); + break; + case false: + AssertSql( + """ +@p0='[]' (Nullable = false) (Size = 2) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 9) + +INSERT INTO "JsonEntitiesBasic" ("OwnedCollectionRoot", "Id", "EntityBasicId", "Name") +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +""", + // + """ +@p0='[{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":null}]' (Nullable = false) (Size = 111) +@p1='2' + +UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = @p0 +WHERE "Id" = @p1 +RETURNING 1; +""", + // + """ +select OwnedCollectionRoot from JsonEntitiesBasic where Id = 2 +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +"""); + break; + default: + AssertSql( + """ +@p0='2' +@p1=NULL (DbType = Int32) +@p2='NewEntity' (Size = 9) + +INSERT INTO "JsonEntitiesBasic" ("Id", "EntityBasicId", "Name") +VALUES (@p0, @p1, @p2); +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +""", + // + """ +@p0='[]' (Nullable = false) (Size = 2) +@p3='2' +@p1=NULL (DbType = Int32) +@p2='NewEntity' (Size = 9) + +UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = @p0, "EntityBasicId" = @p1, "Name" = @p2 +WHERE "Id" = @p3 +RETURNING 1; +""", + // + """ +select OwnedCollectionRoot from JsonEntitiesBasic where Id = 2 +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +"""); + break; + } + } + + public override async Task Add_and_update_nested_optional_owned_collection_to_JSON(bool? value) + { + await base.Add_and_update_nested_optional_owned_collection_to_JSON(value); + + switch (value) + { + case true: + AssertSql( + """ +@p0='{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":[{"Date":"0001-01-01 00:00:00","Enum":0,"Enums":null,"Fraction":"0.0","NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":null,"OwnedReferenceLeaf":null}],"OwnedReferenceBranch":null}' (Nullable = false) (Size = 270) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 9) + +INSERT INTO "JsonEntitiesBasic" ("OwnedReferenceRoot", "Id", "EntityBasicId", "Name") +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +""", + // + """ +@p0=NULL (Nullable = false) +@p1='2' + +UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = json_set("OwnedReferenceRoot", '$.OwnedCollectionBranch', json(@p0)) +WHERE "Id" = @p1 +RETURNING 1; +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +"""); + break; + case false: + AssertSql( + """ +@p0='{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":null}' (Nullable = false) (Size = 107) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 9) + +INSERT INTO "JsonEntitiesBasic" ("OwnedReferenceRoot", "Id", "EntityBasicId", "Name") +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +""", + // + """ +@p0='[{"Date":"0001-01-01 00:00:00","Enum":0,"Enums":null,"Fraction":"0.0","NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":null,"OwnedReferenceLeaf":null}]' (Nullable = false) (Size = 165) +@p1='2' + +UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = json_set("OwnedReferenceRoot", '$.OwnedCollectionBranch', json(@p0)) +WHERE "Id" = @p1 +RETURNING 1; +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +"""); + break; + default: + AssertSql( + """ +@p0='{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":null}' (Nullable = false) (Size = 109) +@p1='2' +@p2=NULL (DbType = Int32) +@p3='NewEntity' (Size = 9) + +INSERT INTO "JsonEntitiesBasic" ("OwnedReferenceRoot", "Id", "EntityBasicId", "Name") +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +""", + // + """ +@p0='{"Name":null,"Names":null,"Number":0,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":null}' (Nullable = false) (Size = 107) +@p1='2' + +UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = @p0 +WHERE "Id" = @p1 +RETURNING 1; +""", + // + """ +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE "j"."Id" = 2 +LIMIT 2 +"""); + break; + } + } + + public override async Task Add_and_update_nested_optional_primitive_collection(bool? value) + { + await base.Add_and_update_nested_optional_primitive_collection(value); + + string characterCollection = value switch + { + true => "[\"A\"]", + false => "[]", + _ => "null" + }; + + string parameterSize = value switch + { + true => "1541", + false => "1538", + _ => "1540" + }; + + string updateParameter = value switch + { + true => "NULL (Nullable = false)", + false => "'[\"Z\"]' (Nullable = false) (Size = 5)", + _ => "'[]' (Nullable = false) (Size = 2)" + }; + + AssertSql( + @"@p0='[{""TestBoolean"":false,""TestBooleanCollection"":[],""TestByte"":0,""TestByteCollection"":null,""TestCharacter"":""\u0000"",""TestCharacterCollection"":" + characterCollection + @",""TestDateOnly"":""0001-01-01"",""TestDateOnlyCollection"":[],""TestDateTime"":""0001-01-01 00:00:00"",""TestDateTimeCollection"":[],""TestDateTimeOffset"":""0001-01-01 00:00:00+00:00"",""TestDateTimeOffsetCollection"":[],""TestDecimal"":""0.0"",""TestDecimalCollection"":[],""TestDefaultString"":null,""TestDefaultStringCollection"":[],""TestDouble"":0,""TestDoubleCollection"":[],""TestEnum"":0,""TestEnumCollection"":[],""TestEnumWithIntConverter"":0,""TestEnumWithIntConverterCollection"":[],""TestGuid"":""00000000-0000-0000-0000-000000000000"",""TestGuidCollection"":[],""TestInt16"":0,""TestInt16Collection"":[],""TestInt32"":0,""TestInt32Collection"":[],""TestInt64"":0,""TestInt64Collection"":[],""TestMaxLengthString"":null,""TestMaxLengthStringCollection"":[],""TestNullableEnum"":null,""TestNullableEnumCollection"":[],""TestNullableEnumWithConverterThatHandlesNulls"":null,""TestNullableEnumWithConverterThatHandlesNullsCollection"":[],""TestNullableEnumWithIntConverter"":null,""TestNullableEnumWithIntConverterCollection"":[],""TestNullableInt32"":null,""TestNullableInt32Collection"":[],""TestSignedByte"":0,""TestSignedByteCollection"":[],""TestSingle"":0,""TestSingleCollection"":[],""TestTimeOnly"":""00:00:00.0000000"",""TestTimeOnlyCollection"":[],""TestTimeSpan"":""0:00:00"",""TestTimeSpanCollection"":[],""TestUnsignedInt16"":0,""TestUnsignedInt16Collection"":[],""TestUnsignedInt32"":0,""TestUnsignedInt32Collection"":[],""TestUnsignedInt64"":0,""TestUnsignedInt64Collection"":[]}]' (Nullable = false) (Size = " + parameterSize + @") +@p1='7624' +@p2='[]' (Size = 2) +@p3=NULL (DbType = Binary) +@p4='[]' (Size = 2) +@p5='[]' (Size = 2) +@p6='[]' (Size = 2) +@p7='[]' (Size = 2) +@p8='[]' (Size = 2) +@p9='[]' (Size = 2) +@p10='[]' (Size = 2) +@p11='[]' (Size = 2) +@p12='[]' (Nullable = false) (Size = 2) +@p13='[]' (Size = 2) +@p14='[]' (Size = 2) +@p15='[]' (Size = 2) +@p16='[]' (Size = 2) +@p17='[]' (Size = 2) +@p18='[]' (Size = 2) +@p19='[]' (Size = 2) +@p20='[]' (Size = 2) +@p21='[]' (Size = 2) +@p22='[]' (Size = 2) +@p23='[]' (Size = 2) +@p24='[]' (Size = 2) +@p25='[]' (Size = 2) +@p26='[]' (Size = 2) + +INSERT INTO ""JsonEntitiesAllTypes"" (""Collection"", ""Id"", ""TestBooleanCollection"", ""TestByteCollection"", ""TestCharacterCollection"", ""TestDateTimeCollection"", ""TestDateTimeOffsetCollection"", ""TestDecimalCollection"", ""TestDefaultStringCollection"", ""TestDoubleCollection"", ""TestEnumCollection"", ""TestEnumWithIntConverterCollection"", ""TestGuidCollection"", ""TestInt16Collection"", ""TestInt32Collection"", ""TestInt64Collection"", ""TestMaxLengthStringCollection"", ""TestNullableEnumCollection"", ""TestNullableEnumWithConverterThatHandlesNullsCollection"", ""TestNullableEnumWithIntConverterCollection"", ""TestNullableInt32Collection"", ""TestSignedByteCollection"", ""TestSingleCollection"", ""TestTimeSpanCollection"", ""TestUnsignedInt16Collection"", ""TestUnsignedInt32Collection"", ""TestUnsignedInt64Collection"") +VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, @p16, @p17, @p18, @p19, @p20, @p21, @p22, @p23, @p24, @p25, @p26);", + // + """ +SELECT "j"."Id", "j"."TestBooleanCollection", "j"."TestByteCollection", "j"."TestCharacterCollection", "j"."TestDateTimeCollection", "j"."TestDateTimeOffsetCollection", "j"."TestDecimalCollection", "j"."TestDefaultStringCollection", "j"."TestDoubleCollection", "j"."TestEnumCollection", "j"."TestEnumWithIntConverterCollection", "j"."TestGuidCollection", "j"."TestInt16Collection", "j"."TestInt32Collection", "j"."TestInt64Collection", "j"."TestMaxLengthStringCollection", "j"."TestNullableEnumCollection", "j"."TestNullableEnumWithConverterThatHandlesNullsCollection", "j"."TestNullableEnumWithIntConverterCollection", "j"."TestNullableInt32Collection", "j"."TestSignedByteCollection", "j"."TestSingleCollection", "j"."TestTimeSpanCollection", "j"."TestUnsignedInt16Collection", "j"."TestUnsignedInt32Collection", "j"."TestUnsignedInt64Collection", "j"."Collection", "j"."Reference" +FROM "JsonEntitiesAllTypes" AS "j" +WHERE "j"."Id" = 7624 +LIMIT 2 +""", +// + +"@p0=" + updateParameter + @" +@p1='7624' + +UPDATE ""JsonEntitiesAllTypes"" SET ""Collection"" = json_set(""Collection"", '$[0].TestCharacterCollection', json(@p0)) +WHERE ""Id"" = @p1 +RETURNING 1;", + // + """ +SELECT "j"."Id", "j"."TestBooleanCollection", "j"."TestByteCollection", "j"."TestCharacterCollection", "j"."TestDateTimeCollection", "j"."TestDateTimeOffsetCollection", "j"."TestDecimalCollection", "j"."TestDefaultStringCollection", "j"."TestDoubleCollection", "j"."TestEnumCollection", "j"."TestEnumWithIntConverterCollection", "j"."TestGuidCollection", "j"."TestInt16Collection", "j"."TestInt32Collection", "j"."TestInt64Collection", "j"."TestMaxLengthStringCollection", "j"."TestNullableEnumCollection", "j"."TestNullableEnumWithConverterThatHandlesNullsCollection", "j"."TestNullableEnumWithIntConverterCollection", "j"."TestNullableInt32Collection", "j"."TestSignedByteCollection", "j"."TestSingleCollection", "j"."TestTimeSpanCollection", "j"."TestUnsignedInt16Collection", "j"."TestUnsignedInt32Collection", "j"."TestUnsignedInt64Collection", "j"."Collection", "j"."Reference" +FROM "JsonEntitiesAllTypes" AS "j" +WHERE "j"."Id" = 7624 +LIMIT 2 +"""); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear();