diff --git a/src/EFCore.Relational/Storage/JsonTypeMapping.cs b/src/EFCore.Relational/Storage/JsonTypeMapping.cs index 9a14f340bae..62ced3a64ac 100644 --- a/src/EFCore.Relational/Storage/JsonTypeMapping.cs +++ b/src/EFCore.Relational/Storage/JsonTypeMapping.cs @@ -18,24 +18,24 @@ namespace Microsoft.EntityFrameworkCore.Storage; /// See Implementation of database providers and extensions /// for more information and examples. /// -public abstract class JsonTypeMapping : RelationalTypeMapping +public abstract class StructuralJsonTypeMapping : RelationalTypeMapping { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The name of the database type. /// The .NET type. /// The to be used. - protected JsonTypeMapping(string storeType, Type clrType, DbType? dbType) + protected StructuralJsonTypeMapping(string storeType, Type clrType, DbType? dbType) : base(storeType, clrType, dbType) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Parameter object for . - protected JsonTypeMapping(RelationalTypeMappingParameters parameters) + protected StructuralJsonTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters) { } @@ -45,3 +45,9 @@ protected override string GenerateNonNullSqlLiteral(object value) => throw new InvalidOperationException( RelationalStrings.MethodNeedsToBeImplementedInTheProvider); } + +/// +/// Use StructuralJsonTypeMapping instead for type mappings representing JSON structural types as opposed to JSON strings. +/// +[Obsolete("Use StructuralJsonTypeMapping instead for type mappings representing JSON structural types as opposed to JSON strings.")] +public class JsonTypeMapping; diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs index 881c64c0d37..06b281186f6 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs @@ -1,7 +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.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.SqlServer.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -55,8 +54,7 @@ public Expression Process(Expression expression) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - [return: NotNullIfNotNull(nameof(expression))] - public override Expression? Visit(Expression? expression) + protected override Expression VisitExtension(Expression expression) { switch (expression) { @@ -128,7 +126,7 @@ public Expression Process(Expression expression) { Check.DebugAssert(newTables is null, "newTables must be null if columnsToRewrite is null"); - result = (SelectExpression)base.Visit(result); + result = (SelectExpression)base.VisitExtension(result); } else { @@ -154,7 +152,7 @@ public Expression Process(Expression expression) // Record the OPENJSON expression and its projected column(s), along with the store type we just removed from the WITH // clause. Then visit the select expression, adding a cast around the matching ColumnExpressions. - result = (SelectExpression)base.Visit(result); + result = (SelectExpression)base.VisitExtension(result); foreach (var columnsToRewriteKey in columnsToRewrite.Keys) { @@ -270,19 +268,30 @@ when _columnsToRewrite.TryGetValue((columnExpression.TableAlias, columnExpressio && left is not SqlConstantExpression { Value: null } && right is not SqlConstantExpression { Value: null }: { - return comparison.Update( - sqlExpressionFactory.Convert( - left, - typeof(string), - typeMappingSource.FindMapping(typeof(string))), - sqlExpressionFactory.Convert( - right, - typeof(string), - typeMappingSource.FindMapping(typeof(string)))); + var stringTypeMapping = typeMappingSource.FindMapping(typeof(string))!; + + return comparison.Update(ConvertToString(left), ConvertToString(right)); + + SqlExpression ConvertToString(SqlExpression expression) + { + if (expression.TypeMapping?.StoreType.Equals("json", StringComparison.OrdinalIgnoreCase) == true) + { + // If the expression happens to be a json literal (CAST('...' AS json)), we can just extract the string inside, + // instead of applying an additional CAST around it + return expression is SqlConstantExpression constant + ? new SqlConstantExpression( + constant.Value, + typeof(string), + (RelationalTypeMapping)stringTypeMapping.WithComposedConverter(constant.TypeMapping!.Converter)) + : sqlExpressionFactory.Convert(expression, typeof(string), stringTypeMapping); + } + + return expression; + } } default: - return base.Visit(expression); + return base.VisitExtension(expression); } static bool IsKeyColumn(SqlExpression sqlExpression, string openJsonTableAlias) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs index 16010bc6676..a575aa0307c 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs @@ -9,28 +9,19 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class SqlServerQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor +public class SqlServerQueryTranslationPostprocessor( + QueryTranslationPostprocessorDependencies dependencies, + RelationalQueryTranslationPostprocessorDependencies relationalDependencies, + SqlServerQueryCompilationContext queryCompilationContext) + : RelationalQueryTranslationPostprocessor(dependencies, relationalDependencies, queryCompilationContext) { - private readonly SqlServerJsonPostprocessor _jsonPostprocessor; - private readonly SqlServerAggregateOverSubqueryPostprocessor _aggregatePostprocessor; - private readonly SqlServerSqlTreePruner _pruner = new(); + private readonly SqlServerJsonPostprocessor _jsonPostprocessor = new( + relationalDependencies.TypeMappingSource, + relationalDependencies.SqlExpressionFactory, + queryCompilationContext.SqlAliasManager); - /// - /// 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 - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public SqlServerQueryTranslationPostprocessor( - QueryTranslationPostprocessorDependencies dependencies, - RelationalQueryTranslationPostprocessorDependencies relationalDependencies, - SqlServerQueryCompilationContext queryCompilationContext) - : base(dependencies, relationalDependencies, queryCompilationContext) - { - _jsonPostprocessor = new SqlServerJsonPostprocessor( - relationalDependencies.TypeMappingSource, relationalDependencies.SqlExpressionFactory, queryCompilationContext.SqlAliasManager); - _aggregatePostprocessor = new SqlServerAggregateOverSubqueryPostprocessor(queryCompilationContext.SqlAliasManager); - } + private readonly SqlServerAggregateOverSubqueryPostprocessor _aggregatePostprocessor = new(queryCompilationContext.SqlAliasManager); + private readonly SqlServerSqlTreePruner _pruner = new(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs index 55177d3d266..d87a3490c31 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs @@ -608,6 +608,71 @@ IComplexType complexType false)); } + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override ShapedQueryExpression? TranslateContains(ShapedQueryExpression source, Expression item) + { + // Attempt to translate to JSON_CONTAINS for SQL Server 2025+ (compatibility level 170+). + // JSON_CONTAINS is more efficient than IN (SELECT ... FROM OPENJSON(...)) for primitive collections. + if (_sqlServerSingletonOptions.SupportsJsonType + && source.QueryExpression is SelectExpression + { + // Primitive collection over OPENJSON (e.g. [p].[Ints]) + Tables: + [ + SqlServerOpenJsonExpression + { + // JSON_CONTAINS() is only supported over json, not nvarchar + Json: { TypeMapping: SqlServerJsonTypeMapping } json, + Path: null, + ColumnInfos: [{ Name: "value" }] + } + ], + Predicate: null, + GroupBy: [], + Having: null, + IsDistinct: false, + Limit: null, + Offset: null + } + && TranslateExpression(item, applyDefaultTypeMapping: false) is { } translatedItem + // Literal untyped NULL not supported as item by JSON_CONTAINS(). + // For any other nullable item, SqlServerNullabilityProcessor will add a null check around the JSON_CONTAINS call. + && translatedItem is not SqlConstantExpression { Value: null } + // Note: JSON_CONTAINS doesn't allow searching for null items within a JSON collection (returns 0) + // As a result, we only translate to JSON_CONTAINS when we know that either the item is non-nullable or the collection's elements are. + && ( + translatedItem is ColumnExpression { IsNullable: false } or SqlConstantExpression { Value: not null } + || !translatedItem.Type.IsNullableType() + || json.Type.GetSequenceType() is var elementClrType && !elementClrType.IsNullableType())) + { + // JSON_CONTAINS returns 1 if found, 0 if not found. It's a search condition expression. + var jsonContains = _sqlExpressionFactory.Equal( + _sqlExpressionFactory.Function( + "JSON_CONTAINS", + [json, translatedItem], + nullable: true, + argumentsPropagateNullability: [false, true], + typeof(int)), + _sqlExpressionFactory.Constant(1)); + +#pragma warning disable EF1001 // Internal EF Core API usage. + var selectExpression = new SelectExpression(jsonContains, _queryCompilationContext.SqlAliasManager); + return source.Update( + selectExpression, + Expression.Convert( + new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)), + typeof(bool))); +#pragma warning restore EF1001 // Internal EF Core API usage. + } + + return base.TranslateContains(source, item); + } + /// /// 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 @@ -623,8 +688,8 @@ IComplexType complexType { switch (source.QueryExpression) { - // index on parameter using a column - // translate via JSON because it is a better translation + // Index on parameter using a column + // Translate via JSON_VALUE() instead of via a VALUES subquery case SelectExpression { Tables: [ValuesExpression { ValuesParameter: { } valuesParameter }], @@ -938,15 +1003,7 @@ protected override bool TrySerializeScalarToJson( // IReadOnlyList (just like Json{Scalar,Query}Expression), but instead we do the slight hack of packaging it // as a constant argument; it will be unpacked and handled in SQL generation. _sqlExpressionFactory.Constant(path, RelationalTypeMapping.NullMapping), - - // If an inline JSON object (complex type) is being assigned, it would be rendered here as a simple string: - // [column].modify('$.foo', '{ "x": 8 }') - // Since it's untyped, modify would treat is as a string rather than a JSON object, and insert it as such into - // the enclosing object, escaping all the special JSON characters - that's not what we want. - // We add a cast to JSON to have it interpreted as a JSON object. - value is SqlConstantExpression { TypeMapping.StoreType: "json" } - ? _sqlExpressionFactory.Convert(value, value.Type, _typeMappingSource.FindMapping("json")!) - : value + value ], nullable: true, instancePropagatesNullability: true, diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index ce62d6755b1..6d75fd93a32 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -138,6 +138,48 @@ protected virtual SqlExpression VisitSqlServerAggregateFunction( : aggregateFunctionExpression; } + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override SqlExpression VisitSqlFunction( + SqlFunctionExpression sqlFunctionExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + if (sqlFunctionExpression is { Name: "JSON_CONTAINS", Arguments: [var collection, var item] } jsonContains) + { + // JSON_CONTAINS() does not allow searching for NULL within a JSON collection (always returns zero when the item is NULL). + // As a result, we do not translate to JSON_CONTAINS() in SqlServerQueryableMethodTranslatingExpressionVisitor unless we know that + // either the item or the collection's elements are non-nullable. + // When the item argument is nullable, we add a null check around JSON_CONTAINS(): + // CASE WHEN @item IS NULL THEN NULL ELSE JSON_CONTAINS(collection, @item) END + item = Visit(item, out var itemNullable); + collection = Visit(collection, out var collectionNullable); + + sqlFunctionExpression = jsonContains.Update(instance: null, arguments: [collection, item]); + + if (itemNullable && !UseRelationalNulls) + { + nullable = true; + return Dependencies.SqlExpressionFactory.Case( + [ + new CaseWhenClause( + Dependencies.SqlExpressionFactory.IsNull(item), + Dependencies.SqlExpressionFactory.Constant(null, typeof(bool?), jsonContains.TypeMapping)) + ], + jsonContains); + } + + nullable = itemNullable || collectionNullable; + return sqlFunctionExpression; + } + + return base.VisitSqlFunction(sqlFunctionExpression, allowOptimizedExpansion, out nullable); + } + /// /// 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/src/EFCore.SqlServer/Query/Internal/SqlServerTypeMappingPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerTypeMappingPostprocessor.cs index 1184e8a7b1e..96101c557b0 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerTypeMappingPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerTypeMappingPostprocessor.cs @@ -101,14 +101,15 @@ protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpress elementTypeMapping = e; } - if (parameterTypeMapping is not SqlServerStringTypeMapping { ElementTypeMapping: not null }) + if (parameterTypeMapping is not SqlServerStringTypeMapping { ElementTypeMapping: not null } + and not SqlServerJsonTypeMapping { ElementTypeMapping: not null }) { - throw new UnreachableException("A SqlServerStringTypeMapping collection type mapping could not be found"); + throw new UnreachableException("A string/JSON collection type mapping was not found"); } return openJsonExpression.Update( parameterExpression.ApplyTypeMapping(parameterTypeMapping), path: null, - [new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping, [])]); + [new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping, Path: [])]); } } diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs new file mode 100644 index 00000000000..369a240eeeb --- /dev/null +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.SqlClient; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; + +/// +/// 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 +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerJsonTypeMapping : StringTypeMapping +{ + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static new SqlServerJsonTypeMapping Default { get; } = new(); + + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerJsonTypeMapping() + : base("json", dbType: null) + { + } + + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected SqlServerJsonTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SqlServerJsonTypeMapping(parameters); + + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override string GenerateNonNullSqlLiteral(object value) + => $"CAST({base.GenerateNonNullSqlLiteral(value)} AS json)"; + + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override void ConfigureParameter(DbParameter parameter) + => ((SqlParameter)parameter).SqlDbType = System.Data.SqlDbType.Json; +} diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs index d265674f6ca..f28f1d15523 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs @@ -34,14 +34,6 @@ public class SqlServerStringTypeMapping : StringTypeMapping /// public static new SqlServerStringTypeMapping Default { get; } = new(); - /// - /// 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 - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static SqlServerStringTypeMapping JsonTypeDefault { get; } = new("json", sqlDbType: SqlDbType.Json); - /// /// 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 @@ -157,13 +149,10 @@ protected override void ConfigureParameter(DbParameter parameter) var value = parameter.Value; var length = (value as string)?.Length; - var sqlDbType = _sqlDbType - ?? (StoreType == "json" ? SqlDbType.Json : null); - - if (sqlDbType.HasValue + if (_sqlDbType.HasValue && parameter is SqlParameter sqlParameter) // To avoid crashing wrapping providers { - sqlParameter.SqlDbType = sqlDbType.Value; + sqlParameter.SqlDbType = _sqlDbType.Value; } if ((value == null diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStructuralJsonTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStructuralJsonTypeMapping.cs index 42fae29b1cc..3de33d52e90 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStructuralJsonTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStructuralJsonTypeMapping.cs @@ -13,7 +13,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class SqlServerStructuralJsonTypeMapping : JsonTypeMapping +public class SqlServerStructuralJsonTypeMapping : StructuralJsonTypeMapping { private static readonly MethodInfo CreateUtf8StreamMethod = typeof(SqlServerStructuralJsonTypeMapping).GetMethod(nameof(CreateUtf8Stream), [typeof(string)])!; @@ -113,7 +113,13 @@ protected virtual string EscapeSqlLiteral(string literal) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => $"'{EscapeSqlLiteral((string)value)}'"; + => StoreTypeNameBase switch + { + "json" => $"CAST('{EscapeSqlLiteral((string)value)}' AS json)", + "nvarchar" => $"'{EscapeSqlLiteral((string)value)}'", + + _ => throw new UnreachableException() + }; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index 8b6613f61c2..2523a347c8f 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs @@ -184,7 +184,7 @@ static SqlServerTypeMappingSource() { "float", [SqlServerDoubleTypeMapping.Default] }, { "image", [ImageBinary] }, { "int", [IntTypeMapping.Default] }, - { "json", [SqlServerStringTypeMapping.JsonTypeDefault] }, + { "json", [SqlServerJsonTypeMapping.Default] }, { "money", [Money] }, { "national char varying", [VariableLengthUnicodeString] }, { "national char varying(max)", [VariableLengthMaxUnicodeString] }, @@ -322,7 +322,7 @@ static SqlServerTypeMappingSource() return Rowversion; case { } t when t == typeof(string) && storeTypeName == "json": - return SqlServerStringTypeMapping.JsonTypeDefault; + return SqlServerJsonTypeMapping.Default; case { } t when t == typeof(string): { diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteJsonTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteJsonTypeMapping.cs index c90b5317a3d..8fcea93409b 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteJsonTypeMapping.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteJsonTypeMapping.cs @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class SqliteJsonTypeMapping : JsonTypeMapping +public class SqliteStructuralJsonTypeMapping : StructuralJsonTypeMapping { private static readonly MethodInfo GetStringMethod = typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), [typeof(int)])!; @@ -31,13 +31,13 @@ private static readonly ConstructorInfo MemoryStreamConstructor /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public static SqliteJsonTypeMapping Default { get; } = new(SqliteTypeMappingSource.TextTypeName); + public static SqliteStructuralJsonTypeMapping Default { get; } = new(SqliteTypeMappingSource.TextTypeName); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The name of the database type. - public SqliteJsonTypeMapping(string storeType) + public SqliteStructuralJsonTypeMapping(string storeType) : base(storeType, typeof(JsonTypePlaceholder), System.Data.DbType.String) { } @@ -48,7 +48,7 @@ public SqliteJsonTypeMapping(string storeType) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected SqliteJsonTypeMapping(RelationalTypeMappingParameters parameters) + protected SqliteStructuralJsonTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters) { } @@ -101,5 +101,5 @@ protected virtual string EscapeSqlLiteral(string literal) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new SqliteJsonTypeMapping(parameters); + => new SqliteStructuralJsonTypeMapping(parameters); } diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs index d28b9373970..c944add36f2 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs @@ -81,7 +81,7 @@ private static readonly HashSet SpatialiteTypes { typeof(double), Real }, { typeof(float), new FloatTypeMapping(RealTypeName) }, { typeof(Guid), SqliteGuidTypeMapping.Default }, - { typeof(JsonTypePlaceholder), SqliteJsonTypeMapping.Default } + { typeof(JsonTypePlaceholder), SqliteStructuralJsonTypeMapping.Default } }; private readonly Dictionary _storeTypeMappings = new(StringComparer.OrdinalIgnoreCase) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateSqlServerTest.cs index 7ca69d042b0..894e1d97cde 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateSqlServerTest.cs @@ -266,12 +266,24 @@ public override async Task Update_associate_to_inline_with_lambda() { await base.Update_associate_to_inline_with_lambda(); - AssertExecuteUpdateSql( - """ + if (Fixture.UsingJsonType) + { + AssertExecuteUpdateSql( + """ +UPDATE [r] +SET [r].[RequiredAssociate] = CAST('{"Id":1000,"Int":70,"Ints":[1,2,4],"Name":"Updated associate name","String":"Updated associate string","NestedCollection":[],"OptionalNestedAssociate":null,"RequiredNestedAssociate":{"Id":1000,"Int":80,"Ints":[1,2,4],"Name":"Updated nested name","String":"Updated nested string"}}' AS json) +FROM [RootEntity] AS [r] +"""); + } + else + { + AssertExecuteUpdateSql( + """ UPDATE [r] SET [r].[RequiredAssociate] = '{"Id":1000,"Int":70,"Ints":[1,2,4],"Name":"Updated associate name","String":"Updated associate string","NestedCollection":[],"OptionalNestedAssociate":null,"RequiredNestedAssociate":{"Id":1000,"Int":80,"Ints":[1,2,4],"Name":"Updated nested name","String":"Updated nested string"}}' FROM [RootEntity] AS [r] """); + } } public override async Task Update_nested_associate_to_inline_with_lambda() @@ -487,7 +499,7 @@ public override async Task Update_primitive_collection_to_parameter() { AssertExecuteUpdateSql( """ -@ints='[1,2,4]' (Size = 8000) +@ints='[1,2,4]' (Size = 7) UPDATE [r] SET [RequiredAssociate].modify('$.Ints', @ints) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonPrimitiveCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonPrimitiveCollectionSqlServerTest.cs index 4b124a5daad..c32a57c936c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonPrimitiveCollectionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonPrimitiveCollectionSqlServerTest.cs @@ -48,8 +48,19 @@ public override async Task Contains() { await base.Contains(); - AssertSql( - """ + if (Fixture.UsingJsonType) + { + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] +FROM [RootEntity] AS [r] +WHERE JSON_CONTAINS(JSON_QUERY([r].[RequiredAssociate], '$.Ints'), 3) = 1 +"""); + } + else + { + AssertSql( + """ SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] FROM [RootEntity] AS [r] WHERE 3 IN ( @@ -57,14 +68,26 @@ SELECT [i].[value] FROM OPENJSON(JSON_QUERY([r].[RequiredAssociate], '$.Ints')) WITH ([value] int '$') AS [i] ) """); + } } public override async Task Any_predicate() { await base.Any_predicate(); - AssertSql( - """ + if (Fixture.UsingJsonType) + { + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] +FROM [RootEntity] AS [r] +WHERE JSON_CONTAINS(JSON_QUERY([r].[RequiredAssociate], '$.Ints'), 2) = 1 +"""); + } + else + { + AssertSql( + """ SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] FROM [RootEntity] AS [r] WHERE 2 IN ( @@ -72,6 +95,7 @@ SELECT [i].[value] FROM OPENJSON(JSON_QUERY([r].[RequiredAssociate], '$.Ints')) WITH ([value] int '$') AS [i] ) """); + } } public override async Task Nested_Count() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs index af756237f77..598087799ec 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs @@ -140,7 +140,7 @@ public override async Task Nested_associate_with_inline() """ SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] FROM [RootEntity] AS [r] -WHERE CAST(JSON_QUERY([r].[RequiredAssociate], '$.RequiredNestedAssociate') AS nvarchar(max)) = CAST('{"Id":1000,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_RequiredNestedAssociate","String":"foo"}' AS nvarchar(max)) +WHERE CAST(JSON_QUERY([r].[RequiredAssociate], '$.RequiredNestedAssociate') AS nvarchar(max)) = N'{"Id":1000,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_RequiredNestedAssociate","String":"foo"}' """); } else @@ -216,7 +216,7 @@ public override async Task Nested_collection_with_inline() """ SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] FROM [RootEntity] AS [r] -WHERE CAST(JSON_QUERY([r].[RequiredAssociate], '$.NestedCollection') AS nvarchar(max)) = CAST('[{"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"},{"Id":1003,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_2","String":"foo"}]' AS nvarchar(max)) +WHERE CAST(JSON_QUERY([r].[RequiredAssociate], '$.NestedCollection') AS nvarchar(max)) = N'[{"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"},{"Id":1003,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_2","String":"foo"}]' """); } else @@ -282,7 +282,7 @@ FROM OPENJSON([r].[RequiredAssociate], '$.NestedCollection') WITH ( [Name] nvarchar(max) '$.Name', [String] nvarchar(max) '$.String' ) AS [n] - WHERE [n].[Id] = 1002 AND [n].[Int] = 8 AND CAST([n].[Ints] AS nvarchar(max)) = CAST('[1,2,3]' AS nvarchar(max)) AND [n].[Name] = N'Root1_RequiredAssociate_NestedCollection_1' AND [n].[String] = N'foo') + WHERE [n].[Id] = 1002 AND [n].[Int] = 8 AND CAST([n].[Ints] AS nvarchar(max)) = N'[1,2,3]' AND [n].[Name] = N'Root1_RequiredAssociate_NestedCollection_1' AND [n].[String] = N'foo') """); } else @@ -318,7 +318,7 @@ public override async Task Contains_with_parameter() """ @entity_equality_nested_Id='1002' (Nullable = true) @entity_equality_nested_Int='8' (Nullable = true) -@entity_equality_nested_Ints='[1,2,3]' (Size = 8000) +@entity_equality_nested_Ints='[1,2,3]' (Size = 7) @entity_equality_nested_Name='Root1_RequiredAssociate_NestedCollection_1' (Size = 4000) @entity_equality_nested_String='foo' (Size = 4000) @@ -373,7 +373,7 @@ public override async Task Contains_with_operators_composed_on_the_collection() @get_Item_Int='106' @entity_equality_get_Item_Id='3003' (Nullable = true) @entity_equality_get_Item_Int='108' (Nullable = true) -@entity_equality_get_Item_Ints='[8,9,109]' (Size = 8000) +@entity_equality_get_Item_Ints='[8,9,109]' (Size = 9) @entity_equality_get_Item_Name='Root3_RequiredAssociate_NestedCollection_2' (Size = 4000) @entity_equality_get_Item_String='foo104' (Size = 4000) @@ -429,7 +429,7 @@ public override async Task Contains_with_nested_and_composed_operators() @get_Item_Id='302' @entity_equality_get_Item_Id='303' (Nullable = true) @entity_equality_get_Item_Int='130' (Nullable = true) -@entity_equality_get_Item_Ints='[8,9,131]' (Size = 8000) +@entity_equality_get_Item_Ints='[8,9,131]' (Size = 9) @entity_equality_get_Item_Name='Root3_AssociateCollection_2' (Size = 4000) @entity_equality_get_Item_String='foo115' (Size = 4000) @entity_equality_get_Item_NestedCollection='[{"Id":3014,"Int":136,"Ints":[8,9,137],"Name":"Root3_AssociateCollection_2_NestedCollection_1","String":"foo118"},{"Id":3015,"Int":138,"Ints":[8,9,139],"Name":"Root3_Root1_AssociateCollection_2_NestedCollection_2","String":"foo119"}]' (Size = 233) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonPrimitiveCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonPrimitiveCollectionSqlServerTest.cs index cdd9ba0b0b4..ae6f59caf65 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonPrimitiveCollectionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonPrimitiveCollectionSqlServerTest.cs @@ -48,8 +48,19 @@ public override async Task Contains() { await base.Contains(); - AssertSql( - """ + if (Fixture.UsingJsonType) + { + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] +FROM [RootEntity] AS [r] +WHERE JSON_CONTAINS(JSON_QUERY([r].[RequiredAssociate], '$.Ints'), 3) = 1 +"""); + } + else + { + AssertSql( + """ SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] FROM [RootEntity] AS [r] WHERE 3 IN ( @@ -57,14 +68,26 @@ SELECT [i].[value] FROM OPENJSON(JSON_QUERY([r].[RequiredAssociate], '$.Ints')) WITH ([value] int '$') AS [i] ) """); + } } public override async Task Any_predicate() { await base.Any_predicate(); - AssertSql( - """ + if (Fixture.UsingJsonType) + { + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] +FROM [RootEntity] AS [r] +WHERE JSON_CONTAINS(JSON_QUERY([r].[RequiredAssociate], '$.Ints'), 2) = 1 +"""); + } + else + { + AssertSql( + """ SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] FROM [RootEntity] AS [r] WHERE 2 IN ( @@ -72,6 +95,7 @@ SELECT [i].[value] FROM OPENJSON(JSON_QUERY([r].[RequiredAssociate], '$.Ints')) WITH ([value] int '$') AS [i] ) """); + } } public override async Task Nested_Count() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index 37589e77a17..96ab5cb01f7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -3,7 +3,7 @@ namespace Microsoft.EntityFrameworkCore.Query; -[SqlServerCondition(SqlServerCondition.SupportsFunctions2022 | SqlServerCondition.SupportsJsonType)] +[SqlServerCondition(SqlServerCondition.SupportsJsonType)] public class PrimitiveCollectionsQuerySqlServerJsonTypeTest : PrimitiveCollectionsQueryRelationalTestBase< PrimitiveCollectionsQuerySqlServerJsonTypeTest.PrimitiveCollectionsQuerySqlServerFixture> { @@ -570,7 +570,7 @@ public override async Task Inline_collection_Contains_with_EF_Parameter() AssertSql( """ -@p='[2,999,1000]' (Size = 4000) +@p='[2,999,1000]' (Size = 12) SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] @@ -587,7 +587,7 @@ public override async Task Inline_collection_Contains_with_IEnumerable_EF_Parame AssertSql( """ -@Select='["10","a","aa"]' (Size = 4000) +@Select='["10","a","aa"]' (Size = 15) SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] @@ -604,7 +604,7 @@ public override async Task Inline_collection_Count_with_column_predicate_with_EF AssertSql( """ -@p='[2,999,1000]' (Size = 4000) +@p='[2,999,1000]' (Size = 12) SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] @@ -1225,10 +1225,7 @@ public override async Task Column_collection_of_ints_Contains() """ SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] -WHERE 10 IN ( - SELECT [i].[value] - FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i] -) +WHERE JSON_CONTAINS([p].[Ints], 10) = 1 """); } @@ -1240,10 +1237,7 @@ public override async Task Column_collection_of_nullable_ints_Contains() """ SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] -WHERE 10 IN ( - SELECT [n].[value] - FROM OPENJSON([p].[NullableInts]) WITH ([value] int '$') AS [n] -) +WHERE JSON_CONTAINS([p].[NullableInts], 10) = 1 """); } @@ -1297,10 +1291,7 @@ public override async Task Column_collection_of_bools_Contains() """ SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] -WHERE CAST(1 AS bit) IN ( - SELECT [b].[value] - FROM OPENJSON([p].[Bools]) WITH ([value] bit '$') AS [b] -) +WHERE JSON_CONTAINS([p].[Bools], CAST(1 AS bit)) = 1 """); } @@ -1509,7 +1500,7 @@ public override async Task Inline_collection_index_Column_with_EF_Constant() """ SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] -WHERE CAST(JSON_VALUE(N'[1,2,3]', '$[' + CAST([p].[Int] AS nvarchar(max)) + ']') AS int) = 1 +WHERE JSON_VALUE(CAST('[1,2,3]' AS json), '$[' + CAST([p].[Int] AS nvarchar(max)) + ']' RETURNING int) = 1 """); } @@ -1552,11 +1543,11 @@ public override async Task Parameter_collection_index_Column_equal_Column() AssertSql( """ -@ints='[0,2,3]' (Size = 4000) +@ints='[0,2,3]' (Size = 7) SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] -WHERE CAST(JSON_VALUE(@ints, '$[' + CAST([p].[Int] AS nvarchar(max)) + ']') AS int) = [p].[Int] +WHERE JSON_VALUE(@ints, '$[' + CAST([p].[Int] AS nvarchar(max)) + ']' RETURNING int) = [p].[Int] """); } @@ -1567,11 +1558,11 @@ public override async Task Parameter_collection_index_Column_equal_constant() AssertSql( """ -@ints='[1,2,3]' (Size = 4000) +@ints='[1,2,3]' (Size = 7) SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] -WHERE CAST(JSON_VALUE(@ints, '$[' + CAST([p].[Int] AS nvarchar(max)) + ']') AS int) = 1 +WHERE JSON_VALUE(@ints, '$[' + CAST([p].[Int] AS nvarchar(max)) + ']' RETURNING int) = 1 """); } @@ -1952,10 +1943,10 @@ public override async Task Parameter_collection_with_type_inference_for_JsonScal AssertSql( """ -@values='["one","two"]' (Size = 4000) +@values='["one","two"]' (Size = 13) SELECT CASE - WHEN [p].[Id] <> 0 THEN JSON_VALUE(@values, '$[' + CAST([p].[Int] % 2 AS nvarchar(max)) + ']') + WHEN [p].[Id] <> 0 THEN JSON_VALUE(@values, '$[' + CAST([p].[Int] % 2 AS nvarchar(max)) + ']' RETURNING nvarchar(max)) ELSE N'foo' END FROM [PrimitiveCollectionsEntity] AS [p] @@ -2053,7 +2044,7 @@ public override async Task Column_collection_equality_parameter_collection() AssertSql( """ -@ints='[1,10]' (Size = 8000) +@ints='[1,10]' (Size = 6) SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] @@ -2076,7 +2067,7 @@ public override async Task Column_collection_equality_inline_collection() """ SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] -WHERE CAST([p].[Ints] AS nvarchar(max)) = CAST('[1,10]' AS nvarchar(max)) +WHERE CAST([p].[Ints] AS nvarchar(max)) = N'[1,10]' """); } @@ -2571,7 +2562,7 @@ protected override ITestStoreFactory TestStoreFactory public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { var options = base.AddOptions(builder); - return options.UseSqlServerCompatibilityLevel(160); + return options.UseSqlServerCompatibilityLevel(170); } protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) @@ -2582,14 +2573,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { // Map DateTime to non-default datetime instead of the default datetime2 to exercise type mapping inference b.Property(p => p.DateTime).HasColumnType("datetime"); - b.PrimitiveCollection(e => e.Strings).HasColumnType("json"); - b.PrimitiveCollection(e => e.Ints).HasColumnType("json"); - b.PrimitiveCollection(e => e.DateTimes).HasColumnType("json"); - b.PrimitiveCollection(e => e.Bools).HasColumnType("json"); - b.PrimitiveCollection(e => e.Ints).HasColumnType("json"); - b.PrimitiveCollection(e => e.Enums).HasColumnType("json"); - b.PrimitiveCollection(e => e.NullableStrings).HasColumnType("json"); - b.PrimitiveCollection(e => e.NullableInts).HasColumnType("json"); }); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs index 33eb74beb23..90e5e937201 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs @@ -22,5 +22,5 @@ public enum SqlServerCondition SupportsFunctions2019 = 1 << 13, SupportsFunctions2022 = 1 << 14, SupportsJsonType = 1 << 15, - SupportsVectorType = 1 << 16, + SupportsVectorType = 1 << 16 } diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs index 6d25944439a..879095bb839 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs @@ -2535,31 +2535,31 @@ public override async Task Add_and_update_nested_optional_primitive_collection(b + parameterSize + @") @p1='7624' -@p2='[]' (Size = 8000) -@p3=NULL (Size = 8000) -@p4='[]' (Size = 8000) -@p5='[]' (Size = 8000) -@p6='[]' (Size = 8000) -@p7='[]' (Size = 8000) -@p8='[]' (Size = 8000) -@p9='[]' (Size = 8000) -@p10='[]' (Size = 8000) -@p11='[]' (Size = 8000) -@p12='[]' (Nullable = false) (Size = 8000) -@p13='[]' (Size = 8000) -@p14='[]' (Size = 8000) -@p15='[]' (Size = 8000) -@p16='[]' (Size = 8000) -@p17='[]' (Size = 8000) -@p18=NULL (Size = 8000) -@p19='[]' (Size = 8000) -@p20='[]' (Size = 8000) -@p21='[]' (Size = 8000) -@p22='[]' (Size = 8000) -@p23='[]' (Size = 8000) -@p24='[]' (Size = 8000) -@p25='[]' (Size = 8000) -@p26='[]' (Size = 8000) +@p2='[]' (Size = 2) +@p3=NULL +@p4='[]' (Size = 2) +@p5='[]' (Size = 2) +@p6='[]' (Size = 2) +@p7='[]' (Size = 2) +@p8='[]' (Size = 2) +@p9='[]' (Size = 2) +@p10='[]' (Size = 2) +@p11='[]' (Size = 2) +@p12='[]' (Nullable = false) (Size = 2) +@p13='[]' (Size = 2) +@p14='[]' (Size = 2) +@p15='[]' (Size = 2) +@p16='[]' (Size = 2) +@p17='[]' (Size = 2) +@p18=NULL +@p19='[]' (Size = 2) +@p20='[]' (Size = 2) +@p21='[]' (Size = 2) +@p22='[]' (Size = 2) +@p23='[]' (Size = 2) +@p24='[]' (Size = 2) +@p25='[]' (Size = 2) +@p26='[]' (Size = 2) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2579,8 +2579,8 @@ FROM [JsonEntitiesAllTypes] AS [j] SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; UPDATE [JsonEntitiesAllTypes] SET [Collection] = JSON_MODIFY([Collection], 'strict $[0].TestCharacterCollection', " -+ updatedP0Reference -+ @") + + updatedP0Reference + + @") OUTPUT 1 WHERE [Id] = @p1;", // @@ -2663,7 +2663,7 @@ public override async Task Edit_single_property_relational_collection_of_bool() AssertSql( """ @p1='1' -@p0='[true,true,false]' (Size = 8000) +@p0='[true,true,false]' (Size = 17) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2686,7 +2686,7 @@ public override async Task Edit_single_property_relational_collection_of_byte() AssertSql( """ @p1='1' -@p0='[25,26]' (Size = 8000) +@p0='[25,26]' (Size = 7) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2717,7 +2717,7 @@ public override async Task Edit_single_property_relational_collection_of_datetim AssertSql( """ @p1='1' -@p0='["2000-01-01T12:34:56","3000-01-01T12:34:56","3000-01-01T12:34:56"]' (Size = 8000) +@p0='["2000-01-01T12:34:56","3000-01-01T12:34:56","3000-01-01T12:34:56"]' (Size = 67) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2740,7 +2740,7 @@ public override async Task Edit_single_property_relational_collection_of_datetim AssertSql( """ @p1='1' -@p0='["3000-01-01T12:34:56-04:00"]' (Size = 8000) +@p0='["3000-01-01T12:34:56-04:00"]' (Size = 29) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2763,7 +2763,7 @@ public override async Task Edit_single_property_relational_collection_of_decimal AssertSql( """ @p1='1' -@p0='[-13579.01]' (Size = 8000) +@p0='[-13579.01]' (Size = 11) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2786,7 +2786,7 @@ public override async Task Edit_single_property_relational_collection_of_double( AssertSql( """ @p1='1' -@p0='[-1.23456789,1.23456789,0,-1.23579]' (Size = 8000) +@p0='[-1.23456789,1.23456789,0,-1.23579]' (Size = 35) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2809,7 +2809,7 @@ public override async Task Edit_single_property_relational_collection_of_guid() AssertSql( """ @p1='1' -@p0='["12345678-1234-4321-5555-987654321000"]' (Nullable = false) (Size = 8000) +@p0='["12345678-1234-4321-5555-987654321000"]' (Nullable = false) (Size = 40) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2832,7 +2832,7 @@ public override async Task Edit_single_property_relational_collection_of_int16() AssertSql( """ @p1='1' -@p0='[-3234]' (Size = 8000) +@p0='[-3234]' (Size = 7) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2855,7 +2855,7 @@ public override async Task Edit_single_property_relational_collection_of_int32() AssertSql( """ @p1='1' -@p0='[-3234]' (Size = 8000) +@p0='[-3234]' (Size = 7) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2878,7 +2878,7 @@ public override async Task Edit_single_property_relational_collection_of_int64() AssertSql( """ @p1='1' -@p0='[]' (Size = 8000) +@p0='[]' (Size = 2) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2901,7 +2901,7 @@ public override async Task Edit_single_property_relational_collection_of_signed_ AssertSql( """ @p1='1' -@p0='[-108]' (Size = 8000) +@p0='[-108]' (Size = 6) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2924,7 +2924,7 @@ public override async Task Edit_single_property_relational_collection_of_single( AssertSql( """ @p1='1' -@p0='[0,-1.234]' (Size = 8000) +@p0='[0,-1.234]' (Size = 10) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2947,7 +2947,7 @@ public override async Task Edit_single_property_relational_collection_of_timespa AssertSql( """ @p1='1' -@p0='["10:01:01.007","7:09:08.007"]' (Size = 8000) +@p0='["10:01:01.007","7:09:08.007"]' (Size = 30) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2970,7 +2970,7 @@ public override async Task Edit_single_property_relational_collection_of_uint16( AssertSql( """ @p1='1' -@p0='[1534]' (Size = 8000) +@p0='[1534]' (Size = 6) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -2993,7 +2993,7 @@ public override async Task Edit_single_property_relational_collection_of_uint32( AssertSql( """ @p1='1' -@p0='[1237775789]' (Size = 8000) +@p0='[1237775789]' (Size = 12) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3016,7 +3016,7 @@ public override async Task Edit_single_property_relational_collection_of_uint64( AssertSql( """ @p1='1' -@p0='[1234555555123456789]' (Size = 8000) +@p0='[1234555555123456789]' (Size = 21) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3039,7 +3039,7 @@ public override async Task Edit_single_property_relational_collection_of_nullabl AssertSql( """ @p1='1' -@p0='[null,-2147483648,0,null,2147483647,null,77,null]' (Size = 8000) +@p0='[null,-2147483648,0,null,2147483647,null,77,null]' (Size = 49) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3062,7 +3062,7 @@ public override async Task Edit_single_property_relational_collection_of_nullabl AssertSql( """ @p1='1' -@p0=NULL (Size = 8000) +@p0=NULL SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3085,7 +3085,7 @@ public override async Task Edit_single_property_relational_collection_of_enum() AssertSql( """ @p1='1' -@p0='[-3]' (Size = 8000) +@p0='[-3]' (Size = 4) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3108,7 +3108,7 @@ public override async Task Edit_single_property_relational_collection_of_enum_wi AssertSql( """ @p1='1' -@p0='[-3]' (Size = 8000) +@p0='[-3]' (Size = 4) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3131,7 +3131,7 @@ public override async Task Edit_single_property_relational_collection_of_nullabl AssertSql( """ @p1='1' -@p0='[-3]' (Size = 8000) +@p0='[-3]' (Size = 4) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3154,7 +3154,7 @@ public override async Task Edit_single_property_relational_collection_of_nullabl AssertSql( """ @p1='1' -@p0=NULL (Size = 8000) +@p0=NULL SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3177,7 +3177,7 @@ public override async Task Edit_single_property_relational_collection_of_nullabl AssertSql( """ @p1='1' -@p0='[-1,-3,-7,2]' (Size = 8000) +@p0='[-1,-3,-7,2]' (Size = 12) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3200,7 +3200,7 @@ public override async Task Edit_single_property_relational_collection_of_nullabl AssertSql( """ @p1='1' -@p0=NULL (Size = 8000) +@p0=NULL SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3223,7 +3223,7 @@ public override async Task Edit_single_property_relational_collection_of_nullabl AssertSql( """ @p1='1' -@p0='[-1]' (Size = 8000) +@p0='[-1]' (Size = 4) SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; @@ -3246,7 +3246,7 @@ public override async Task Edit_single_property_relational_collection_of_nullabl AssertSql( """ @p1='1' -@p0=NULL (Size = 8000) +@p0=NULL SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON;