From 5d9fd6720a049f80c7944522d3da91406bd21cd2 Mon Sep 17 00:00:00 2001 From: maumar Date: Mon, 22 Aug 2022 00:10:14 -0700 Subject: [PATCH] Fix to #28595 - Use partial updates for JSON Currently, whenever we update/add/delete part of aggregate mapped to JSON column, we were replacing the entire structure. Improvement is to use JSON_MODIFY which can alter just a portion of JSON structure, give a JSON path. We analyze the entries that are being edited and for each JSON column we find the common denominator that needs to be replaced to accommodate all the requested changes Note: Whenever we add/remove element from a collection we need to replace the entire collection in order to populate ordinal key values properly. Also, modifying a single property is not yet supported - the smallest fragment that will be replaced is one that represents an entity. Fixes #28595 --- .../Update/ColumnModification.cs | 6 +- .../Update/ColumnModificationParameters.cs | 60 ++- .../Update/IColumnModification.cs | 5 + .../Update/ModificationCommand.cs | 224 +++++++++-- .../Update/UpdateSqlGenerator.cs | 35 +- .../Internal/SqlServerUpdateSqlGenerator.cs | 27 ++ .../Update/JsonUpdateFixtureBase.cs | 112 ++++++ .../Update/JsonUpdateTestBase.cs | 230 ++++++----- ...eTest.cs => JsonUpdateSqlServerFixture.cs} | 2 +- .../Update/JsonUpdateSqlServerTest.cs | 379 ++++++++++++++++++ .../SqliteComplianceTest.cs | 2 +- 11 files changed, 933 insertions(+), 149 deletions(-) create mode 100644 test/EFCore.Relational.Specification.Tests/Update/JsonUpdateFixtureBase.cs rename test/EFCore.SqlServer.FunctionalTests/Update/{SqlServerJsonUpdateTest.cs => JsonUpdateSqlServerFixture.cs} (82%) create mode 100644 test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs diff --git a/src/EFCore.Relational/Update/ColumnModification.cs b/src/EFCore.Relational/Update/ColumnModification.cs index 3c5d5ae2b6f..1fb54784a11 100644 --- a/src/EFCore.Relational/Update/ColumnModification.cs +++ b/src/EFCore.Relational/Update/ColumnModification.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Data; - namespace Microsoft.EntityFrameworkCore.Update; /// @@ -54,6 +52,7 @@ public ColumnModification(in ColumnModificationParameters columnModificationPara IsNullable = columnModificationParameters.IsNullable; _generateParameterName = columnModificationParameters.GenerateParameterName; Entry = columnModificationParameters.Entry; + JsonPath = columnModificationParameters.JsonPath; UseParameter = _generateParameterName != null; } @@ -174,6 +173,9 @@ public virtual object? Value } } + /// + public virtual string? JsonPath { get; } + /// public virtual void AddSharedColumnModification(IColumnModification modification) { diff --git a/src/EFCore.Relational/Update/ColumnModificationParameters.cs b/src/EFCore.Relational/Update/ColumnModificationParameters.cs index f58d11e7287..7ddd7ce9150 100644 --- a/src/EFCore.Relational/Update/ColumnModificationParameters.cs +++ b/src/EFCore.Relational/Update/ColumnModificationParameters.cs @@ -76,7 +76,7 @@ public readonly record struct ColumnModificationParameters /// Indicates whether the column is part of a primary or alternate key. /// public bool IsKey { get; init; } - + /// /// The column. /// @@ -92,6 +92,11 @@ public readonly record struct ColumnModificationParameters /// public string? ColumnType { get; init; } + /// + /// In case of JSON column modification, the JSON path leading to the JSON element that needs to be updated. + /// + public string? JsonPath { get; init; } + /// /// Creates a new instance. /// @@ -137,8 +142,9 @@ public ColumnModificationParameters( GenerateParameterName = null; Entry = null; + JsonPath = null; } - + /// /// Creates a new instance. /// @@ -182,6 +188,7 @@ public ColumnModificationParameters( GenerateParameterName = null; Entry = null; + JsonPath = null; } /// @@ -225,5 +232,54 @@ public ColumnModificationParameters( GenerateParameterName = generateParameterName; Entry = entry; + JsonPath = null; + } + + /// + /// Creates a new instance. + /// + /// The name of the column. + /// The original value of the property mapped to this column. + /// The current value of the property mapped to this column. + /// The JSON path leading to the JSON element that needs to be updated. + /// The database type of the column. + /// The relational type mapping to be used for the command parameter. + /// Indicates whether a value must be read from the database for the column. + /// Indicates whether a value must be written to the database for the column. + /// Indicates whether the column part of a primary or alternate key. + /// Indicates whether the column is used in the WHERE clause when updating. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + /// A value indicating whether the value could be null. + public ColumnModificationParameters( + string columnName, + object? originalValue, + object? value, + string? columnType, + RelationalTypeMapping? typeMapping, + string jsonPath, + bool read, + bool write, + bool key, + bool condition, + bool sensitiveLoggingEnabled, + bool? isNullable = null) + { + Column = null; + ColumnName = columnName; + OriginalValue = originalValue; + Value = value; + Property = null; + ColumnType = columnType; + TypeMapping = typeMapping; + JsonPath = jsonPath; + IsRead = read; + IsWrite = write; + IsKey = key; + IsCondition = condition; + SensitiveLoggingEnabled = sensitiveLoggingEnabled; + IsNullable = isNullable; + + GenerateParameterName = null; + Entry = null; } } diff --git a/src/EFCore.Relational/Update/IColumnModification.cs b/src/EFCore.Relational/Update/IColumnModification.cs index b449dec4abd..5e8c3510bd9 100644 --- a/src/EFCore.Relational/Update/IColumnModification.cs +++ b/src/EFCore.Relational/Update/IColumnModification.cs @@ -122,6 +122,11 @@ public interface IColumnModification /// public object? Value { get; set; } + /// + /// In case of JSON column modification, the JSON path leading to the JSON element that needs to be updated. + /// + public string? JsonPath { get; } + /// /// Adds a modification affecting the same database value. /// diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index 69744ad6b91..c04e8cfb38b 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -6,6 +6,7 @@ using System.Text.Json.Nodes; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using IColumnMapping = Microsoft.EntityFrameworkCore.Metadata.IColumnMapping; using ITableMapping = Microsoft.EntityFrameworkCore.Metadata.ITableMapping; @@ -250,6 +251,26 @@ public virtual IColumnModification AddColumnModification(in ColumnModificationPa protected virtual IColumnModification CreateColumnModification(in ColumnModificationParameters columnModificationParameters) => new ColumnModification(columnModificationParameters); + private sealed record JsonPartialUpdatePathEntry + { + public JsonPartialUpdatePathEntry( + string propertyName, + int? ordinal, + IUpdateEntry parentEntry, + INavigation navigation) + { + PropertyName = propertyName; + Ordinal = ordinal; + ParentEntry = parentEntry; + Navigation = navigation; + } + + public string PropertyName { get; } + public int? Ordinal { get; } + public IUpdateEntry ParentEntry { get; } + public INavigation Navigation { get; } + } + private List GenerateColumnModifications() { var state = EntityState; @@ -257,6 +278,7 @@ private List GenerateColumnModifications() var updating = state == EntityState.Modified; var columnModifications = new List(); Dictionary? sharedTableColumnMap = null; + var jsonEntry = false; if (_entries.Count > 1 || (_entries.Count == 1 && _entries[0].SharedIdentityEntry != null)) @@ -290,74 +312,107 @@ private List GenerateColumnModifications() } InitializeSharedColumns(entry, tableMapping, updating, sharedTableColumnMap); + + if (!jsonEntry && entry.EntityType.IsMappedToJson()) + { + jsonEntry = true; + } } } - var processedJsonNavigations = new List(); - foreach (var entry in _entries) + if (jsonEntry) { - if (entry.EntityType.IsMappedToJson()) + var jsonColumnsUpdateMap = new Dictionary>(); + var processedEntries = new List(); + foreach (var entry in _entries.Where(e => e.EntityType.IsMappedToJson())) { - // for JSON entry, traverse to the entry for root JSON entity - // and build entire JSON structure based on it - // this will be the column modification command - var jsonColumnName = entry.EntityType.GetContainerColumnName()!; - var jsonColumnTypeMapping = entry.EntityType.GetContainerColumnTypeMapping()!; - - var currentEntry = entry; - var currentOwnership = currentEntry.EntityType.FindOwnership()!; - while (currentEntry.EntityType.IsMappedToJson()) + var modifiedMembers = entry.ToEntityEntry().Members.Where(m => m is not NavigationEntry && m.IsModified).ToList(); + var jsonColumn = entry.EntityType.GetContainerColumnName()!; + var jsonPartialUpdateInfo = FindJsonPartialUpdateInfo(entry, processedEntries); + processedEntries.Add(entry); + + if (jsonPartialUpdateInfo == null) { - currentOwnership = currentEntry.EntityType.FindOwnership()!; -#pragma warning disable EF1001 // Internal EF Core API usage. - currentEntry = ((InternalEntityEntry)currentEntry).StateManager.FindPrincipal((InternalEntityEntry)currentEntry, currentOwnership)!; -#pragma warning restore EF1001 // Internal EF Core API usage. + // this entry is a subtree of an entry that we already processed + // so we already need to update the parent - no need to have extra entry for the subtree + continue; } - var navigation = currentOwnership.GetNavigation(pointsToPrincipal: false)!; - if (processedJsonNavigations.Contains(navigation)) + if (jsonColumnsUpdateMap.TryGetValue(jsonColumn, out var currentJsonPartialUpdateInfo)) { - continue; + jsonPartialUpdateInfo = FindCommonJsonPartialUpdateInfo( + currentJsonPartialUpdateInfo, + jsonPartialUpdateInfo); } - processedJsonNavigations.Add(navigation); + jsonColumnsUpdateMap[jsonColumn] = jsonPartialUpdateInfo; + } + + foreach (var (jsonColumnName, updatePath) in jsonColumnsUpdateMap) + { + var finalUpdatePathElement = updatePath.Last(); + var navigation = finalUpdatePathElement.Navigation; + + var jsonColumnTypeMapping = navigation.TargetEntityType.GetContainerColumnTypeMapping()!; + var navigationValue = finalUpdatePathElement.ParentEntry.GetCurrentValue(navigation); - // parent entity got deleted, no need to do any json-specific processing - if (currentEntry.EntityState == EntityState.Deleted) + var json = default(JsonNode?); + if (finalUpdatePathElement.Ordinal != null && navigationValue != null) { - continue; - } + int i = 0; + foreach (var navigationValueElement in (IEnumerable)navigationValue) + { + if (i == finalUpdatePathElement.Ordinal) + { + json = CreateJson( + navigationValueElement, + finalUpdatePathElement.ParentEntry, + navigation.TargetEntityType, + ordinal: null, + isCollection: false); + + break; + } - var navigationValue = currentEntry.GetCurrentValue(navigation)!; + i++; + } + } + else + { + json = CreateJson( + navigationValue, + finalUpdatePathElement.ParentEntry, + navigation.TargetEntityType, + ordinal: null, + isCollection: navigation.IsCollection); + } - var json = CreateJson( - navigationValue, - currentEntry, - currentOwnership.DeclaringEntityType, - ordinal: null, - isCollection: navigation.IsCollection); + var jsonPathString = string.Join( + ".", updatePath.Select(x => x.PropertyName + (x.Ordinal != null ? "[" + x.Ordinal + "]" : ""))); var columnModificationParameters = new ColumnModificationParameters( - jsonColumnName, - originalValue: null, - value: json?.ToJsonString(), - property: null, - columnType: jsonColumnTypeMapping.StoreType, - jsonColumnTypeMapping, - read: false, - write: true, - key: false, - condition: false, - _sensitiveLoggingEnabled) + jsonColumnName, + originalValue: null, + value: json?.ToJsonString(), + columnType: jsonColumnTypeMapping.StoreType, + jsonColumnTypeMapping, + jsonPath: jsonPathString, + read: false, + write: true, + key: false, + condition: false, + _sensitiveLoggingEnabled) { GenerateParameterName = _generateParameterName, }; columnModifications.Add(new ColumnModification(columnModificationParameters)); - - continue; } + } + var processedJsonNavigations = new List(); + foreach (var entry in _entries.Where(x => !x.EntityType.IsMappedToJson())) + { var nonMainEntry = !_mainEntryAdded || entry != _entries[0]; IEnumerable columnMappings; @@ -526,6 +581,87 @@ entry.EntityState is EntityState.Modified or EntityState.Added } return columnModifications; + + static List? FindJsonPartialUpdateInfo(IUpdateEntry entry, List processedEntries) + { + var result = new List(); + var currentEntry = entry; + var currentOwnership = currentEntry.EntityType.FindOwnership()!; + + while (currentEntry.EntityType.IsMappedToJson()) + { + var jsonPropertyName = currentEntry.EntityType.GetJsonPropertyName()!; + currentOwnership = currentEntry.EntityType.FindOwnership()!; + var previousEntry = currentEntry; +#pragma warning disable EF1001 // Internal EF Core API usage. + currentEntry = ((InternalEntityEntry)currentEntry).StateManager.FindPrincipal((InternalEntityEntry)currentEntry, currentOwnership)!; +#pragma warning restore EF1001 // Internal EF Core API usage. + + if (processedEntries.Contains(currentEntry)) + { + return null; + } + + var ordinal = default(int?); + if (!currentOwnership.IsUnique + && previousEntry.EntityState != EntityState.Added + && previousEntry.EntityState != EntityState.Deleted) + { + var ordinalProperty = previousEntry.EntityType.FindPrimaryKey()!.Properties.Last(); + ordinal = (int)previousEntry.GetCurrentProviderValue(ordinalProperty)! - 1; + } + + var pathEntry = new JsonPartialUpdatePathEntry( + currentOwnership.PrincipalEntityType.IsMappedToJson() ? jsonPropertyName : "$", + ordinal, + currentEntry, + currentOwnership.GetNavigation(pointsToPrincipal: false)!); + + result.Insert(0, pathEntry); + } + + // parent entity got deleted, no need to do any json-specific processing + if (currentEntry.EntityState == EntityState.Deleted) + { + return null; + } + + return result; + } + + static List FindCommonJsonPartialUpdateInfo( + List first, + List second) + { + var result = new List(); + for (var i = 0; i < Math.Min(first.Count, second.Count); i++) + { + if (first[i].PropertyName == second[i].PropertyName) + { + if (first[i].Ordinal == second[i].Ordinal) + { + result.Add(first[i]); + continue; + } + else + { + var common = new JsonPartialUpdatePathEntry( + first[i].PropertyName, + null, + first[i].ParentEntry, + first[i].Navigation); + + result.Add(common); + } + + break; + } + } + + Debug.Assert(result.Count > 0, "Common denominator should always have at least one node - the root."); + + return result; + } } private JsonNode? CreateJson(object? navigationValue, IUpdateEntry parentEntry, IEntityType entityType, int? ordinal, bool isCollection) diff --git a/src/EFCore.Relational/Update/UpdateSqlGenerator.cs b/src/EFCore.Relational/Update/UpdateSqlGenerator.cs index 3a8c9cde012..f8ebf596685 100644 --- a/src/EFCore.Relational/Update/UpdateSqlGenerator.cs +++ b/src/EFCore.Relational/Update/UpdateSqlGenerator.cs @@ -317,17 +317,36 @@ protected virtual void AppendUpdateCommandHeader( var (g, n, s) = p; g.SqlGenerationHelper.DelimitIdentifier(sb, o.ColumnName); sb.Append(" = "); - if (!o.UseCurrentValueParameter) - { - AppendSqlLiteral(sb, o, n, s); - } - else - { - g.SqlGenerationHelper.GenerateParameterNamePlaceholder(sb, o.ParameterName); - } + AppendUpdateColumnValue(g.SqlGenerationHelper, o, sb, n, s); }); } + /// + /// Appends a SQL fragment representing the value that is assigned to a column which is being updated. + /// + /// The update sql generator helper. + /// The operation representing the data to be updated. + /// The builder to which the SQL should be appended. + /// The name of the table. + /// The table schema, or to use the default schema. + protected virtual void AppendUpdateColumnValue( + ISqlGenerationHelper updateSqlGeneratorHelper, + IColumnModification columnModification, + StringBuilder stringBuilder, + string name, + string? schema) + { + if (!columnModification.UseCurrentValueParameter) + { + AppendSqlLiteral(stringBuilder, columnModification, name, schema); + } + else + { + updateSqlGeneratorHelper.GenerateParameterNamePlaceholder( + stringBuilder, columnModification.ParameterName); + } + } + /// public virtual ResultSetMapping AppendStoredProcedureCall( StringBuilder commandStringBuilder, diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs index 4ae6e84480c..960161c7e6a 100644 --- a/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs +++ b/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs @@ -135,6 +135,33 @@ protected override void AppendUpdateCommand( commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator); } + /// + protected override void AppendUpdateColumnValue( + ISqlGenerationHelper updateSqlGeneratorHelper, + IColumnModification columnModification, + StringBuilder stringBuilder, + string name, + string? schema) + { + if (columnModification.JsonPath != null + && columnModification.JsonPath != "$") + { + stringBuilder.Append("JSON_MODIFY("); + updateSqlGeneratorHelper.DelimitIdentifier(stringBuilder, columnModification.ColumnName); + + // using strict so that we don't remove json elements when they are assigned NULL value + stringBuilder.Append(", 'strict "); + stringBuilder.Append(columnModification.JsonPath); + stringBuilder.Append("', JSON_QUERY("); + base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema); + stringBuilder.Append("))"); + } + else + { + base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema); + } + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateFixtureBase.cs new file mode 100644 index 00000000000..8d2a773a636 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateFixtureBase.cs @@ -0,0 +1,112 @@ +// 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.TestModels.JsonQuery; + +namespace Microsoft.EntityFrameworkCore.Update; + +public abstract class JsonUpdateFixtureBase : SharedStoreFixtureBase +{ + protected override string StoreName => "JsonUpdateTest"; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity().OwnsOne(x => x.OwnedReferenceRoot, b => + { + b.ToJson(); + b.WithOwner(x => x.Owner); + b.OwnsOne(x => x.OwnedReferenceBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf).WithOwner(x => x.Parent); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + }); + b.OwnsMany(x => x.OwnedCollectionBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.Navigation(x => x.OwnedReferenceLeaf).IsRequired(false); + bb.OwnsMany(x => x.OwnedCollectionLeaf).WithOwner(x => x.Parent); + }); + }); + + modelBuilder.Entity().Navigation(x => x.OwnedReferenceRoot).IsRequired(false); + + modelBuilder.Entity().OwnsMany(x => x.OwnedCollectionRoot, b => + { + b.OwnsOne(x => x.OwnedReferenceBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf).WithOwner(x => x.Parent); + }); + + b.OwnsMany(x => x.OwnedCollectionBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf).WithOwner(x => x.Parent); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + }); + b.ToJson(); + }); + + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity(b => + { + b.OwnsOne(x => x.ReferenceOnBase, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + + b.OwnsMany(x => x.CollectionOnBase, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + }); + + modelBuilder.Entity(b => + { + b.HasBaseType(); + b.OwnsOne(x => x.ReferenceOnDerived, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + + b.OwnsMany(x => x.CollectionOnDerived, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + }); + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + base.OnModelCreating(modelBuilder, context); + } + + protected override void Seed(JsonQueryContext context) + { + var jsonEntitiesBasic = JsonQueryData.CreateJsonEntitiesBasic(); + var jsonEntitiesInheritance = JsonQueryData.CreateJsonEntitiesInheritance(); + + context.JsonEntitiesBasic.AddRange(jsonEntitiesBasic); + context.JsonEntitiesInheritance.AddRange(jsonEntitiesInheritance); + context.SaveChanges(); + } +} diff --git a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs index b55f4a8c988..50ca05af8aa 100644 --- a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs @@ -5,9 +5,17 @@ namespace Microsoft.EntityFrameworkCore.Update; -public abstract class JsonUpdateTestBase: SharedStoreFixtureBase +public abstract class JsonUpdateTestBase : IClassFixture + where TFixture : JsonUpdateFixtureBase, new() { - protected override string StoreName => "JsonUpdateTest"; + public TFixture Fixture { get; } + + protected JsonUpdateTestBase(TFixture fixture) + { + Fixture = fixture; + } + + public JsonQueryContext CreateContext() => Fixture.CreateContext(); [ConditionalFact] public virtual Task Add_entity_with_json() @@ -42,6 +50,7 @@ public virtual Task Add_entity_with_json() }; context.Set().Add(newEntity); + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -104,6 +113,7 @@ public virtual Task Add_json_reference_root() OwnedReferenceLeaf = new JsonOwnedLeaf { SomethingSomething = "ss3" } } }; + ClearLog(); await context.SaveChangesAsync(); }, @@ -133,7 +143,7 @@ public virtual Task Add_json_reference_leaf() { var query = await context.JsonEntitiesBasic.ToListAsync(); var entity = query.Single(); - entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf = null; + entity.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedReferenceLeaf = null; await context.SaveChangesAsync(); }, async context => @@ -141,16 +151,17 @@ public virtual Task Add_json_reference_leaf() var query = await context.JsonEntitiesBasic.ToListAsync(); var entity = query.Single(); - Assert.Null(entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf); + Assert.Null(entity.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedReferenceLeaf); var newLeaf = new JsonOwnedLeaf { SomethingSomething = "ss3" }; - entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf = newLeaf; - + entity.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedReferenceLeaf = newLeaf; + + ClearLog(); await context.SaveChangesAsync(); }, async context => { var updatedEntity = await context.JsonEntitiesBasic.SingleAsync(); - var updatedReference = updatedEntity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf; + var updatedReference = updatedEntity.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedReferenceLeaf; Assert.Equal("ss3", updatedReference.SomethingSomething); }); @@ -184,6 +195,7 @@ public virtual Task Add_element_to_json_collection_root() }; entity.OwnedCollectionRoot.Add(newRoot); + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -227,6 +239,7 @@ public virtual Task Add_element_to_json_collection_branch() }; entity.OwnedReferenceRoot.OwnedCollectionBranch.Add(newBranch); + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -255,6 +268,7 @@ public virtual Task Add_element_to_json_collection_leaf() var entity = query.Single(); var newLeaf = new JsonOwnedLeaf { SomethingSomething = "ss1" }; entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf.Add(newLeaf); + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -276,6 +290,7 @@ public virtual Task Delete_entity_with_json() var entity = query.Single(); context.Set().Remove(entity); + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -295,6 +310,7 @@ public virtual Task Delete_json_reference_root() var query = await context.JsonEntitiesBasic.ToListAsync(); var entity = query.Single(); entity.OwnedReferenceRoot = null; + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -313,6 +329,7 @@ public virtual Task Delete_json_reference_leaf() var query = await context.JsonEntitiesBasic.ToListAsync(); var entity = query.Single(); entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf = null; + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -331,6 +348,7 @@ public virtual Task Delete_json_collection_root() var query = await context.JsonEntitiesBasic.ToListAsync(); var entity = query.Single(); entity.OwnedCollectionRoot = null; + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -349,6 +367,7 @@ public virtual Task Delete_json_collection_branch() var query = await context.JsonEntitiesBasic.ToListAsync(); var entity = query.Single(); entity.OwnedReferenceRoot.OwnedCollectionBranch = null; + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -367,6 +386,7 @@ public virtual Task Edit_element_in_json_collection_root1() var query = await context.JsonEntitiesBasic.ToListAsync(); var entity = query.Single(); entity.OwnedCollectionRoot[0].Name = "Modified"; + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -387,6 +407,7 @@ public virtual Task Edit_element_in_json_collection_root2() var query = await context.JsonEntitiesBasic.ToListAsync(); var entity = query.Single(); entity.OwnedCollectionRoot[1].Name = "Modified"; + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -407,6 +428,7 @@ public virtual Task Edit_element_in_json_collection_branch() var query = await context.JsonEntitiesBasic.ToListAsync(); var entity = query.Single(); entity.OwnedCollectionRoot[0].OwnedCollectionBranch[0].Date = new DateTime(2111, 11, 11); + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -439,6 +461,7 @@ public virtual Task Add_element_to_json_collection_on_derived() }; entity.CollectionOnDerived.Add(newBranch); + ClearLog(); await context.SaveChangesAsync(); }, async context => @@ -456,109 +479,134 @@ public virtual Task Add_element_to_json_collection_on_derived() Assert.Equal("ss2", collectionLeaf[1].SomethingSomething); }); - public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - protected override void Seed(JsonQueryContext context) - { - var jsonEntitiesBasic = JsonQueryData.CreateJsonEntitiesBasic(); - var jsonEntitiesInheritance = JsonQueryData.CreateJsonEntitiesInheritance(); + [ConditionalFact] + public virtual Task Edit_element_in_json_multiple_levels_partial_update() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedReferenceRoot.OwnedReferenceBranch.Date = new DateTime(2111, 11, 11); + entity.OwnedReferenceRoot.Name = "edit"; + entity.OwnedCollectionRoot[0].OwnedCollectionBranch[1].OwnedCollectionLeaf[0].SomethingSomething = "yet another change"; + entity.OwnedCollectionRoot[0].OwnedCollectionBranch[1].OwnedCollectionLeaf[1].SomethingSomething = "and another"; + entity.OwnedCollectionRoot[0].OwnedCollectionBranch[0].OwnedCollectionLeaf[0].SomethingSomething = "...and another"; - context.JsonEntitiesBasic.AddRange(jsonEntitiesBasic); - context.JsonEntitiesInheritance.AddRange(jsonEntitiesInheritance); - context.SaveChanges(); - } + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.Set().SingleAsync(); + Assert.Equal(new DateTime(2111, 11, 11), result.OwnedReferenceRoot.OwnedReferenceBranch.Date); + Assert.Equal("edit", result.OwnedReferenceRoot.Name); + Assert.Equal("yet another change", result.OwnedCollectionRoot[0].OwnedCollectionBranch[1].OwnedCollectionLeaf[0].SomethingSomething); + Assert.Equal("and another", result.OwnedCollectionRoot[0].OwnedCollectionBranch[1].OwnedCollectionLeaf[1].SomethingSomething); + Assert.Equal("...and another", result.OwnedCollectionRoot[0].OwnedCollectionBranch[0].OwnedCollectionLeaf[0].SomethingSomething); + }); - protected override void Clean(DbContext context) - { - base.Clean(context); - } + [ConditionalFact] + public virtual Task Edit_element_in_json_branch_collection_and_add_element_to_the_same_collection() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedReferenceRoot.OwnedCollectionBranch[0].Fraction = 4321.3m; + entity.OwnedReferenceRoot.OwnedCollectionBranch.Add(new JsonOwnedBranch + { + Date = new DateTime(2222, 11, 11), + Enum = JsonEnum.Three, + Fraction = 45.32m, + OwnedReferenceLeaf = new JsonOwnedLeaf { SomethingSomething = "cc" }, + }); - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); - modelBuilder.Entity().OwnsOne(x => x.OwnedReferenceRoot, b => - { - b.ToJson(); - b.WithOwner(x => x.Owner); - b.OwnsOne(x => x.OwnedReferenceBranch, bb => - { - bb.Property(x => x.Fraction).HasPrecision(18, 2); - bb.OwnsOne(x => x.OwnedReferenceLeaf).WithOwner(x => x.Parent); - bb.OwnsMany(x => x.OwnedCollectionLeaf); - }); - b.OwnsMany(x => x.OwnedCollectionBranch, bb => + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => { - bb.Property(x => x.Fraction).HasPrecision(18, 2); - bb.OwnsOne(x => x.OwnedReferenceLeaf); - bb.Navigation(x => x.OwnedReferenceLeaf).IsRequired(false); - bb.OwnsMany(x => x.OwnedCollectionLeaf).WithOwner(x => x.Parent); + var result = await context.Set().SingleAsync(); + Assert.Equal(4321.3m, result.OwnedReferenceRoot.OwnedCollectionBranch[0].Fraction); + + Assert.Equal(new DateTime(2222, 11, 11), result.OwnedReferenceRoot.OwnedCollectionBranch[2].Date); + Assert.Equal(JsonEnum.Three, result.OwnedReferenceRoot.OwnedCollectionBranch[2].Enum); + Assert.Equal(45.32m, result.OwnedReferenceRoot.OwnedCollectionBranch[2].Fraction); + Assert.Equal("cc", result.OwnedReferenceRoot.OwnedCollectionBranch[2].OwnedReferenceLeaf.SomethingSomething); }); - }); - modelBuilder.Entity().Navigation(x => x.OwnedReferenceRoot).IsRequired(false); + [ConditionalFact] + public virtual Task Edit_two_elements_in_the_same_json_collection() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[0].SomethingSomething = "edit1"; + entity.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[1].SomethingSomething = "edit2"; - modelBuilder.Entity().OwnsMany(x => x.OwnedCollectionRoot, b => - { - b.OwnsOne(x => x.OwnedReferenceBranch, bb => + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => { - bb.Property(x => x.Fraction).HasPrecision(18, 2); - bb.OwnsOne(x => x.OwnedReferenceLeaf); - bb.OwnsMany(x => x.OwnedCollectionLeaf).WithOwner(x => x.Parent); + var result = await context.Set().SingleAsync(); + Assert.Equal("edit1", result.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[0].SomethingSomething); + Assert.Equal("edit2", result.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[1].SomethingSomething); }); - b.OwnsMany(x => x.OwnedCollectionBranch, bb => + [ConditionalFact] + public virtual Task Edit_two_elements_in_the_same_json_collection_at_the_root() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => { - bb.Property(x => x.Fraction).HasPrecision(18, 2); - bb.OwnsOne(x => x.OwnedReferenceLeaf).WithOwner(x => x.Parent); - bb.OwnsMany(x => x.OwnedCollectionLeaf); - }); - b.ToJson(); - }); - - modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); - modelBuilder.Entity(b => - { - b.OwnsOne(x => x.ReferenceOnBase, bb => - { - bb.ToJson(); - bb.OwnsOne(x => x.OwnedReferenceLeaf); - bb.OwnsMany(x => x.OwnedCollectionLeaf); - bb.Property(x => x.Fraction).HasPrecision(18, 2); - }); + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedCollectionRoot[0].Name = "edit1"; + entity.OwnedCollectionRoot[1].Name = "edit2"; - b.OwnsMany(x => x.CollectionOnBase, bb => + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => { - bb.ToJson(); - bb.OwnsOne(x => x.OwnedReferenceLeaf); - bb.OwnsMany(x => x.OwnedCollectionLeaf); - bb.Property(x => x.Fraction).HasPrecision(18, 2); + var result = await context.Set().SingleAsync(); + Assert.Equal("edit1", result.OwnedCollectionRoot[0].Name); + Assert.Equal("edit2", result.OwnedCollectionRoot[1].Name); }); - }); - modelBuilder.Entity(b => - { - b.HasBaseType(); - b.OwnsOne(x => x.ReferenceOnDerived, bb => + [ConditionalFact] + public virtual Task Edit_collection_element_and_reference_at_once() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => { - bb.ToJson(); - bb.OwnsOne(x => x.OwnedReferenceLeaf); - bb.OwnsMany(x => x.OwnedCollectionLeaf); - bb.Property(x => x.Fraction).HasPrecision(18, 2); - }); + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedReferenceRoot.OwnedCollectionBranch[1].OwnedCollectionLeaf[0].SomethingSomething = "edit1"; + entity.OwnedReferenceRoot.OwnedCollectionBranch[1].OwnedReferenceLeaf.SomethingSomething = "edit2"; - b.OwnsMany(x => x.CollectionOnDerived, bb => + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => { - bb.ToJson(); - bb.OwnsOne(x => x.OwnedReferenceLeaf); - bb.OwnsMany(x => x.OwnedCollectionLeaf); - bb.Property(x => x.Fraction).HasPrecision(18, 2); + var result = await context.Set().SingleAsync(); + Assert.Equal("edit1", result.OwnedReferenceRoot.OwnedCollectionBranch[1].OwnedCollectionLeaf[0].SomethingSomething); + Assert.Equal("edit2", result.OwnedReferenceRoot.OwnedCollectionBranch[1].OwnedReferenceLeaf.SomethingSomething); }); - }); - modelBuilder.Ignore(); - modelBuilder.Ignore(); + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); - base.OnModelCreating(modelBuilder, context); - } + protected abstract void ClearLog(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerJsonUpdateTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerFixture.cs similarity index 82% rename from test/EFCore.SqlServer.FunctionalTests/Update/SqlServerJsonUpdateTest.cs rename to test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerFixture.cs index 849d21649ef..cdee2fa67bf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerJsonUpdateTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerFixture.cs @@ -3,7 +3,7 @@ namespace Microsoft.EntityFrameworkCore.Update; -public class SqlServerJsonUpdateTest : JsonUpdateTestBase +public class JsonUpdateSqlServerFixture : JsonUpdateFixtureBase { protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs new file mode 100644 index 00000000000..4cb48b4afda --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs @@ -0,0 +1,379 @@ +// 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.Update; + +public class JsonUpdateSqlServerTest : JsonUpdateTestBase +{ + public JsonUpdateSqlServerTest(JsonUpdateSqlServerFixture fixture) + : base(fixture) + { + ClearLog(); + } + + public override async Task Add_element_to_json_collection_branch() + { + await base.Add_element_to_json_collection_branch(); + + AssertSql( + @"@p0='[{""Date"":""2101-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":10.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_r_c1_c1""},{""SomethingSomething"":""e1_r_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_r_c1_r""}},{""Date"":""2102-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":10.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_r_c2_c1""},{""SomethingSomething"":""e1_r_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_r_c2_r""}},{""Date"":""2010-10-10T00:00:00"",""Enum"":""Three"",""Fraction"":42.42,""OwnedCollectionLeaf"":[{""SomethingSomething"":""ss1""},{""SomethingSomething"":""ss2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""ss3""}}]' (Nullable = false) (Size = 622) +@p1='1' + +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].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Add_element_to_json_collection_leaf() + { + await base.Add_element_to_json_collection_leaf(); + + AssertSql( + @"@p0='[{""SomethingSomething"":""e1_r_r_c1""},{""SomethingSomething"":""e1_r_r_c2""},{""SomethingSomething"":""ss1""}]' (Nullable = false) (Size = 100) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = JSON_MODIFY([OwnedReferenceRoot], 'strict $.OwnedReferenceBranch.OwnedCollectionLeaf', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Add_element_to_json_collection_on_derived() + { + await base.Add_element_to_json_collection_on_derived(); + + AssertSql( + @"@p0='[{""Date"":""2221-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":221.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""d2_r_c1""},{""SomethingSomething"":""d2_r_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""d2_r_r""}},{""Date"":""2222-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":222.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""d2_r_c1""},{""SomethingSomething"":""d2_r_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""d2_r_r""}},{""Date"":""2010-10-10T00:00:00"",""Enum"":""Three"",""Fraction"":42.42,""OwnedCollectionLeaf"":[{""SomethingSomething"":""ss1""},{""SomethingSomething"":""ss2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""ss3""}}]' (Nullable = false) (Size = 606) +@p1='2' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesInheritance] SET [CollectionOnDerived] = @p0 +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Discriminator], [j].[Name], [j].[Fraction], JSON_QUERY([j].[CollectionOnBase],'$'), JSON_QUERY([j].[ReferenceOnBase],'$'), JSON_QUERY([j].[CollectionOnDerived],'$'), JSON_QUERY([j].[ReferenceOnDerived],'$') +FROM [JsonEntitiesInheritance] AS [j] +WHERE [j].[Discriminator] = N'JsonEntityInheritanceDerived'"); + } + + public override async Task Add_element_to_json_collection_root() + { + await base.Add_element_to_json_collection_root(); + + AssertSql( + @"@p0='[{""Name"":""e1_c1"",""Number"":11,""OwnedCollectionBranch"":[{""Date"":""2111-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":11.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_c1_c1""},{""SomethingSomething"":""e1_c1_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_c1_r""}},{""Date"":""2112-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":11.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_c2_c1""},{""SomethingSomething"":""e1_c1_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_c2_r""}}],""OwnedReferenceBranch"":{""Date"":""2110-01-01T00:00:00"",""Enum"":""One"",""Fraction"":11.0,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_r_c1""},{""SomethingSomething"":""e1_c1_r_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_r_r""}}},{""Name"":""e1_c2"",""Number"":12,""OwnedCollectionBranch"":[{""Date"":""2121-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":12.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c2_c1_c1""},{""SomethingSomething"":""e1_c2_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c2_c1_r""}},{""Date"":""2122-01-01T00:00:00"",""Enum"":""One"",""Fraction"":12.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c2_c2_c1""},{""SomethingSomething"":""e1_c2_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c2_c2_r""}}],""OwnedReferenceBranch"":{""Date"":""2120-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":12.0,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c2_r_c1""},{""SomethingSomething"":""e1_c2_r_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c2_r_r""}}},{""Name"":""new Name"",""Number"":142,""OwnedCollectionBranch"":[],""OwnedReferenceBranch"":{""Date"":""2010-10-10T00:00:00"",""Enum"":""Three"",""Fraction"":42.42,""OwnedCollectionLeaf"":[{""SomethingSomething"":""ss1""},{""SomethingSomething"":""ss2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""ss3""}}}]' (Nullable = false) (Size = 1723) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Add_entity_with_json() + { + await base.Add_entity_with_json(); + + AssertSql( + @"@p0='{""Name"":""RootName"",""Number"":42,""OwnedCollectionBranch"":[],""OwnedReferenceBranch"":{""Date"":""2010-10-10T00:00:00"",""Enum"":""Three"",""Fraction"":42.42,""OwnedCollectionLeaf"":[{""SomethingSomething"":""ss1""},{""SomethingSomething"":""ss2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""ss3""}}}' (Nullable = false) (Size = 276) +@p1='2' +@p2='NewEntity' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [JsonEntitiesBasic] ([OwnedReferenceRoot], [Id], [Name]) +VALUES (@p0, @p1, @p2);", + // + @"SELECT [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Add_json_reference_leaf() + { + await base.Add_json_reference_leaf(); + + AssertSql( + @"@p0='{""SomethingSomething"":""ss3""}' (Nullable = false) (Size = 28) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = JSON_MODIFY([OwnedReferenceRoot], 'strict $.OwnedCollectionBranch[0].OwnedReferenceLeaf', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Add_json_reference_root() + { + await base.Add_json_reference_root(); + + AssertSql( + @"@p0='{""Name"":""RootName"",""Number"":42,""OwnedCollectionBranch"":[],""OwnedReferenceBranch"":{""Date"":""2010-10-10T00:00:00"",""Enum"":""Three"",""Fraction"":42.42,""OwnedCollectionLeaf"":[{""SomethingSomething"":""ss1""},{""SomethingSomething"":""ss2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""ss3""}}}' (Nullable = false) (Size = 276) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Delete_entity_with_json() + { + await base.Delete_entity_with_json(); + + AssertSql( + @"@p0='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +DELETE FROM [JsonEntitiesBasic] +OUTPUT 1 +WHERE [Id] = @p0;", + // + @"SELECT COUNT(*) +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Delete_json_collection_branch() + { + await base.Delete_json_collection_branch(); + + AssertSql( + @"@p0='[]' (Nullable = false) (Size = 2) +@p1='1' + +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].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Delete_json_collection_root() + { + await base.Delete_json_collection_root(); + + AssertSql( + @"@p0='[]' (Nullable = false) (Size = 2) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Delete_json_reference_leaf() + { + await base.Delete_json_reference_leaf(); + + AssertSql( + @"@p0=NULL (Nullable = false) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = JSON_MODIFY([OwnedReferenceRoot], 'strict $.OwnedReferenceBranch.OwnedReferenceLeaf', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Delete_json_reference_root() + { + await base.Delete_json_reference_root(); + + AssertSql( + @"@p0=NULL (Nullable = false) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Edit_element_in_json_collection_branch() + { + await base.Edit_element_in_json_collection_branch(); + + AssertSql( + @"@p0='{""Date"":""2111-11-11T00:00:00"",""Enum"":""Two"",""Fraction"":11.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_c1_c1""},{""SomethingSomething"":""e1_c1_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_c1_r""}}' (Nullable = false) (Size = 214) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = JSON_MODIFY([OwnedCollectionRoot], 'strict $[0].OwnedCollectionBranch[0]', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Edit_element_in_json_collection_root1() + { + await base.Edit_element_in_json_collection_root1(); + + AssertSql( + @"@p0='{""Name"":""Modified"",""Number"":11,""OwnedCollectionBranch"":[{""Date"":""2111-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":11.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_c1_c1""},{""SomethingSomething"":""e1_c1_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_c1_r""}},{""Date"":""2112-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":11.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_c2_c1""},{""SomethingSomething"":""e1_c1_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_c2_r""}}],""OwnedReferenceBranch"":{""Date"":""2110-01-01T00:00:00"",""Enum"":""One"",""Fraction"":11.0,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_r_c1""},{""SomethingSomething"":""e1_c1_r_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_r_r""}}}' (Nullable = false) (Size = 724) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = JSON_MODIFY([OwnedCollectionRoot], 'strict $[0]', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Edit_element_in_json_collection_root2() + { + await base.Edit_element_in_json_collection_root2(); + + AssertSql( + @"@p0='{""Name"":""Modified"",""Number"":12,""OwnedCollectionBranch"":[{""Date"":""2121-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":12.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c2_c1_c1""},{""SomethingSomething"":""e1_c2_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c2_c1_r""}},{""Date"":""2122-01-01T00:00:00"",""Enum"":""One"",""Fraction"":12.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c2_c2_c1""},{""SomethingSomething"":""e1_c2_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c2_c2_r""}}],""OwnedReferenceBranch"":{""Date"":""2120-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":12.0,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c2_r_c1""},{""SomethingSomething"":""e1_c2_r_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c2_r_r""}}}' (Nullable = false) (Size = 724) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = JSON_MODIFY([OwnedCollectionRoot], 'strict $[1]', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Edit_element_in_json_multiple_levels_partial_update() + { + await base.Edit_element_in_json_multiple_levels_partial_update(); + + AssertSql( + @"@p0='[{""Date"":""2111-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":11.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""...and another""},{""SomethingSomething"":""e1_c1_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_c1_r""}},{""Date"":""2112-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":11.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""yet another change""},{""SomethingSomething"":""and another""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_c2_r""}}]' (Nullable = false) (Size = 443) +@p1='{""Name"":""edit"",""Number"":10,""OwnedCollectionBranch"":[{""Date"":""2101-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":10.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_r_c1_c1""},{""SomethingSomething"":""e1_r_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_r_c1_r""}},{""Date"":""2102-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":10.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_r_c2_c1""},{""SomethingSomething"":""e1_r_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_r_c2_r""}}],""OwnedReferenceBranch"":{""Date"":""2111-11-11T00:00:00"",""Enum"":""One"",""Fraction"":10.0,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_r_r_c1""},{""SomethingSomething"":""e1_r_r_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_r_r_r""}}}' (Nullable = false) (Size = 711) +@p2='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = JSON_MODIFY([OwnedCollectionRoot], 'strict $[0].OwnedCollectionBranch', JSON_QUERY(@p0)), [OwnedReferenceRoot] = @p1 +OUTPUT 1 +WHERE [Id] = @p2;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Edit_element_in_json_branch_collection_and_add_element_to_the_same_collection() + { + await base.Edit_element_in_json_branch_collection_and_add_element_to_the_same_collection(); + + AssertSql( + @"@p0='[{""Date"":""2101-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":4321.3,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_r_c1_c1""},{""SomethingSomething"":""e1_r_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_r_c1_r""}},{""Date"":""2102-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":10.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_r_c2_c1""},{""SomethingSomething"":""e1_r_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_r_c2_r""}},{""Date"":""2222-11-11T00:00:00"",""Enum"":""Three"",""Fraction"":45.32,""OwnedCollectionLeaf"":[],""OwnedReferenceLeaf"":{""SomethingSomething"":""cc""}}]' (Nullable = false) (Size = 566) +@p1='1' + +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].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Edit_two_elements_in_the_same_json_collection() + { + await base.Edit_two_elements_in_the_same_json_collection(); + + AssertSql( + @"@p0='[{""SomethingSomething"":""edit1""},{""SomethingSomething"":""edit2""}]' (Nullable = false) (Size = 63) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = JSON_MODIFY([OwnedReferenceRoot], 'strict $.OwnedCollectionBranch[0].OwnedCollectionLeaf', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Edit_two_elements_in_the_same_json_collection_at_the_root() + { + await base.Edit_two_elements_in_the_same_json_collection_at_the_root(); + + AssertSql( + @"@p0='[{""Name"":""edit1"",""Number"":11,""OwnedCollectionBranch"":[{""Date"":""2111-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":11.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_c1_c1""},{""SomethingSomething"":""e1_c1_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_c1_r""}},{""Date"":""2112-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":11.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_c2_c1""},{""SomethingSomething"":""e1_c1_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_c2_r""}}],""OwnedReferenceBranch"":{""Date"":""2110-01-01T00:00:00"",""Enum"":""One"",""Fraction"":11.0,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c1_r_c1""},{""SomethingSomething"":""e1_c1_r_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c1_r_r""}}},{""Name"":""edit2"",""Number"":12,""OwnedCollectionBranch"":[{""Date"":""2121-01-01T00:00:00"",""Enum"":""Two"",""Fraction"":12.1,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c2_c1_c1""},{""SomethingSomething"":""e1_c2_c1_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c2_c1_r""}},{""Date"":""2122-01-01T00:00:00"",""Enum"":""One"",""Fraction"":12.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c2_c2_c1""},{""SomethingSomething"":""e1_c2_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c2_c2_r""}}],""OwnedReferenceBranch"":{""Date"":""2120-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":12.0,""OwnedCollectionLeaf"":[{""SomethingSomething"":""e1_c2_r_c1""},{""SomethingSomething"":""e1_c2_r_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""e1_c2_r_r""}}}]' (Nullable = false) (Size = 1445) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedCollectionRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Edit_collection_element_and_reference_at_once() + { + await base.Edit_collection_element_and_reference_at_once(); + + AssertSql( + @"@p0='{""Date"":""2102-01-01T00:00:00"",""Enum"":""Three"",""Fraction"":10.2,""OwnedCollectionLeaf"":[{""SomethingSomething"":""edit1""},{""SomethingSomething"":""e1_r_c2_c2""}],""OwnedReferenceLeaf"":{""SomethingSomething"":""edit2""}}' (Nullable = false) (Size = 204) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = JSON_MODIFY([OwnedReferenceRoot], 'strict $.OwnedCollectionBranch[1]', JSON_QUERY(@p0)) +OUTPUT 1 +WHERE [Id] = @p1;", + // + @"SELECT TOP(2) [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs b/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs index c3d15497ae7..0aba24228e3 100644 --- a/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs @@ -9,7 +9,7 @@ public class SqliteComplianceTest : RelationalComplianceTestBase { typeof(FromSqlSprocQueryTestBase<>), typeof(JsonQueryTestBase<>), - typeof(JsonUpdateTestBase), + typeof(JsonUpdateTestBase<>), typeof(SqlExecutorTestBase<>), typeof(UdfDbFunctionTestBase<>), typeof(TPCRelationshipsQueryTestBase<>), // internal class is added