-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Feature/translate json functions #29306 #30010
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feature/translate json functions #29306 #30010
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this PR - it's a good start, see comments below.
Note that there are some other JSON functions supported by SQL Server (ISJSON, JSON_ARRAY, JSON_OBJECT...). Ideally we'd have translations for all of them (and the relevant SQLite ones as well); of course you don't have to do that in this PR, but if you feel like working on those that would be great.
src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Sqlite.Core/Extensions/SqliteDbFunctionsExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Sqlite.Core/Extensions/SqliteDbFunctionsExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Sqlite.Core/Query/Internal/SqliteJsonFunctionsTranslator.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Sqlite.Core/Query/Internal/SqliteJsonFunctionsTranslator.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Sqlite.Core/Query/Internal/SqliteJsonFunctionsTranslator.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made some changes now. And think I got them all covered. But little bit uncertain about the propagation of null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@joelmandell sorry, it's taking long for me to get around to this, here are some comments. Please rebase this PR on the latest main and check out any test failures after that.
src/EFCore.SqlServer/Query/Internal/SqlServerJsonFunctionsTranslator.cs
Outdated
Show resolved
Hide resolved
src/EFCore.SqlServer/Query/Internal/SqlServerJsonFunctionsTranslator.cs
Outdated
Show resolved
Hide resolved
src/EFCore.SqlServer/Query/Internal/SqlServerJsonFunctionsTranslator.cs
Outdated
Show resolved
Hide resolved
src/EFCore.SqlServer/Query/Internal/SqlServerJsonFunctionsTranslator.cs
Outdated
Show resolved
Hide resolved
src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.SqlServer/Query/Internal/SqlServerJsonFunctionsTranslator.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Sqlite.Core/Extensions/SqliteDbFunctionsExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Sqlite.Core/Query/Internal/SqliteJsonFunctionsTranslator.cs
Outdated
Show resolved
Hide resolved
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace Microsoft.EntityFrameworkCore.TestModels.JsonQuery; | ||
public class JsonEntityBasicString |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to add this rather than simply using JsonEntityBasic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JsonEntityBasic has mappings of the columns to classes, and this works only for strings If I understand it correctly.
test/EFCore.Sqlite.FunctionalTests/Query/JsonFunctionSqliteTest.cs
Outdated
Show resolved
Hide resolved
json_value in differents rdbms: mysql: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=591207ceddca6fe4f11a3ad8589e7c5b |
@roji I know I was late to the party and missed to tag you in to this before feature freeze for 8.0... Do you have any advice or comments on how I can move forward with this one to perhaps get it ready for 9.0? I can as well look into the other requests for json functions if possible. |
@joelmandell sure thing - I'd also definitely like us to get this in for 9.0. Here's my advice... Give us around a month, maybe a bit more to deal with the 8.0 release and the subsequent patching and other work - and then ping me here again. Assuming it's a good time to iterate on it, you can then rebase this PR on the latest main, and then we can get this to completed state and merge. |
/// </summary> | ||
public SqlExpression? Translate( | ||
SqlExpression? instance, | ||
MethodInfo method, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You must resolve conflict
Ping @roji. I do not know if it is to early to continue on this one now? :) |
ee39973
to
354a9df
Compare
@joelmandell I'm looking at this PR again (sorry for all the breaks and pauses). Since there was previously some problematic merge commit which messed up the entire history, I've squashed everything cleanly rebased on top of the latest main. In the interest of finally getting moving quickly and finally getting this merge, I'll do whatever fixup is still necessary and push commits on this PR. |
@roji Sounds good. Thx |
@roji @joelmandell The extension should be added on the relational level and the mapping is left to each provider to define. I think json expression should be tested before translation to allows only
For SqlServer arguments[0].TypeMapping is SqlServerOwnedJsonTypeMapping or StringTypeMapping For Sqlite arguments[0].TypeMapping is SqliteJsonTypeMapping or StringTypeMapping |
You're probably right for this case. Conflict between same-named extension methods across different providers is something we already have in various other places where multiple providers implement the same extension method (especially in model building). We generally tell users to simply use static method syntax instead of extension method - it seems much better for the few users who actually target mutiple databases to do this, rather than the vast majority of users who target a single database to have weird naming (FYI early versions of EF prefixed all extension methods with ForSqlServer and similar). Putting something on the relational level usually has a high bar, since even when the function exists with the same name across different databases, there are typically slight differences in the signature etc. However, for JSON_VALUE specifically (and probably some of the other JSON functions), it's part of the SQL standard and seems to be pretty closely followed across most databases which do support it:
So I think this all makes sense. Note the extra clauses RETURNING, ON EMPTY and ON ERROR which are widely supported (and probably in the standard). These have special syntax beyond the regular SQL function syntax, but we already internally have a special JsonScalarExpression which represents JSON_VALUE, and which could be augmented to support these clauses. For the EF.Functions side, we could support these clauses via extra optional parameters - I'll see when implementing if that makes sense. |
In general, users using functions on EF.Functions take responsibility for what they do - it's a relatively direct way to specify a SQL function invocation, and there's not much point for us adding validation in EF if the database will error anyway. It's also possible for users to do some convoluted things, e.g. do JSON_VALUE over a non-column (parameter/literal) JSON document, with the JSON path coming from a database column; if we restricted the translation to only support the JSON type mapping, this scenario would be blocked (and again, for no good reason). |
non-column (parameter/literal) JSON document should be of type string and the mapping should be of type |
@mseada94 right, so what's there to test exactly? What's needed beyond just typing the arguments as string, for now (until we make it possible to also invoke these functions with arbitrary objects mapped as complex types/owned entity types)? |
The test here to allow translation and calling for expression with valid type mapping (having first argument as object or generic type) arguments[0].TypeMapping is SqlServerOwnedJsonTypeMapping or StringTypeMapping
It should prevent translation otherwise for example
We need to validate type mapping not the type itself |
Introducing complex type json support will reduce the need for using please consider the following example
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using var context = new SomeDbContext();
var validUsage = context.Users.Where(u => EF.Functions.JsonValue(u.Address, "$.Country") == "Egypt");
var inValidUsage = context.Users.Where(u => EF.Functions.JsonValue(u.Department, "$.Name") == "Engineering");
public class User
{
public Guid Id { get; set; }
public Address Address { get; set; }
public Department Department { get; set; }
}
public class Department
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class Address
{
public string Country { get; set; }
public string City { get; set; }
}
public class SomeDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Department> Departments { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer(Your.ConnectionString);
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().HasOne(d => d.Department).WithMany();
modelBuilder.Entity<User>().Property(x => x.Address).HasConversion(
address => JsonSerializer.Serialize(address, DefaultJsonSerializerOptions),
json => JsonSerializer.Deserialize<Address>(json, DefaultJsonSerializerOptions)!);
}
private static JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
} |
/// <param name="path">The paths</param> | ||
/// <returns>INTEGER or REAL for a JSON numeric value, an INTEGER zero for a JSON false value, an INTEGER one for a JSON true value, the dequoted text for a JSON string value, and a text representation for JSON object and array values</returns> | ||
/// <seealso href="https://www.sqlite.org/json1.html#the_json_extract_function">SQLite documentation for <c>json_extract</c>.</seealso> | ||
public static object JsonExtract(this DbFunctions _, object expression, string path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the great feature. I tested this in my project where path
is a(n interpolated) variable and got the following exception:
System.InvalidCastException: 'Unable to cast object of type 'Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlParameterExpression' to type 'Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlConstantExpression'.'
(I forgot to copy the stacktrace, but it was from SqliteJsonFunctionsTranslator.cs#60
with the SqlConstantExpression
cast.)
This was fixed by tagging path
with [NotParameterized]
here. I'm not 100 % sure if this is the correct fix, but something to consider.
Fixes #29306