@@ -159,6 +159,23 @@ protected override void GenerateTop(SelectExpression selectExpression)
159159 // No TOP() in PostgreSQL, see GenerateLimitOffset
160160 }
161161
162+ /// <summary>
163+ /// Generates SQL for a constant.
164+ /// </summary>
165+ /// <param name="sqlConstantExpression">The <see cref="SqlConstantExpression" /> for which to generate SQL.</param>
166+ protected override Expression VisitSqlConstant ( SqlConstantExpression sqlConstantExpression )
167+ {
168+ // Certain JSON functions (e.g. jsonb_set()) accept a JSONPATH argument - this is (currently) flown here as a
169+ // SqlConstantExpression over IReadOnlyList<PathSegment>. Render that to a string here.
170+ if ( sqlConstantExpression is { Value : IReadOnlyList < PathSegment > path } )
171+ {
172+ GenerateJsonPath ( ConvertJsonPathSegments ( path ) ) ;
173+ return sqlConstantExpression ;
174+ }
175+
176+ return base . VisitSqlConstant ( sqlConstantExpression ) ;
177+ }
178+
162179 /// <summary>
163180 /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
164181 /// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -1058,31 +1075,33 @@ protected virtual Expression VisitILike(PgILikeExpression likeExpression, bool n
10581075 protected override Expression VisitJsonScalar ( JsonScalarExpression jsonScalarExpression )
10591076 {
10601077 // TODO: Stop producing empty JsonScalarExpressions, #30768
1061- var path = jsonScalarExpression . Path ;
1062- if ( path . Count == 0 )
1078+ var segmentsPath = jsonScalarExpression . Path ;
1079+ if ( segmentsPath . Count == 0 )
10631080 {
10641081 Visit ( jsonScalarExpression . Json ) ;
10651082 return jsonScalarExpression ;
10661083 }
10671084
1085+ var path = ConvertJsonPathSegments ( segmentsPath ) ;
1086+
10681087 switch ( jsonScalarExpression . TypeMapping )
10691088 {
10701089 // This case is for when a nested JSON entity is being accessed. We want the json/jsonb fragment in this case (not text),
10711090 // so we can perform further JSON operations on it.
10721091 case NpgsqlStructuralJsonTypeMapping :
1073- GenerateJsonPath ( returnsText : false ) ;
1092+ GenerateJsonPath ( jsonScalarExpression . Json , returnsText : false , path ) ;
10741093 break ;
10751094
10761095 // No need to cast the output when we expect a string anyway
10771096 case StringTypeMapping :
1078- GenerateJsonPath ( returnsText : true ) ;
1097+ GenerateJsonPath ( jsonScalarExpression . Json , returnsText : true , path ) ;
10791098 break ;
10801099
10811100 // bytea requires special handling, since we encode the binary data as base64 inside the JSON, but that requires a special
10821101 // conversion function to be extracted out to a PG bytea.
10831102 case NpgsqlByteArrayTypeMapping :
10841103 Sql . Append ( "decode(" ) ;
1085- GenerateJsonPath ( returnsText : true ) ;
1104+ GenerateJsonPath ( jsonScalarExpression . Json , returnsText : true , path ) ;
10861105 Sql . Append ( ", 'base64')" ) ;
10871106 break ;
10881107
@@ -1092,33 +1111,20 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
10921111 case NpgsqlArrayTypeMapping arrayMapping :
10931112 Sql . Append ( "(ARRAY(SELECT CAST(element AS " ) . Append ( arrayMapping . ElementTypeMapping . StoreType )
10941113 . Append ( ") FROM jsonb_array_elements_text(" ) ;
1095- GenerateJsonPath ( returnsText : false ) ;
1114+ GenerateJsonPath ( jsonScalarExpression . Json , returnsText : false , path ) ;
10961115 Sql . Append ( ") WITH ORDINALITY AS t(element) ORDER BY ordinality))" ) ;
10971116 break ;
10981117
10991118 default :
11001119 Sql . Append ( "CAST(" ) ;
1101- GenerateJsonPath ( returnsText : true ) ;
1120+ GenerateJsonPath ( jsonScalarExpression . Json , returnsText : true , path ) ;
11021121 Sql . Append ( " AS " ) ;
11031122 Sql . Append ( jsonScalarExpression . TypeMapping ! . StoreType ) ;
11041123 Sql . Append ( ")" ) ;
11051124 break ;
11061125 }
11071126
11081127 return jsonScalarExpression ;
1109-
1110- void GenerateJsonPath ( bool returnsText )
1111- => this . GenerateJsonPath (
1112- jsonScalarExpression . Json ,
1113- returnsText : returnsText ,
1114- jsonScalarExpression . Path . Select (
1115- s => s switch
1116- {
1117- { PropertyName : string propertyName }
1118- => new SqlConstantExpression ( propertyName , _textTypeMapping ??= _typeMappingSource . FindMapping ( typeof ( string ) ) ) ,
1119- { ArrayIndex : SqlExpression arrayIndex } => arrayIndex ,
1120- _ => throw new UnreachableException ( )
1121- } ) . ToList ( ) ) ;
11221128 }
11231129
11241130 /// <summary>
@@ -1148,6 +1154,11 @@ private void GenerateJsonPath(SqlExpression expression, bool returnsText, IReadO
11481154 // Multiple path components
11491155 Sql . Append ( returnsText ? " #>> " : " #> " ) ;
11501156
1157+ GenerateJsonPath ( path ) ;
1158+ }
1159+
1160+ private void GenerateJsonPath ( IReadOnlyList < SqlExpression > path )
1161+ {
11511162 // Use simplified array literal syntax if all path components are constants for cleaner SQL
11521163 if ( path . All ( p => p is SqlConstantExpression { Value : var pathSegment }
11531164 && ( pathSegment is not string s || s . All ( char . IsAsciiLetterOrDigit ) ) ) )
@@ -1173,6 +1184,23 @@ private void GenerateJsonPath(SqlExpression expression, bool returnsText, IReadO
11731184 }
11741185 }
11751186
1187+ /// <summary>
1188+ /// Converts the standard EF <see cref="IReadOnlyList{PathSegment}" /> to an <see cref="IReadOnlyList{SqlExpression}" />
1189+ /// (the EF built-in <see cref="JsonScalarExpression" /> and <see cref="JsonQueryExpression" /> don't support non-constant
1190+ /// property names, but we do via the Npgsql-specific JSON DOM support).
1191+ /// </summary>
1192+ private IReadOnlyList < SqlExpression > ConvertJsonPathSegments ( IReadOnlyList < PathSegment > path )
1193+ => path
1194+ . Select (
1195+ s => s switch
1196+ {
1197+ { PropertyName : string propertyName }
1198+ => new SqlConstantExpression ( propertyName , _textTypeMapping ??= _typeMappingSource . FindMapping ( typeof ( string ) ) ) ,
1199+ { ArrayIndex : SqlExpression arrayIndex } => arrayIndex ,
1200+ _ => throw new UnreachableException ( )
1201+ } )
1202+ . ToList ( ) ;
1203+
11761204 /// <summary>
11771205 /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
11781206 /// the same compatibility standards as public APIs. It may be changed or removed without notice in
0 commit comments