diff --git a/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlQuerySqlGenerator.cs b/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlQuerySqlGenerator.cs index e6344f625..3e6f155ab 100644 --- a/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlQuerySqlGenerator.cs +++ b/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlQuerySqlGenerator.cs @@ -106,13 +106,33 @@ private Expression VisitInlinedParameterExpression(MySqlInlinedParameterExpressi protected virtual Expression VisitJsonPathTraversal(MySqlJsonTraversalExpression expression) { - // If the path contains parameters, then the -> and ->> aliases are not supported by MySQL, because - // we need to concatenate the path and the parameters. - // We will use JSON_EXTRACT (and JSON_UNQUOTE if needed) only in this case, because the aliases - // are much more readable. - var isSimplePath = expression.Path.All( - l => l is SqlConstantExpression || - l is MySqlJsonArrayIndexExpression e && e.Expression is SqlConstantExpression); + var isScalar = expression.Type.IsPrimitive(); + var useJsonValue = _options.ServerVersion.Supports.JsonValue && + isScalar; + + if (useJsonValue) + { + VisitJsonPathTraversalCore(expression, "JSON_VALUE"); + return expression; + } + + // JSON_EXTRACT() returns the SQL string `null` for a JSON value of `null`, instead of a SQL value of `NULL`. + // This is an unfortunate oversight by the MySQL team, that is unlikely to be corrected (see + // https://bugs.mysql.com/bug.php?id=85755). + // While JSON_VALUE() handles this scenario correctly for scalar values, for MySQL versions without JSON_VALUE() + // support, we need to explicitly ensure that the returned value is not `null`, or we could return a string + // containing `null` independent of what is expected. + + Sql.Append("CASE "); + + VisitJsonPathTraversalCore( + expression, + "JSON_CONTAINS", + new SqlConstantExpression( + Expression.Constant("null"), + _typeMappingSource.GetMapping(typeof(string)))); + + Sql.Append(" WHEN TRUE THEN NULL ELSE "); if (expression.ReturnsText) { @@ -121,67 +141,100 @@ protected virtual Expression VisitJsonPathTraversal(MySqlJsonTraversalExpression if (expression.Path.Count > 0) { - Sql.Append("JSON_EXTRACT("); + VisitJsonPathTraversalCore(expression, "JSON_EXTRACT"); } + if (expression.ReturnsText) + { + Sql.Append(")"); + } + + Sql.Append(" END"); + + return expression; + } + + protected virtual Expression VisitJsonPathTraversalCore( + MySqlJsonTraversalExpression expression, + string functionName, + SqlExpression secondArgumentExpression = null) + { + Sql.Append(functionName) + .Append("("); + Visit(expression.Expression); + if (secondArgumentExpression is not null) + { + Sql.Append(", "); + + Visit(secondArgumentExpression); + } + if (expression.Path.Count > 0) { Sql.Append(", "); - if (!isSimplePath) - { - Sql.Append("CONCAT("); - } + GenerateJsonPathExpression(expression.Path); + } - Sql.Append("'$"); + Sql.Append(")"); - foreach (var location in expression.Path) - { - if (location is MySqlJsonArrayIndexExpression arrayIndexExpression) - { - var isConstantExpression = arrayIndexExpression.Expression is SqlConstantExpression; + return expression; + } - Sql.Append("["); + private void GenerateJsonPathExpression(IReadOnlyList path) + { + // If the path contains parameters, then the -> and ->> aliases are not supported by MySQL, because + // we need to concatenate the path and the parameters. + // We will use JSON_EXTRACT (and JSON_UNQUOTE if needed) only in this case, because the aliases + // are much more readable. + var isSimplePath = path.All( + l => l is SqlConstantExpression || + l is MySqlJsonArrayIndexExpression e && e.Expression is SqlConstantExpression); - if (!isConstantExpression) - { - Sql.Append("', "); - } + if (!isSimplePath) + { + Sql.Append("CONCAT("); + } - Visit(arrayIndexExpression.Expression); + Sql.Append("'$"); - if (!isConstantExpression) - { - Sql.Append(", '"); - } + foreach (var location in path) + { + if (location is MySqlJsonArrayIndexExpression arrayIndexExpression) + { + var isConstantExpression = arrayIndexExpression.Expression is SqlConstantExpression; - Sql.Append("]"); - } - else + Sql.Append("["); + + if (!isConstantExpression) { - Sql.Append("."); - Visit(location); + Sql.Append("', "); } - } - Sql.Append("'"); + Visit(arrayIndexExpression.Expression); + + if (!isConstantExpression) + { + Sql.Append(", '"); + } - if (!isSimplePath) + Sql.Append("]"); + } + else { - Sql.Append(")"); + Sql.Append("."); + Visit(location); } - - Sql.Append(")"); } - if (expression.ReturnsText) + Sql.Append("'"); + + if (!isSimplePath) { Sql.Append(")"); } - - return expression; } protected override Expression VisitColumn(ColumnExpression columnExpression) diff --git a/src/EFCore.MySql/Query/Expressions/Internal/MySqlJsonTraversalExpression.cs b/src/EFCore.MySql/Query/Expressions/Internal/MySqlJsonTraversalExpression.cs index 68aed5a0c..239028789 100644 --- a/src/EFCore.MySql/Query/Expressions/Internal/MySqlJsonTraversalExpression.cs +++ b/src/EFCore.MySql/Query/Expressions/Internal/MySqlJsonTraversalExpression.cs @@ -39,7 +39,7 @@ public MySqlJsonTraversalExpression( bool returnsText, [NotNull] Type type, [CanBeNull] RelationalTypeMapping typeMapping) - : this(expression, new SqlExpression[0], returnsText, type, typeMapping) + : this(expression, new SqlExpression[0], returnsText, type.UnwrapNullableType(), typeMapping) { } diff --git a/test/EFCore.MySql.FunctionalTests/Query/NorthwindFunctionsQueryMySqlTest.MySql.cs b/test/EFCore.MySql.FunctionalTests/Query/NorthwindFunctionsQueryMySqlTest.MySql.cs index 1ce21d3f2..e33025dc2 100644 --- a/test/EFCore.MySql.FunctionalTests/Query/NorthwindFunctionsQueryMySqlTest.MySql.cs +++ b/test/EFCore.MySql.FunctionalTests/Query/NorthwindFunctionsQueryMySqlTest.MySql.cs @@ -813,7 +813,7 @@ await AssertQueryScalar( AssertSql( $""" -SELECT LOG(7.0, CAST(`o`.`Discount` AS double)) +SELECT LOG(7.0, {MySqlTestHelpers.CastAsDouble("`o`.`Discount`")}) FROM `Order Details` AS `o` WHERE ((`o`.`OrderID` = 11077) AND (`o`.`Discount` > 0)) AND (LOG(7.0, {MySqlTestHelpers.CastAsDouble("`o`.`Discount`")}) < -1.0) """);