diff --git a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs
index f0f6a0e1ea8..3eb33258d9d 100644
--- a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs
@@ -31,10 +31,7 @@ public static class RelationalDbFunctionsExtensions
/// The instance.
/// The operand to which to apply the collation.
/// The name of the collation.
- public static TProperty Collate(
- this DbFunctions _,
- TProperty operand,
- [NotParameterized] string collation)
+ public static TProperty Collate(this DbFunctions _, TProperty operand, [NotParameterized] string collation)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Collate)));
///
@@ -42,9 +39,7 @@ public static TProperty Collate(
///
/// The instance.
/// The list of values from which return the smallest value.
- public static T Least(
- this DbFunctions _,
- [NotParameterized] params T[] values)
+ public static T Least(this DbFunctions _, [NotParameterized] params T[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Least)));
///
@@ -52,8 +47,15 @@ public static T Least(
///
/// The instance.
/// The list of values from which return the greatest value.
- public static T Greatest(
- this DbFunctions _,
- [NotParameterized] params T[] values)
+ public static T Greatest(this DbFunctions _, [NotParameterized] params T[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Greatest)));
+
+ ///
+ /// Returns a value indicating whether a given JSON path exists within the specified JSON.
+ ///
+ /// The instance.
+ /// The JSON value to check.
+ /// The JSON path to look for.
+ public static bool JsonExists(this DbFunctions _, object json, string path)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExists)));
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs
index ea2aa94c58b..0af9d673f26 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs
@@ -240,6 +240,46 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
_typeMappingSource.FindMapping(isUnicode ? "nvarchar(max)" : "varchar(max)"));
}
+ // We translate EF.Functions.JsonExists here and not in a method translator since we need to support JsonExists over
+ // complex and owned JSON properties, which requires special handling.
+ case nameof(RelationalDbFunctionsExtensions.JsonExists)
+ when declaringType == typeof(RelationalDbFunctionsExtensions)
+ && @object is null
+ && arguments is [_, var json, var path]:
+ {
+ if (Translate(path) is not SqlExpression translatedPath)
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+
+#pragma warning disable EF1001 // TranslateProjection() is pubternal
+ var translatedJson = TranslateProjection(json) switch
+ {
+ // The JSON argument is a scalar string property
+ SqlExpression scalar => scalar,
+
+ // The JSON argument is a complex or owned JSON property
+ RelationalStructuralTypeShaperExpression { ValueBufferExpression: JsonQueryExpression { JsonColumn: var c } } => c,
+
+ _ => null
+ };
+#pragma warning restore EF1001
+
+ return translatedJson is null
+ ? QueryCompilationContext.NotTranslatedExpression
+ : _sqlExpressionFactory.Equal(
+ _sqlExpressionFactory.Function(
+ "JSON_PATH_EXISTS",
+ [translatedJson, translatedPath],
+ nullable: true,
+ // Note that JSON_PATH_EXISTS() does propagate nullability; however, our query pipeline assumes that if
+ // arguments propagate nullability, that's the *only* reason for the function to return null; this means that
+ // if the arguments are non-nullable, the IS NOT NULL wrapping check can be optimized away.
+ argumentsPropagateNullability: [false, false],
+ typeof(int)),
+ _sqlExpressionFactory.Constant(1));
+ }
+
default:
return QueryCompilationContext.NotTranslatedExpression;
}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs
index 9e13d5e0bfd..2001be61a61 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs
@@ -214,21 +214,65 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}
var method = methodCallExpression.Method;
+ var declaringType = method.DeclaringType;
+ var @object = methodCallExpression.Object;
+ var arguments = methodCallExpression.Arguments;
- // https://learn.microsoft.com/dotnet/api/system.string.startswith#system-string-startswith(system-string)
- // https://learn.microsoft.com/dotnet/api/system.string.startswith#system-string-startswith(system-char)
- // https://learn.microsoft.com/dotnet/api/system.string.endswith#system-string-endswith(system-string)
- // https://learn.microsoft.com/dotnet/api/system.string.endswith#system-string-endswith(system-char)
- if (method.Name is nameof(string.StartsWith) or nameof(string.EndsWith)
- && methodCallExpression.Object is not null
- && method.DeclaringType == typeof(string)
- && methodCallExpression.Arguments is [Expression value]
- && (value.Type == typeof(string) || value.Type == typeof(char)))
+ switch (method.Name)
{
- return TranslateStartsEndsWith(
- methodCallExpression.Object,
- value,
- method.Name is nameof(string.StartsWith));
+ // https://learn.microsoft.com/dotnet/api/system.string.startswith#system-string-startswith(system-string)
+ // https://learn.microsoft.com/dotnet/api/system.string.startswith#system-string-startswith(system-char)
+ // https://learn.microsoft.com/dotnet/api/system.string.endswith#system-string-endswith(system-string)
+ // https://learn.microsoft.com/dotnet/api/system.string.endswith#system-string-endswith(system-char)
+ case nameof(string.StartsWith) or nameof(string.EndsWith)
+ when methodCallExpression.Object is not null
+ && declaringType == typeof(string)
+ && arguments is [Expression value]
+ && (value.Type == typeof(string) || value.Type == typeof(char)):
+ {
+ return TranslateStartsEndsWith(
+ methodCallExpression.Object,
+ value,
+ method.Name is nameof(string.StartsWith));
+ }
+
+ // We translate EF.Functions.JsonExists here and not in a method translator since we need to support JsonExists over
+ // complex and owned JSON properties, which requires special handling.
+ case nameof(RelationalDbFunctionsExtensions.JsonExists)
+ when declaringType == typeof(RelationalDbFunctionsExtensions)
+ && @object is null
+ && arguments is [_, var json, var path]:
+ {
+ if (Translate(path) is not SqlExpression translatedPath)
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+
+#pragma warning disable EF1001 // TranslateProjection() is pubternal
+ var translatedJson = TranslateProjection(json) switch
+ {
+ // The JSON argument is a scalar string property
+ SqlExpression scalar => scalar,
+
+ // The JSON argument is a complex or owned JSON property
+ RelationalStructuralTypeShaperExpression { ValueBufferExpression: JsonQueryExpression { JsonColumn: var c } } => c,
+ _ => null
+ };
+#pragma warning restore EF1001
+
+ return translatedJson is null
+ ? QueryCompilationContext.NotTranslatedExpression
+ : _sqlExpressionFactory.IsNotNull(
+ _sqlExpressionFactory.Function(
+ "json_type",
+ [translatedJson, translatedPath],
+ nullable: true,
+ // Note that json_type() does propagate nullability; however, our query pipeline assumes that if arguments
+ // propagate nullability, that's the *only* reason for the function to return null; this means that if the
+ // arguments are non-nullable, the IS NOT NULL wrapping check can be optimized away.
+ argumentsPropagateNullability: [false, false],
+ typeof(int)));
+ }
}
return QueryCompilationContext.NotTranslatedExpression;
diff --git a/test/EFCore.Relational.Specification.Tests/Query/Translations/JsonTranslationsRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/Translations/JsonTranslationsRelationalTestBase.cs
new file mode 100644
index 00000000000..9b8d7d2b3e0
--- /dev/null
+++ b/test/EFCore.Relational.Specification.Tests/Query/Translations/JsonTranslationsRelationalTestBase.cs
@@ -0,0 +1,233 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Nodes;
+
+namespace Microsoft.EntityFrameworkCore.Query.Translations;
+
+// This test suite covers translations of JSON functions on EF.Functions (e.g. EF.Functions.JsonExists).
+// It does not cover general, built-in JSON support via complex type mapping, etc.
+public abstract class JsonTranslationsRelationalTestBase(TFixture fixture) : QueryTestBase(fixture)
+ where TFixture : JsonTranslationsRelationalTestBase.JsonTranslationsQueryFixtureBase, new()
+{
+ [ConditionalFact]
+ public virtual Task JsonExists_on_scalar_string_column()
+ => AssertQuery(
+ ss => ss.Set()
+ .Where(b => EF.Functions.JsonExists(b.JsonString, "$.OptionalInt")),
+ ss => ss.Set()
+ .Where(b => ((IDictionary)JsonNode.Parse(b.JsonString)!).ContainsKey("OptionalInt")));
+
+ [ConditionalFact]
+ public virtual Task JsonExists_on_complex_property()
+ => AssertQuery(
+ ss => ss.Set()
+ .Where(b => EF.Functions.JsonExists(b.JsonComplexType, "$.OptionalInt")),
+ ss => ss.Set()
+ .Where(b => ((IDictionary)JsonNode.Parse(b.JsonString)!).ContainsKey("OptionalInt")));
+
+ [ConditionalFact]
+ public virtual Task JsonExists_on_owned_entity()
+ => AssertQuery(
+ ss => ss.Set()
+ .Where(b => EF.Functions.JsonExists(b.JsonOwnedType, "$.OptionalInt")),
+ ss => ss.Set()
+ .Where(b => ((IDictionary)JsonNode.Parse(b.JsonString)!).ContainsKey("OptionalInt")));
+
+ public class JsonTranslationsEntity
+ {
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public int Id { get; set; }
+
+ public required string JsonString { get; set; }
+
+ public required JsonComplexType JsonComplexType { get; set; }
+ public required JsonOwnedType JsonOwnedType { get; set; }
+ }
+
+ public class JsonComplexType
+ {
+ public required int RequiredInt { get; set; }
+ public int? OptionalInt { get; set; }
+ }
+
+ public class JsonOwnedType
+ {
+ public required int RequiredInt { get; set; }
+ public int? OptionalInt { get; set; }
+ }
+
+ public class JsonTranslationsQueryContext(DbContextOptions options) : PoolableDbContext(options)
+ {
+ public DbSet JsonEntities { get; set; } = null!;
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(b =>
+ {
+ b.ComplexProperty(j => j.JsonComplexType, j => j.ToJson());
+#pragma warning disable EF8001 // ToJson() on owned entities is obsolete
+ b.OwnsOne(j => j.JsonOwnedType, j => j.ToJson());
+#pragma warning restore EF8001
+ });
+ }
+ }
+
+ // The translation tests usually use BasicTypesQueryFixtureBase, which manages a single database with all the data needed for the tests.
+ // However, here in the JSON translation tests we use a separate fixture and database, since not all providers necessary implement full
+ // JSON support, and we don't want to make life difficult for them with the basic translation tests.
+ public abstract class JsonTranslationsQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory
+ {
+ private JsonTranslationsData? _expectedData;
+
+ protected override string StoreName
+ => "JsonTranslationsQueryTest";
+
+ protected override async Task SeedAsync(JsonTranslationsQueryContext context)
+ {
+ var data = new JsonTranslationsData();
+ context.AddRange(data.JsonTranslationsEntities);
+ await context.SaveChangesAsync();
+
+ var entityType = context.Model.FindEntityType(typeof(JsonTranslationsEntity))!;
+ var sqlGenerationHelper = context.GetService();
+ var table = sqlGenerationHelper.DelimitIdentifier(entityType.GetTableName()!);
+ var idColumn = sqlGenerationHelper.DelimitIdentifier(
+ entityType.FindProperty(nameof(JsonTranslationsEntity.Id))!.GetColumnName());
+ var complexTypeColumn = sqlGenerationHelper.DelimitIdentifier(
+ entityType.FindComplexProperty(nameof(JsonTranslationsEntity.JsonComplexType))!.ComplexType.GetContainerColumnName()!);
+ var ownedColumn = sqlGenerationHelper.DelimitIdentifier(
+ entityType.FindNavigation(nameof(JsonTranslationsEntity.JsonOwnedType))!.TargetEntityType.GetContainerColumnName()!);
+
+ await context.Database.ExecuteSqlRawAsync(
+ $$"""
+ UPDATE {{table}} SET {{complexTypeColumn}} = {{RemoveJsonProperty(complexTypeColumn, "$.OptionalInt")}} WHERE {{idColumn}} = 4;
+ UPDATE {{table}} SET {{ownedColumn}} = {{RemoveJsonProperty(ownedColumn, "$.OptionalInt")}} WHERE {{idColumn}} = 4;
+ """);
+ }
+
+ protected abstract string RemoveJsonProperty(string column, string jsonPath);
+
+ public virtual ISetSource GetExpectedData()
+ => _expectedData ??= new JsonTranslationsData();
+
+ public IReadOnlyDictionary EntitySorters { get; } = new Dictionary>
+ {
+ { typeof(JsonTranslationsEntity), e => ((JsonTranslationsEntity?)e)?.Id },
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+
+ public IReadOnlyDictionary EntityAsserters { get; } = new Dictionary>
+ {
+ {
+ typeof(JsonTranslationsEntity), (e, a) =>
+ {
+ Assert.Equal(e == null, a == null);
+
+ if (a != null)
+ {
+ var ee = (JsonTranslationsEntity)e!;
+ var aa = (JsonTranslationsEntity)a;
+
+ Assert.Equal(ee.Id, aa.Id);
+
+ Assert.Equal(ee.JsonString, aa.JsonString);
+ }
+ }
+ }
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+
+ public Func GetContextCreator()
+ => CreateContext;
+
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+ }
+
+ public class JsonTranslationsData : ISetSource
+ {
+ public IReadOnlyList JsonTranslationsEntities { get; } = CreateJsonTranslationsEntities();
+
+ public IQueryable Set()
+ where TEntity : class
+ => typeof(TEntity) == typeof(JsonTranslationsEntity)
+ ? (IQueryable)JsonTranslationsEntities.AsQueryable()
+ : throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity));
+
+ public static IReadOnlyList CreateJsonTranslationsEntities() =>
+ [
+ // In the following, JsonString should correspond exactly to JsonComplexType and JsonOwnedType;
+ // we don't currently support mapping both a string scalar property and a complex/owned JSON property
+ // to the same column in the database.
+
+ new()
+ {
+ Id = 1,
+ JsonString = """{ "RequiredInt": 8, "OptionalInt": 8 }""",
+ JsonComplexType = new()
+ {
+ RequiredInt = 8,
+ OptionalInt = 8
+ },
+ JsonOwnedType = new()
+ {
+ RequiredInt = 8,
+ OptionalInt = 8
+ }
+ },
+ // Different values
+ new()
+ {
+ Id = 2,
+ JsonString = """{ "RequiredInt": 9, "OptionalInt": 9 }""",
+ JsonComplexType = new()
+ {
+ RequiredInt = 9,
+ OptionalInt = 9
+ },
+ JsonOwnedType = new()
+ {
+ RequiredInt = 9,
+ OptionalInt = 9
+ }
+ },
+ // OptionalInt is null.
+ new()
+ {
+ Id = 3,
+ JsonString = """{ "RequiredInt": 10, "OptionalInt": null }""",
+ JsonComplexType = new()
+ {
+ RequiredInt = 10,
+ OptionalInt = null
+ },
+ JsonOwnedType = new()
+ {
+ RequiredInt = 10,
+ OptionalInt = null
+ }
+ },
+ // OptionalInt is missing (not null).
+ // Note that this requires a manual SQL update since EF's complex/owned type support always writes out the property (with null);
+ // any change here requires updating JsonTranslationsQueryContext.SeedAsync as well.
+ new()
+ {
+ Id = 4,
+ JsonString = """{ "RequiredInt": 10 }""",
+ JsonComplexType = new()
+ {
+ RequiredInt = 10,
+ OptionalInt = null // This will be replaced by a missing property
+ },
+ JsonOwnedType = new()
+ {
+ RequiredInt = 10,
+ OptionalInt = null // This will be replaced by a missing property
+ }
+ }
+ ];
+ }
+
+ protected JsonTranslationsQueryContext CreateContext()
+ => Fixture.CreateContext();
+}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonSqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonSqlServerFixture.cs
index 3584e53423e..300689eb1e9 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonSqlServerFixture.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonSqlServerFixture.cs
@@ -12,6 +12,7 @@ protected override ITestStoreFactory TestStoreFactory
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
{
var options = base.AddOptions(builder);
+
return TestEnvironment.SqlServerMajorVersion < 17
? options
: options.UseSqlServerCompatibilityLevel(170);
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/JsonTranslationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/JsonTranslationsSqlServerTest.cs
new file mode 100644
index 00000000000..19a28d8c267
--- /dev/null
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/JsonTranslationsSqlServerTest.cs
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query.Translations;
+
+public class JsonTranslationsSqlServerTest : JsonTranslationsRelationalTestBase
+{
+ public JsonTranslationsSqlServerTest(JsonTranslationsQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ }
+
+ [ConditionalFact, SqlServerCondition(SqlServerCondition.SupportsFunctions2022)]
+ public override async Task JsonExists_on_scalar_string_column()
+ {
+ await base.JsonExists_on_scalar_string_column();
+
+ AssertSql(
+ """
+SELECT [j].[Id], [j].[JsonString], [j].[JsonComplexType], [j].[JsonOwnedType]
+FROM [JsonEntities] AS [j]
+WHERE JSON_PATH_EXISTS([j].[JsonString], N'$.OptionalInt') = 1
+""");
+ }
+
+ [ConditionalFact, SqlServerCondition(SqlServerCondition.SupportsFunctions2022)]
+ public override async Task JsonExists_on_complex_property()
+ {
+ await base.JsonExists_on_complex_property();
+
+ AssertSql(
+ """
+SELECT [j].[Id], [j].[JsonString], [j].[JsonComplexType], [j].[JsonOwnedType]
+FROM [JsonEntities] AS [j]
+WHERE JSON_PATH_EXISTS([j].[JsonComplexType], N'$.OptionalInt') = 1
+""");
+ }
+
+ [ConditionalFact, SqlServerCondition(SqlServerCondition.SupportsFunctions2022)]
+ public override async Task JsonExists_on_owned_entity()
+ {
+ await base.JsonExists_on_owned_entity();
+
+ AssertSql(
+ """
+SELECT [j].[Id], [j].[JsonString], [j].[JsonComplexType], [j].[JsonOwnedType]
+FROM [JsonEntities] AS [j]
+WHERE JSON_PATH_EXISTS([j].[JsonOwnedType], N'$.OptionalInt') = 1
+""");
+ }
+
+ public class JsonTranslationsQuerySqlServerFixture : JsonTranslationsQueryFixtureBase, ITestSqlLoggerFactory
+ {
+ protected override ITestStoreFactory TestStoreFactory
+ => SqlServerTestStoreFactory.Instance;
+
+ // When testing against SQL Server 2025 or later, set the compatibility level to 170 to use the json type instead of nvarchar(max).
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ {
+ var options = base.AddOptions(builder);
+
+ return TestEnvironment.SqlServerMajorVersion < 17
+ ? options
+ : options.UseSqlServerCompatibilityLevel(170);
+ }
+
+ protected override string RemoveJsonProperty(string column, string jsonPath)
+ => $"JSON_MODIFY({column}, '{jsonPath}', NULL)";
+ }
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Translations/JsonTranslationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/JsonTranslationsSqliteTest.cs
new file mode 100644
index 00000000000..f1c82ae2a8e
--- /dev/null
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/JsonTranslationsSqliteTest.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query.Translations;
+
+public class JsonTranslationsSqliteTest : JsonTranslationsRelationalTestBase
+{
+ public JsonTranslationsSqliteTest(JsonTranslationsQuerySqliteFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ }
+
+ [ConditionalFact]
+ public override async Task JsonExists_on_scalar_string_column()
+ {
+ await base.JsonExists_on_scalar_string_column();
+
+ AssertSql(
+ """
+SELECT "j"."Id", "j"."JsonString", "j"."JsonComplexType", "j"."JsonOwnedType"
+FROM "JsonEntities" AS "j"
+WHERE json_type("j"."JsonString", '$.OptionalInt') IS NOT NULL
+""");
+ }
+
+ [ConditionalFact]
+ public override async Task JsonExists_on_complex_property()
+ {
+ await base.JsonExists_on_complex_property();
+
+ AssertSql(
+ """
+SELECT "j"."Id", "j"."JsonString", "j"."JsonComplexType", "j"."JsonOwnedType"
+FROM "JsonEntities" AS "j"
+WHERE json_type("j"."JsonComplexType", '$.OptionalInt') IS NOT NULL
+""");
+ }
+
+ [ConditionalFact]
+ public override async Task JsonExists_on_owned_entity()
+ {
+ await base.JsonExists_on_owned_entity();
+
+ AssertSql(
+ """
+SELECT "j"."Id", "j"."JsonString", "j"."JsonComplexType", "j"."JsonOwnedType"
+FROM "JsonEntities" AS "j"
+WHERE json_type("j"."JsonOwnedType", '$.OptionalInt') IS NOT NULL
+""");
+ }
+
+ public class JsonTranslationsQuerySqliteFixture : JsonTranslationsQueryFixtureBase, ITestSqlLoggerFactory
+ {
+ protected override ITestStoreFactory TestStoreFactory
+ => SqliteTestStoreFactory.Instance;
+
+ protected override string RemoveJsonProperty(string column, string jsonPath)
+ => $"json_remove({column}, '{jsonPath}')";
+ }
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}