Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
{
b.ToTable(nameof(JsonTypeEntity<>));

modelBuilder.Entity<JsonTypeEntity<T>>().Property(e => e.Id).ValueGeneratedNever();

b.ComplexProperty(e => e.JsonContainer, jc =>
modelBuilder.Entity<JsonTypeEntity<T>>(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);
});
});
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<JsonTypeEntity<T>>().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()
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
""");
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsonTypeEntity<T>>().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();
Expand Down Expand Up @@ -55,6 +67,22 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build
public class PointTypeTest(PointTypeTest.PointTypeFixture fixture)
: GeographyTypeTestBase<Point, PointTypeTest.PointTypeFixture>(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 };
Expand All @@ -65,6 +93,22 @@ public class PointTypeFixture() : GeographyTypeFixture
public class LineStringTypeTest(LineStringTypeTest.LineStringTypeFixture fixture)
: GeographyTypeTestBase<LineString, LineStringTypeTest.LineStringTypeFixture>(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(
Expand All @@ -87,6 +131,22 @@ public class LineStringTypeFixture() : GeographyTypeFixture
public class PolygonTypeTest(PolygonTypeTest.PolygonTypeFixture fixture)
: GeographyTypeTestBase<Polygon, PolygonTypeTest.PolygonTypeFixture>(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
Expand Down Expand Up @@ -116,6 +176,22 @@ public class PolygonTypeFixture() : GeographyTypeFixture
public class MultiPointTypeTest(MultiPointTypeTest.MultiPointTypeFixture fixture)
: GeographyTypeTestBase<MultiPoint, MultiPointTypeTest.MultiPointTypeFixture>(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([
Expand All @@ -136,6 +212,22 @@ public class MultiPointTypeFixture() : GeographyTypeFixture
public class MultiLineStringTypeTest(MultiLineStringTypeTest.MultiLineStringTypeFixture fixture)
: GeographyTypeTestBase<MultiLineString, MultiLineStringTypeTest.MultiLineStringTypeFixture>(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([
Expand Down Expand Up @@ -167,6 +259,22 @@ public class MultiLineStringTypeFixture() : GeographyTypeFixture
public class MultiPolygonTypeTest(MultiPolygonTypeTest.MultiPolygonTypeFixture fixture)
: GeographyTypeTestBase<MultiPolygon, MultiPolygonTypeTest.MultiPolygonTypeFixture>(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(
Expand Down Expand Up @@ -212,6 +320,22 @@ public class MultiPolygonTypeFixture() : GeographyTypeFixture
public class GeometryCollectionTypeTest(GeometryCollectionTypeTest.GeometryCollectionTypeFixture fixture)
: GeographyTypeTestBase<GeometryCollection, GeometryCollectionTypeTest.GeometryCollectionTypeFixture>(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(
Expand Down
Loading
Loading