Skip to content

Commit 4e9910a

Browse files
authored
Merge pull request #150 from microting/copilot/fix-no-coercion-operator-error
Fix "No coercion operator is defined" error for JSON complex types
2 parents 7f9f81b + f8acf3e commit 4e9910a

6 files changed

Lines changed: 164 additions & 9 deletions

File tree

src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlQuerySqlGenerator.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,9 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
477477
var path = jsonScalarExpression.Path;
478478
if (path.Count == 0)
479479
{
480+
// For empty paths (selecting the entire JSON column), just visit the JSON expression
481+
// This handles complex JSON types where we're projecting the whole column
482+
Visit(jsonScalarExpression.Json);
480483
return jsonScalarExpression;
481484
}
482485

@@ -1023,5 +1026,10 @@ protected override void CheckComposableSql(string sql)
10231026
// MySQL supports CTE (WITH) expressions within subqueries, as well as others,
10241027
// so we allow any raw SQL to be composed over.
10251028
}
1029+
1030+
protected override Expression VisitSelect(SelectExpression selectExpression)
1031+
{
1032+
return base.VisitSelect(selectExpression);
1033+
}
10261034
}
10271035
}

src/EFCore.MySql/Storage/Internal/MySqlJsonTypeMapping.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Data.Common;
7+
using System.IO;
78
using System.Linq;
9+
using System.Linq.Expressions;
10+
using System.Reflection;
11+
using System.Text;
812
using JetBrains.Annotations;
913
using Microsoft.EntityFrameworkCore;
1014
using Microsoft.EntityFrameworkCore.ChangeTracking;
@@ -57,6 +61,9 @@ protected override RelationalTypeMapping Clone(bool? noBackslashEscapes = null,
5761

5862
public abstract class MySqlJsonTypeMapping : MySqlStringTypeMapping, IMySqlCSharpRuntimeAnnotationTypeMappingCodeGenerator
5963
{
64+
protected static readonly MethodInfo _getString
65+
= typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), new[] { typeof(int) });
66+
6067
public MySqlJsonTypeMapping(
6168
[NotNull] string storeType,
6269
[NotNull] Type clrType,
@@ -118,6 +125,28 @@ protected override void ConfigureParameter(DbParameter parameter)
118125
}
119126
}
120127

