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..4dc4d9f55a5 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 record struct 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