diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index edbd7b8d778..f260058111b 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Internal; @@ -627,7 +628,12 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp // 3. Can do JSON-specific decoding (e.g. base64 for varbinary) // Note that RETURNING is only (currently) supported over the json type (not nvarchar(max)). // Note that we don't need to check the compatibility level - if the json type is being used, then RETURNING is supported. - var useJsonValueReturningClause = !jsonQuery && jsonScalarExpression.Json.TypeMapping?.StoreType is "json"; + var useJsonValueReturningClause = !jsonQuery + && jsonScalarExpression.Json.TypeMapping?.StoreType is "json" + // The following types aren't supported by the JSON_VALUE() RETURNING clause (#36627). + // Note that for varbinary we already transform the JSON_VALUE() into OPENJSON() earlier, in SqlServerJsonPostprocessor. + && jsonScalarExpression.TypeMapping?.StoreType.ToLower(CultureInfo.InvariantCulture) + is not ("uniqueidentifier" or "geometry" or "geography"); // For JSON_VALUE(), if we can use the RETURNING clause, always do that. // Otherwise, JSON_VALUE always returns nvarchar(4000) (https://learn.microsoft.com/sql/t-sql/functions/json-value-transact-sql), diff --git a/test/EFCore.Relational.Specification.Tests/Types/RelationalTypeFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Types/RelationalTypeFixtureBase.cs index 4d0565cf5b9..5396b5bfda1 100644 --- a/test/EFCore.Relational.Specification.Tests/Types/RelationalTypeFixtureBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Types/RelationalTypeFixtureBase.cs @@ -22,14 +22,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { b.ToTable(nameof(JsonTypeEntity<>)); - modelBuilder.Entity>().Property(e => e.Id).ValueGeneratedNever(); - - b.ComplexProperty(e => e.JsonContainer, jc => + modelBuilder.Entity>(b => { - jc.ToJson(); + b.Property(e => e.Id).ValueGeneratedNever(); + + b.Property(e => e.Value).HasColumnType(StoreType); + b.Property(e => e.OtherValue).HasColumnType(StoreType); + + b.ComplexProperty(e => e.JsonContainer, jc => + { + jc.ToJson(); - jc.Property(e => e.Value).HasColumnType(StoreType); - jc.Property(e => e.OtherValue).HasColumnType(StoreType); + jc.Property(e => e.Value).HasColumnType(StoreType); + jc.Property(e => e.OtherValue).HasColumnType(StoreType); + }); }); }); } diff --git a/test/EFCore.Relational.Specification.Tests/Types/RelationalTypeTestBase.cs b/test/EFCore.Relational.Specification.Tests/Types/RelationalTypeTestBase.cs index 5a506bdbab1..f5f15e5f0df 100644 --- a/test/EFCore.Relational.Specification.Tests/Types/RelationalTypeTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Types/RelationalTypeTestBase.cs @@ -14,7 +14,7 @@ public RelationalTypeTestBase(TFixture fixture, ITestOutputHelper testOutputHelp Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - #region SaveChanges + #region JSON [ConditionalFact] public virtual async Task SaveChanges_within_json() @@ -39,9 +39,17 @@ public virtual async Task SaveChanges_within_json() } }); - #endregion SaveChanges + [ConditionalFact] + public virtual async Task Query_property_within_json() + { + await using var context = Fixture.CreateContext(); + + Fixture.TestSqlLoggerFactory.Clear(); + + var result = await context.Set>().Where(e => e.JsonContainer.Value.Equals(Fixture.Value)).SingleAsync(); - #region ExecuteUpdate + Assert.Equal(Fixture.Value, result.JsonContainer.Value, Fixture.Comparer); + } [ConditionalFact] public virtual async Task ExecuteUpdate_within_json_to_parameter() @@ -121,7 +129,7 @@ public virtual async Task ExecuteUpdate_within_json_to_nonjson_column() } }); - #endregion ExecuteUpdate + #endregion JSON protected void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs index f38e57a7dc8..71b9815e0f3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs @@ -2272,14 +2272,13 @@ FROM [JsonEntitiesAllTypes] AS [j] """); } - [ConditionalTheory(Skip = "#36627")] public override async Task Json_all_types_projection_individual_properties(bool async) { await base.Json_all_types_projection_individual_properties(async); AssertSql( - """ -SELECT JSON_VALUE([j].[Reference], '$.TestDefaultString') AS [TestDefaultString], JSON_VALUE([j].[Reference], '$.TestMaxLengthString') AS [TestMaxLengthString], CAST(JSON_VALUE([j].[Reference], '$.TestBoolean') AS bit) AS [TestBoolean], CAST(JSON_VALUE([j].[Reference], '$.TestByte') AS tinyint) AS [TestByte], JSON_VALUE([j].[Reference], '$.TestCharacter') AS [TestCharacter], CAST(JSON_VALUE([j].[Reference], '$.TestDateTime') AS datetime2) AS [TestDateTime], CAST(JSON_VALUE([j].[Reference], '$.TestDateTimeOffset') AS datetimeoffset) AS [TestDateTimeOffset], CAST(JSON_VALUE([j].[Reference], '$.TestDecimal') AS decimal(18,3)) AS [TestDecimal], CAST(JSON_VALUE([j].[Reference], '$.TestDouble') AS float) AS [TestDouble], CAST(JSON_VALUE([j].[Reference], '$.TestGuid') AS uniqueidentifier) AS [TestGuid], CAST(JSON_VALUE([j].[Reference], '$.TestInt16') AS smallint) AS [TestInt16], CAST(JSON_VALUE([j].[Reference], '$.TestInt32') AS int) AS [TestInt32], CAST(JSON_VALUE([j].[Reference], '$.TestInt64') AS bigint) AS [TestInt64], CAST(JSON_VALUE([j].[Reference], '$.TestSignedByte') AS smallint) AS [TestSignedByte], CAST(JSON_VALUE([j].[Reference], '$.TestSingle') AS real) AS [TestSingle], CAST(JSON_VALUE([j].[Reference], '$.TestTimeSpan') AS time) AS [TestTimeSpan], CAST(JSON_VALUE([j].[Reference], '$.TestDateOnly') AS date) AS [TestDateOnly], CAST(JSON_VALUE([j].[Reference], '$.TestTimeOnly') AS time) AS [TestTimeOnly], CAST(JSON_VALUE([j].[Reference], '$.TestUnsignedInt16') AS int) AS [TestUnsignedInt16], CAST(JSON_VALUE([j].[Reference], '$.TestUnsignedInt32') AS bigint) AS [TestUnsignedInt32], CAST(JSON_VALUE([j].[Reference], '$.TestUnsignedInt64') AS decimal(20,0)) AS [TestUnsignedInt64], CAST(JSON_VALUE([j].[Reference], '$.TestEnum') AS int) AS [TestEnum], CAST(JSON_VALUE([j].[Reference], '$.TestEnumWithIntConverter') AS int) AS [TestEnumWithIntConverter], CAST(JSON_VALUE([j].[Reference], '$.TestNullableEnum') AS int) AS [TestNullableEnum], CAST(JSON_VALUE([j].[Reference], '$.TestNullableEnumWithIntConverter') AS int) AS [TestNullableEnumWithIntConverter], JSON_VALUE([j].[Reference], '$.TestNullableEnumWithConverterThatHandlesNulls') AS [TestNullableEnumWithConverterThatHandlesNulls] +""" +SELECT JSON_VALUE([j].[Reference], '$.TestDefaultString' RETURNING nvarchar(max)) AS [TestDefaultString], JSON_VALUE([j].[Reference], '$.TestMaxLengthString' RETURNING nvarchar(5)) AS [TestMaxLengthString], JSON_VALUE([j].[Reference], '$.TestBoolean' RETURNING bit) AS [TestBoolean], JSON_VALUE([j].[Reference], '$.TestByte' RETURNING tinyint) AS [TestByte], JSON_VALUE([j].[Reference], '$.TestCharacter' RETURNING nvarchar(1)) AS [TestCharacter], JSON_VALUE([j].[Reference], '$.TestDateTime' RETURNING datetime2) AS [TestDateTime], JSON_VALUE([j].[Reference], '$.TestDateTimeOffset' RETURNING datetimeoffset) AS [TestDateTimeOffset], JSON_VALUE([j].[Reference], '$.TestDecimal' RETURNING decimal(18,3)) AS [TestDecimal], JSON_VALUE([j].[Reference], '$.TestDouble' RETURNING float) AS [TestDouble], CAST(JSON_VALUE([j].[Reference], '$.TestGuid') AS uniqueidentifier) AS [TestGuid], JSON_VALUE([j].[Reference], '$.TestInt16' RETURNING smallint) AS [TestInt16], JSON_VALUE([j].[Reference], '$.TestInt32' RETURNING int) AS [TestInt32], JSON_VALUE([j].[Reference], '$.TestInt64' RETURNING bigint) AS [TestInt64], JSON_VALUE([j].[Reference], '$.TestSignedByte' RETURNING smallint) AS [TestSignedByte], JSON_VALUE([j].[Reference], '$.TestSingle' RETURNING real) AS [TestSingle], JSON_VALUE([j].[Reference], '$.TestTimeSpan' RETURNING time) AS [TestTimeSpan], JSON_VALUE([j].[Reference], '$.TestDateOnly' RETURNING date) AS [TestDateOnly], JSON_VALUE([j].[Reference], '$.TestTimeOnly' RETURNING time) AS [TestTimeOnly], JSON_VALUE([j].[Reference], '$.TestUnsignedInt16' RETURNING int) AS [TestUnsignedInt16], JSON_VALUE([j].[Reference], '$.TestUnsignedInt32' RETURNING bigint) AS [TestUnsignedInt32], JSON_VALUE([j].[Reference], '$.TestUnsignedInt64' RETURNING decimal(20,0)) AS [TestUnsignedInt64], JSON_VALUE([j].[Reference], '$.TestEnum' RETURNING int) AS [TestEnum], JSON_VALUE([j].[Reference], '$.TestEnumWithIntConverter' RETURNING int) AS [TestEnumWithIntConverter], JSON_VALUE([j].[Reference], '$.TestNullableEnum' RETURNING int) AS [TestNullableEnum], JSON_VALUE([j].[Reference], '$.TestNullableEnumWithIntConverter' RETURNING int) AS [TestNullableEnumWithIntConverter], JSON_VALUE([j].[Reference], '$.TestNullableEnumWithConverterThatHandlesNulls' RETURNING nvarchar(max)) AS [TestNullableEnumWithConverterThatHandlesNulls] FROM [JsonEntitiesAllTypes] AS [j] """); } @@ -2478,7 +2477,6 @@ WHERE JSON_VALUE([j].[Reference], '$.TestEnumWithIntConverter' RETURNING int) <> """); } - [ConditionalTheory(Skip = "#36627")] public override async Task Json_predicate_on_guid(bool async) { await base.Json_predicate_on_guid(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerGeographyTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerGeographyTypeTest.cs index f24c152ca18..04268d750f5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerGeographyTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerGeographyTypeTest.cs @@ -19,6 +19,18 @@ public override async Task Equality_in_query() Assert.Equal(Fixture.Value, result.Value, Fixture.Comparer); } + // SQL Server doesn't support the equality operator on geometry, override to use EqualsTopologically + public override async Task Query_property_within_json() + { + await using var context = Fixture.CreateContext(); + + Fixture.TestSqlLoggerFactory.Clear(); + + var result = await context.Set>().Where(e => e.JsonContainer.Value.EqualsTopologically(Fixture.Value)).SingleAsync(); + + Assert.Equal(Fixture.Value, result.JsonContainer.Value, Fixture.Comparer); + } + public override async Task ExecuteUpdate_within_json_to_nonjson_column() { await base.ExecuteUpdate_within_json_to_nonjson_column(); @@ -55,6 +67,22 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build public class PointTypeTest(PointTypeTest.PointTypeFixture fixture) : GeographyTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0xE6100000010CC9772975C9CF4740DCF4673F52965EC0' (Size = 22) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geography).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class PointTypeFixture() : GeographyTypeFixture { public override Point Value { get; } = new(-122.34877, 47.6233355) { SRID = 4326 }; @@ -65,6 +93,22 @@ public class PointTypeFixture() : GeographyTypeFixture public class LineStringTypeTest(LineStringTypeTest.LineStringTypeFixture fixture) : GeographyTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0xE61000000114C9772975C9CF4740DCF4673F52965EC0AAD2BB1D86CC47407854...' (Size = 38) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geography).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class LineStringTypeFixture() : GeographyTypeFixture { public override LineString Value { get; } = new( @@ -87,6 +131,22 @@ public class LineStringTypeFixture() : GeographyTypeFixture public class PolygonTypeTest(PolygonTypeTest.PolygonTypeFixture fixture) : GeographyTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0xE61000000104050000008FC2F5285CCF47406666666666965EC0AE47E17A14CE...' (Size = 112) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geography).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class PolygonTypeFixture() : GeographyTypeFixture { // Simple rectangle @@ -116,6 +176,22 @@ public class PolygonTypeFixture() : GeographyTypeFixture public class MultiPointTypeTest(MultiPointTypeTest.MultiPointTypeFixture fixture) : GeographyTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0xE61000000104020000008FC2F5285CCF47406666666666965EC01F85EB51B8CE...' (Size = 87) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geography).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class MultiPointTypeFixture() : GeographyTypeFixture { public override MultiPoint Value { get; } = new([ @@ -136,6 +212,22 @@ public class MultiPointTypeFixture() : GeographyTypeFixture public class MultiLineStringTypeTest(MultiLineStringTypeTest.MultiLineStringTypeFixture fixture) : GeographyTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0xE61000000104040000008FC2F5285CCF47406666666666965EC01F85EB51B8CE...' (Size = 119) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geography).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class MultiLineStringTypeFixture() : GeographyTypeFixture { public override MultiLineString Value { get; } = new([ @@ -167,6 +259,22 @@ public class MultiLineStringTypeFixture() : GeographyTypeFixture public class MultiPolygonTypeTest(MultiPolygonTypeTest.MultiPolygonTypeFixture fixture) : GeographyTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0xE610000001040A0000008FC2F5285CCF47406666666666965EC01F85EB51B8CE...' (Size = 215) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geography).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class MultiPolygonTypeFixture() : GeographyTypeFixture { public override MultiPolygon Value { get; } = new( @@ -212,6 +320,22 @@ public class MultiPolygonTypeFixture() : GeographyTypeFixture public class GeometryCollectionTypeTest(GeometryCollectionTypeTest.GeometryCollectionTypeFixture fixture) : GeographyTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0xE61000000104080000008FC2F5285CCF47406666666666965EC08FC2F5285CCF...' (Size = 197) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geography).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class GeometryCollectionTypeFixture() : GeographyTypeFixture { public override GeometryCollection Value { get; } = new( diff --git a/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerGeometryTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerGeometryTypeTest.cs index a9a04cc2342..454c6facb09 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerGeometryTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerGeometryTypeTest.cs @@ -15,11 +15,25 @@ public override async Task Equality_in_query() { await using var context = Fixture.CreateContext(); + Fixture.TestSqlLoggerFactory.Clear(); + var result = await context.Set>().Where(e => e.Value.EqualsTopologically(Fixture.Value)).SingleAsync(); Assert.Equal(Fixture.Value, result.Value, Fixture.Comparer); } + // SQL Server doesn't support the equality operator on geometry, override to use EqualsTopologically + public override async Task Query_property_within_json() + { + await using var context = Fixture.CreateContext(); + + Fixture.TestSqlLoggerFactory.Clear(); + + var result = await context.Set>().Where(e => e.JsonContainer.Value.EqualsTopologically(Fixture.Value)).SingleAsync(); + + Assert.Equal(Fixture.Value, result.JsonContainer.Value, Fixture.Comparer); + } + public override async Task ExecuteUpdate_within_json_to_nonjson_column() { await base.ExecuteUpdate_within_json_to_nonjson_column(); @@ -58,157 +72,269 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build public class PointTypeTest(PointTypeTest.PointTypeFixture fixture, ITestOutputHelper testOutputHelper) : GeometryTypeTestBase(fixture, testOutputHelper) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0x00000000010C00000000000024400000000000003440' (Size = 22) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geometry).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class PointTypeFixture() : GeometryTypeFixture { - public override Point Value { get; } = new(-122.34877, 47.6233355) { SRID = 4326 }; - public override Point OtherValue { get; } = new(-121.7500, 46.2500) { SRID = 4326 }; + public override Point Value { get; } = new(10, 20); + public override Point OtherValue { get; } = new(30, 40); } } public class LineStringTypeTest(LineStringTypeTest.LineStringTypeFixture fixture, ITestOutputHelper testOutputHelper) : GeometryTypeTestBase(fixture, testOutputHelper) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0x000000000114000000000000244000000000000034400000000000002E400000...' (Size = 38) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geometry).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class LineStringTypeFixture() : GeometryTypeFixture { public override LineString Value { get; } = new( [ - new Coordinate(-122.34877, 47.6233355), - new Coordinate(-122.3308366, 47.5978429) - ]) { SRID = 4326 }; + new Coordinate(10, 20), + new Coordinate(15, 25) + ]); public override LineString OtherValue { get; } = new( [ - new Coordinate(-120.5000, 46.9000), - new Coordinate(-119.8000, 46.7000), - new Coordinate(-118.6000, 46.4000) - ]) { SRID = 4326 }; + new Coordinate(30, 40), + new Coordinate(35, 45), + new Coordinate(40, 50) + ]); } } public class PolygonTypeTest(PolygonTypeTest.PolygonTypeFixture fixture, ITestOutputHelper testOutputHelper) : GeometryTypeTestBase(fixture, testOutputHelper) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0x0000000001040500000000000000000000000000000000000000000000000000...' (Size = 112) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geometry).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class PolygonTypeFixture() : GeometryTypeFixture { public override Polygon Value { get; } = new( new LinearRing( [ - new Coordinate(-122.3500, 47.6200), // NW - new Coordinate(-122.3500, 47.6100), // SW - new Coordinate(-122.3400, 47.6100), // SE - new Coordinate(-122.3400, 47.6200), // NE - new Coordinate(-122.3500, 47.6200) - ])) { SRID = 4326 }; + new Coordinate(0, 0), // NW + new Coordinate(0, 10), // SW + new Coordinate(10, 10), // SE + new Coordinate(10, 0), // NE + new Coordinate(0, 0) + ])); public override Polygon OtherValue { get; } = new( new LinearRing( [ - new Coordinate(-119.3000, 45.8800), // NW - new Coordinate(-119.3000, 45.8600), // SW - new Coordinate(-119.1500, 45.8600), // SE - new Coordinate(-119.1500, 45.8800), // NE - new Coordinate(-119.3000, 45.8800) - ])) { SRID = 4326 }; + new Coordinate(20, 20), // NW + new Coordinate(20, 30), // SW + new Coordinate(30, 30), // SE + new Coordinate(30, 20), // NE + new Coordinate(20, 20) + ])); } } public class MultiPointTypeTest(MultiPointTypeTest.MultiPointTypeFixture fixture, ITestOutputHelper testOutputHelper) : GeometryTypeTestBase(fixture, testOutputHelper) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0x0000000001040200000000000000000014400000000000001440000000000000...' (Size = 87) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geometry).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class MultiPointTypeFixture() : GeometryTypeFixture { public override MultiPoint Value { get; } = new MultiPoint( [ - new Point(-122.3500, 47.6200) { SRID = 4326 }, - new Point(-122.3450, 47.6150) { SRID = 4326 } - ]) { SRID = 4326 }; + new Point(5, 5), + new Point(10, 10) + ]); public override MultiPoint OtherValue { get; } = new MultiPoint( [ - new Point(-121.9000, 46.9500) { SRID = 4326 }, - new Point(-121.5000, 46.6000) { SRID = 4326 }, - new Point(-121.2000, 46.3000) { SRID = 4326 } - ]) { SRID = 4326 }; + new Point(15, 15), + new Point(20, 20), + new Point(25, 25) + ]); } } public class MultiLineStringTypeTest(MultiLineStringTypeTest.MultiLineStringTypeFixture fixture, ITestOutputHelper testOutputHelper) : GeometryTypeTestBase(fixture, testOutputHelper) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0x00000000010404000000000000000000F03F000000000000F03F000000000000...' (Size = 119) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geometry).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class MultiLineStringTypeFixture() : GeometryTypeFixture { public override MultiLineString Value { get; } = new MultiLineString( [ new LineString([ - new Coordinate(-122.3500, 47.6200), - new Coordinate(-122.3450, 47.6150) - ]) { SRID = 4326 }, + new Coordinate(1, 1), + new Coordinate(2, 2) + ]), new LineString([ - new Coordinate(-122.3480, 47.6180), - new Coordinate(-122.3420, 47.6130) - ]) { SRID = 4326 } - ]) { SRID = 4326 }; + new Coordinate(3, 3), + new Coordinate(4, 4) + ]) + ]); public override MultiLineString OtherValue { get; } = new MultiLineString( [ new LineString([ - new Coordinate(-120.9000, 46.9500), - new Coordinate(-120.4000, 46.8200) - ]) { SRID = 4326 }, + new Coordinate(10, 10), + new Coordinate(11, 11) + ]), new LineString([ - new Coordinate(-120.7000, 46.7800), - new Coordinate(-120.2000, 46.5500) - ]) { SRID = 4326 } - ]) { SRID = 4326 }; + new Coordinate(12, 12), + new Coordinate(13, 13) + ]) + ]); } } public class MultiPolygonTypeTest(MultiPolygonTypeTest.MultiPolygonTypeFixture fixture, ITestOutputHelper testOutputHelper) : GeometryTypeTestBase(fixture, testOutputHelper) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0x0000000001040A00000000000000000000000000000000000000000000000000...' (Size = 215) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geometry).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public class MultiPolygonTypeFixture() : GeometryTypeFixture { public override MultiPolygon Value { get; } = new MultiPolygon( [ new Polygon(new LinearRing([ - new Coordinate(-122.3500, 47.6200), // NW - new Coordinate(-122.3500, 47.6150), // SW - new Coordinate(-122.3450, 47.6150), // SE - new Coordinate(-122.3450, 47.6200), // NE - new Coordinate(-122.3500, 47.6200) - ])) { SRID = 4326 }, + new Coordinate(0, 0), // NW + new Coordinate(0, 5), // SW + new Coordinate(5, 5), // SE + new Coordinate(5, 0), // NE + new Coordinate(0, 0) + ])), new Polygon(new LinearRing([ - new Coordinate(-122.3400, 47.6240), // NW - new Coordinate(-122.3400, 47.6220), // SW - new Coordinate(-122.3380, 47.6220), // SE - new Coordinate(-122.3380, 47.6240), // NE - new Coordinate(-122.3400, 47.6240) - ])) { SRID = 4326 } - ]) { SRID = 4326 }; + new Coordinate(10, 10), // NW + new Coordinate(10, 15), // SW + new Coordinate(15, 15), // SE + new Coordinate(15, 10), // NE + new Coordinate(10, 10) + ])) + ]); public override MultiPolygon OtherValue { get; } = new MultiPolygon( [ new Polygon(new LinearRing([ - new Coordinate(-119.8000, 45.9000), // NW - new Coordinate(-119.8000, 45.8800), // SW - new Coordinate(-119.6500, 45.8800), // SE - new Coordinate(-119.6500, 45.9000), // NE - new Coordinate(-119.8000, 45.9000) - ])) { SRID = 4326 }, + new Coordinate(20, 20), // NW + new Coordinate(20, 25), // SW + new Coordinate(25, 25), // SE + new Coordinate(25, 20), // NE + new Coordinate(20, 20) + ])), new Polygon(new LinearRing([ - new Coordinate(-119.6000, 45.8950), // NW - new Coordinate(-119.6000, 45.8850), // SW - new Coordinate(-119.5800, 45.8850), // SE - new Coordinate(-119.5800, 45.8950), // NE - new Coordinate(-119.6000, 45.8950) - ])) { SRID = 4326 } - ]) { SRID = 4326 }; + new Coordinate(30, 30), // NW + new Coordinate(30, 35), // SW + new Coordinate(35, 35), // SE + new Coordinate(35, 30), // NE + new Coordinate(30, 30) + ])) + ]); } } public class GeometryCollectionTypeTest(GeometryCollectionTypeTest.GeometryCollectionTypeFixture fixture, ITestOutputHelper testOutputHelper) : GeometryTypeTestBase(fixture, testOutputHelper) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with geometry even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='0x00000000010408000000000000000000F03F000000000000F03F000000000000...' (Size = 197) (DbType = Object) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS geometry).STEquals(@Fixture_Value) = CAST(1 AS bit) +"""); + } + public override async Task ExecuteUpdate_within_json_to_constant() { await base.ExecuteUpdate_within_json_to_constant(); @@ -218,7 +344,7 @@ public override async Task ExecuteUpdate_within_json_to_constant() AssertSql( """ UPDATE [j] -SET [JsonContainer].modify('$.Value', N'GEOMETRYCOLLECTION (POINT (-120.9 46.95), LINESTRING (-120.9 46.95, -120.4 46.82), POLYGON ((-120.8 46.94, -120.8 46.92, -120.78 46.92, -120.78 46.94, -120.8 46.94)))') +SET [JsonContainer].modify('$.Value', N'GEOMETRYCOLLECTION (POINT (10 10), LINESTRING (11 11, 12 12), POLYGON ((13 13, 13 15, 15 15, 15 13, 13 13)))') FROM [JsonTypeEntity] AS [j] """); } @@ -227,7 +353,7 @@ FROM [JsonTypeEntity] AS [j] AssertSql( """ UPDATE [j] -SET [j].[JsonContainer] = JSON_MODIFY([j].[JsonContainer], '$.Value', N'GEOMETRYCOLLECTION (POINT (-120.9 46.95), LINESTRING (-120.9 46.95, -120.4 46.82), POLYGON ((-120.8 46.94, -120.8 46.92, -120.78 46.92, -120.78 46.94, -120.8 46.94)))') +SET [j].[JsonContainer] = JSON_MODIFY([j].[JsonContainer], '$.Value', N'GEOMETRYCOLLECTION (POINT (10 10), LINESTRING (11 11, 12 12), POLYGON ((13 13, 13 15, 15 15, 15 13, 13 13)))') FROM [JsonTypeEntity] AS [j] """); } @@ -237,36 +363,34 @@ public class GeometryCollectionTypeFixture() : GeometryTypeFixture { public override GeometryCollection Value { get; } = new GeometryCollection( [ - new Point(-122.3500, 47.6200) { SRID = 4326 }, + new Point(1, 1), new LineString([ - new Coordinate(-122.3500, 47.6200), - new Coordinate(-122.3450, 47.6150) - ]) { SRID = 4326 }, + new Coordinate(2, 2), + new Coordinate(3, 3) + ]), new Polygon(new LinearRing([ - new Coordinate(-122.3480, 47.6190), // NW - new Coordinate(-122.3480, 47.6170), // SW - new Coordinate(-122.3460, 47.6170), // SE - new Coordinate(-122.3460, 47.6190), // NE - new Coordinate(-122.3480, 47.6190) - ])) { SRID = 4326 } - ]) - { SRID = 4326 }; + new Coordinate(4, 4), // NW + new Coordinate(4, 6), // SW + new Coordinate(6, 6), // SE + new Coordinate(6, 4), // NE + new Coordinate(4, 4) + ])) + ]); public override GeometryCollection OtherValue { get; } = new GeometryCollection( [ - new Point(-120.9000, 46.9500) { SRID = 4326 }, + new Point(10, 10), new LineString([ - new Coordinate(-120.9000, 46.9500), - new Coordinate(-120.4000, 46.8200) - ]) { SRID = 4326 }, + new Coordinate(11, 11), + new Coordinate(12, 12) + ]), new Polygon(new LinearRing([ - new Coordinate(-120.8000, 46.9400), // NW - new Coordinate(-120.8000, 46.9200), // SW - new Coordinate(-120.7800, 46.9200), // SE - new Coordinate(-120.7800, 46.9400), // NE - new Coordinate(-120.8000, 46.9400) - ])) { SRID = 4326 } - ]) - { SRID = 4326 }; + new Coordinate(13, 13), // NW + new Coordinate(13, 15), // SW + new Coordinate(15, 15), // SE + new Coordinate(15, 13), // NE + new Coordinate(13, 13) + ])) + ]); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerMiscellaneousTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerMiscellaneousTypeTest.cs index a465ae4405d..ed7af56f884 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerMiscellaneousTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerMiscellaneousTypeTest.cs @@ -6,6 +6,34 @@ namespace Microsoft.EntityFrameworkCore.Types.Miscellaneous; public class BoolTypeTest(BoolTypeTest.BoolTypeFixture fixture) : RelationalTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + if (Fixture.UsingJsonType) + { + AssertSql( + """ +@Fixture_Value='True' + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE JSON_VALUE([j].[JsonContainer], '$.Value' RETURNING bit) = @Fixture_Value +"""); + } + else + { + AssertSql( + """ +@Fixture_Value='True' + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS bit) = @Fixture_Value +"""); + } + } + public class BoolTypeFixture : SqlServerTypeFixture { public override bool Value { get; } = true; @@ -18,6 +46,34 @@ public class BoolTypeFixture : SqlServerTypeFixture public class StringTypeTest(StringTypeTest.StringTypeFixture fixture) : RelationalTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + if (Fixture.UsingJsonType) + { + AssertSql( + """ +@Fixture_Value='foo' (Size = 4000) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE JSON_VALUE([j].[JsonContainer], '$.Value' RETURNING nvarchar(max)) = @Fixture_Value +"""); + } + else + { + AssertSql( + """ +@Fixture_Value='foo' (Size = 4000) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE JSON_VALUE([j].[JsonContainer], '$.Value') = @Fixture_Value +"""); + } + } + public class StringTypeFixture : SqlServerTypeFixture { public override string Value { get; } = "foo"; @@ -30,6 +86,22 @@ public class StringTypeFixture : SqlServerTypeFixture public class GuidTypeTest(GuidTypeTest.GuidTypeFixture fixture) : RelationalTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with uniqueidentifier even on SQL Server 2025, as that type isn't + // supported (#36627). + AssertSql( + """ +@Fixture_Value='8f7331d6-cde9-44fb-8611-81fff686f280' + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +WHERE CAST(JSON_VALUE([j].[JsonContainer], '$.Value') AS uniqueidentifier) = @Fixture_Value +"""); + } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] public override async Task ExecuteUpdate_within_json_to_nonjson_column() { @@ -59,7 +131,6 @@ UPDATE [j] FROM [JsonTypeEntity] AS [j] """); } - } public class GuidTypeFixture : SqlServerTypeFixture @@ -77,6 +148,25 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build public class ByteArrayTypeTest(ByteArrayTypeTest.ByteArrayTypeFixture fixture) : RelationalTypeTestBase(fixture) { + public override async Task Query_property_within_json() + { + await base.Query_property_within_json(); + + // Note that the JSON_VALUE RETURNING clause is never used with varbinary even on SQL Server 2025, as that type isn't supported + // (#36627). + // We also can't just wrap JSON_VALUE() with CAST(... AS varbinary(max)), as that would apply a SQL Server binary format + // conversion, and not base64. So we use OPENJSON which does perform base64 conversion. + AssertSql( + """ +@Fixture_Value='0x010203' (Size = 8000) + +SELECT TOP(2) [j].[Id], [j].[OtherValue], [j].[Value], [j].[JsonContainer] +FROM [JsonTypeEntity] AS [j] +OUTER APPLY OPENJSON([j].[JsonContainer]) WITH ([Value] varbinary(max) '$.Value') AS [v] +WHERE [v].[Value] = @Fixture_Value +"""); + } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] public override async Task ExecuteUpdate_within_json_to_nonjson_column() { diff --git a/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerTemporalTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerTemporalTypeTest.cs index 4ca3f0f049e..05405f96acb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerTemporalTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Types/SqlServerTemporalTypeTest.cs @@ -79,10 +79,10 @@ FROM [JsonTypeEntity] AS [j] { AssertSql( """ - UPDATE [j] - SET [j].[JsonContainer] = JSON_MODIFY([j].[JsonContainer], '$.Value', JSON_VALUE(JSON_OBJECT('v': [j].[OtherValue]), '$.v')) - FROM [JsonTypeEntity] AS [j] - """); +UPDATE [j] +SET [j].[JsonContainer] = JSON_MODIFY([j].[JsonContainer], '$.Value', JSON_VALUE(JSON_OBJECT('v': [j].[OtherValue]), '$.v')) +FROM [JsonTypeEntity] AS [j] +"""); } } } @@ -239,10 +239,10 @@ FROM [JsonTypeEntity] AS [j] { AssertSql( """ - UPDATE [j] - SET [j].[JsonContainer] = JSON_MODIFY([j].[JsonContainer], '$.Value', JSON_VALUE(JSON_OBJECT('v': [j].[OtherValue]), '$.v')) - FROM [JsonTypeEntity] AS [j] - """); +UPDATE [j] +SET [j].[JsonContainer] = JSON_MODIFY([j].[JsonContainer], '$.Value', JSON_VALUE(JSON_OBJECT('v': [j].[OtherValue]), '$.v')) +FROM [JsonTypeEntity] AS [j] +"""); } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Types/SqliteMiscellaneousTypeTest.cs b/test/EFCore.Sqlite.FunctionalTests/Types/SqliteMiscellaneousTypeTest.cs index efca0f9f78c..092f7332376 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Types/SqliteMiscellaneousTypeTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Types/SqliteMiscellaneousTypeTest.cs @@ -49,6 +49,10 @@ public class GuidTypeFixture : RelationalTypeFixtureBase public class ByteArrayTypeTest(ByteArrayTypeTest.ByteArrayTypeFixture fixture) : RelationalTypeTestBase(fixture) { + // TODO: string representation discrepancy between our JSON and M.D.SQLite's string representation, see #36749. + public override Task Query_property_within_json() + => Assert.ThrowsAsync(() => base.Query_property_within_json()); + public override async Task ExecuteUpdate_within_json_to_nonjson_column() { // See #36688 for supporting this for Sqlite types other than string/numeric/bool diff --git a/test/EFCore.Sqlite.FunctionalTests/Types/SqliteTemporalTypeTest.cs b/test/EFCore.Sqlite.FunctionalTests/Types/SqliteTemporalTypeTest.cs index 405de087189..82f62ccef2e 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Types/SqliteTemporalTypeTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Types/SqliteTemporalTypeTest.cs @@ -62,6 +62,10 @@ public class DateTypeFixture : RelationalTypeFixtureBase public class TimeOnlyTypeTest(TimeOnlyTypeTest.TimeTypeFixture fixture) : RelationalTypeTestBase(fixture) { + // TODO: string representation discrepancy between our JSON and M.D.SQLite's string representation, see #36749. + public override Task Query_property_within_json() + => Assert.ThrowsAsync(() => base.Query_property_within_json()); + public override async Task ExecuteUpdate_within_json_to_nonjson_column() { // See #36688 for supporting this for Sqlite types other than string/numeric/bool