128+
/// <summary>
129+
/// Returns the method to be used for reading JSON values from the database.
130+
/// MySQL stores JSON as strings, so we use GetString instead of the default GetFieldValue&lt;T&gt;.
131+
/// </summary>
132+
public override MethodInfo GetDataReaderMethod()
133+
{
134+
Console.WriteLine($"[DEBUG] MySqlJsonTypeMapping.GetDataReaderMethod() called - ClrType: {ClrType.Name} - returning DbDataReader.GetString");
135+
return _getString;
136+
}
137+
138+
/// <summary>
139+
/// Customizes the data reader expression for JSON types.
140+
/// This is only used for regular JSON columns mapped to string properties.
141+
/// Complex JSON types use MySqlStructuralJsonTypeMapping instead.
142+
/// </summary>
143+
public override Expression CustomizeDataReaderExpression(Expression expression)
144+
{
145+
Console.WriteLine($"[DEBUG] MySqlJsonTypeMapping.CustomizeDataReaderExpression() called - ClrType: {ClrType.Name} - no conversion");
146+
// For regular JSON columns, no conversion needed - just return the string
147+
return base.CustomizeDataReaderExpression(expression);
148+
}
149+
121150
void IMySqlCSharpRuntimeAnnotationTypeMappingCodeGenerator.Create(
122151
CSharpRuntimeAnnotationCodeGeneratorParameters codeGeneratorParameters,
123152
CSharpRuntimeAnnotationCodeGeneratorDependencies codeGeneratorDependencies)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) Pomelo Foundation. All rights reserved.
2+
// Licensed under the MIT. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using System.Data.Common;
6+
using System.IO;
7+
using System.Linq.Expressions;
8+
using System.Reflection;
9+
using System.Text;
10+
using Microsoft.EntityFrameworkCore.Storage;
11+
using MySqlConnector;
12+
13+
namespace Pomelo.EntityFrameworkCore.MySql.Storage.Internal
14+
{
15+
/// <summary>
16+
/// Type mapping for complex JSON types (types mapped with .ToJson()).
17+
/// This mapping handles the conversion from MySQL's string-based JSON to MemoryStream expected by EF Core.
18+
/// Similar to Npgsql's NpgsqlStructuralJsonTypeMapping.
19+
/// </summary>
20+
public class MySqlStructuralJsonTypeMapping : JsonTypeMapping
21+
{
22+
private static readonly MethodInfo _getStringMethod
23+
= typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), new[] { typeof(int) });
24+
25+
private static readonly PropertyInfo _utf8Property
26+
= typeof(Encoding).GetProperty(nameof(Encoding.UTF8));
27+
28+
private static readonly MethodInfo _getBytesMethod
29+
= typeof(Encoding).GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) });
30+
31+
private static readonly ConstructorInfo _memoryStreamConstructor
32+
= typeof(MemoryStream).GetConstructor(new[] { typeof(byte[]) });
33+
34+
public static MySqlStructuralJsonTypeMapping Default { get; } = new("json");
35+
36+
public MySqlStructuralJsonTypeMapping(string storeType)
37+
: base(storeType, typeof(JsonTypePlaceholder), dbType: null)
38+
{
39+
Console.WriteLine($"[DEBUG] MySqlStructuralJsonTypeMapping created - StoreType: {storeType}, ClrType: JsonTypePlaceholder, DbType: null");
40+
}
41+
42+
protected MySqlStructuralJsonTypeMapping(RelationalTypeMappingParameters parameters)
43+
: base(parameters)
44+
{
45+
Console.WriteLine($"[DEBUG] MySqlStructuralJsonTypeMapping cloned - StoreType: {parameters.StoreType}");
46+
}
47+
48+
/// <summary>
49+
/// MySQL stores JSON as strings, so we read using GetString.
50+
/// </summary>
51+
public override MethodInfo GetDataReaderMethod()
52+
{
53+
Console.WriteLine("[DEBUG] MySqlStructuralJsonTypeMapping.GetDataReaderMethod() called - returning DbDataReader.GetString");
54+
return _getStringMethod;
55+
}
56+
57+
/// <summary>
58+
/// Customizes the data reader expression to convert string to MemoryStream.
59+
/// Creates: new MemoryStream(Encoding.UTF8.GetBytes(stringValue))
60+
/// </summary>
61+
public override Expression CustomizeDataReaderExpression(Expression expression)
62+
{
63+
Console.WriteLine("[DEBUG] MySqlStructuralJsonTypeMapping.CustomizeDataReaderExpression() called - converting string to MemoryStream");
64+
return Expression.New(
65+
_memoryStreamConstructor,
66+
Expression.Call(
67+
Expression.Property(null, _utf8Property),
68+
_getBytesMethod,
69+
expression));
70+
}
71+
72+
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
73+
=> new MySqlStructuralJsonTypeMapping(parameters);
74+
75+
/// <summary>
76+
/// Override to ensure we return JsonTypePlaceholder type consistently
77+
/// </summary>
78+
public override Type ClrType => typeof(JsonTypePlaceholder);
79+
80+
protected string EscapeSqlLiteral(string literal)
81+
=> literal.Replace("'", "''");
82+
83+
protected override string GenerateNonNullSqlLiteral(object value)
84+
=> $"'{EscapeSqlLiteral((string)value)}'";
85+
86+
protected override void ConfigureParameter(DbParameter parameter)
87+
{
88+
// MySQL uses JSON db type for JSON columns
89+
if (parameter is MySqlParameter mySqlParameter)
90+
{
91+
mySqlParameter.MySqlDbType = MySqlDbType.JSON;
92+
}
93+
94+
base.ConfigureParameter(parameter);
95+
}
96+
}
97+
}

src/EFCore.MySql/Storage/Internal/MySqlTypeMappingSource.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ private readonly RelationalTypeMapping _binaryRowVersion6
8787
// guid
8888
private GuidTypeMapping _guid;
8989

90-
// JSON default mapping
90+
// JSON default mapping for regular JSON columns mapped to string
9191
private MySqlJsonTypeMapping<string> _jsonDefaultString;
92+
// JSON mapping for complex types (types mapped with .ToJson())
93+
private MySqlStructuralJsonTypeMapping _jsonStructural;
9294

9395
// Scaffolding type mappings
9496
private readonly MySqlCodeGenerationMemberAccessTypeMapping _codeGenerationMemberAccess = MySqlCodeGenerationMemberAccessTypeMapping.Default;
@@ -135,6 +137,7 @@ private void Initialize()
135137
: null;
136138

137139
_jsonDefaultString = new MySqlJsonTypeMapping<string>("json", null, null, _options.NoBackslashEscapes, _options.ReplaceLineBreaksWithCharFunction);
140+
_jsonStructural = new MySqlStructuralJsonTypeMapping("json");
138141

139142
_storeTypeMappings
140143
= new Dictionary<string, RelationalTypeMapping[]>(StringComparer.OrdinalIgnoreCase)
@@ -309,6 +312,15 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn
309312
var storeTypeName = mappingInfo.StoreTypeName;
310313
var storeTypeNameBase = mappingInfo.StoreTypeNameBase;
311314

315+
// Special case for JSON columns: EF Core passes JsonTypePlaceholder as the CLR type
316+
// when creating JSON columns for complex types/collections. Return our structural JSON mapping.
317+
// This MUST be checked first, before any store type lookups, similar to SQL Server's implementation.
318+
if (clrType?.Name == "JsonTypePlaceholder")
319+
{
320+
Console.WriteLine($"[DEBUG] MySqlTypeMappingSource: Detected JsonTypePlaceholder - returning MySqlStructuralJsonTypeMapping");
321+
return _jsonStructural;
322+
}
323+
312324
if (storeTypeName != null)
313325
{
314326
// First look for the fully qualified store type name.
@@ -320,13 +332,6 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn
320332
// If a CLR type was provided, look for a mapping between the store and CLR types. If none is found,
321333
// fail immediately.
322334

323-
// Special case for JSON columns: EF Core passes JsonTypePlaceholder as the CLR type
324-
// when creating JSON columns for complex types/collections. Return our JSON mapping.
325-
if (clrType?.Name == "JsonTypePlaceholder" && storeTypeName.Equals("json", StringComparison.OrdinalIgnoreCase))
326-
{
327-
return _jsonDefaultString;
328-
}
329-
330335
return clrType == null
331336
? mappings[0]
332337
: mappings.FirstOrDefault(m => m.ClrType == clrType);

src/EFCore.MySql/Update/Internal/MySqlModificationCommandBatch.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ public override void Complete(bool moreBatchesExpected)
120120
ApplyPendingBulkInsertCommands();
121121

122122
base.Complete(moreBatchesExpected);
123+
124+
// DEBUG: Log the complete SQL command
125+
var sqlText = SqlBuilder.ToString();
126+
Console.WriteLine($"[DEBUG SQL COMPLETE] SQL Text Length: {sqlText.Length}");
127+
Console.WriteLine($"[DEBUG SQL COMPLETE] SQL Text: {sqlText}");
123128
}
124129

125130
/// <summary>

src/EFCore.MySql/Update/Internal/MySqlUpdateSqlGenerator.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,20 @@ public override ResultSetMapping AppendUpdateOperation(
181181
IReadOnlyModificationCommand command,
182182
int commandPosition,
183183
out bool requiresTransaction)
184-
=> _options.ServerVersion.Supports.Returning
184+
{
185+
var startLength = commandStringBuilder.Length;
186+
var result = _options.ServerVersion.Supports.Returning
185187
? AppendUpdateReturningOperation(commandStringBuilder, command, commandPosition, out requiresTransaction)
186188
: base.AppendUpdateOperation(commandStringBuilder, command, commandPosition, out requiresTransaction);
189+
190+
// Debug: Log the generated SQL
191+
var generatedSql = commandStringBuilder.ToString(startLength, commandStringBuilder.Length - startLength);
192+
Console.WriteLine($"[DEBUG SQL Generated] AppendUpdateOperation:");
193+
Console.WriteLine(generatedSql);
194+
Console.WriteLine($"[DEBUG SQL Generated] Table: {command.TableName}, Columns: {command.ColumnModifications.Count}");
195+
196+
return result;
197+
}
187198

188199
/// <summary>
189200
/// Appends SQL for updating a row to the commands being built, via an UPDATE containing a RETURNING clause

0 commit comments

Comments
 (0)