@@ -22,11 +22,12 @@ public class NpgsqlQuerySqlGenerator : QuerySqlGenerator
2222 /// </summary>
2323 private readonly bool _reverseNullOrderingEnabled ;
2424
25+ private readonly Version _postgresVersion ;
26+
2527 /// <summary>
26- /// The backend version to target. If null, it means the user hasn't set a compatibility version, and the
27- /// latest should be targeted.
28+ /// True for PG17 and above (JSON_VALUE, JSON_QUERY)
2829 /// </summary>
29- private readonly Version _postgresVersion ;
30+ private readonly bool _useNewJsonFunctions ;
3031
3132 /// <inheritdoc />
3233 public NpgsqlQuerySqlGenerator (
@@ -40,6 +41,7 @@ public NpgsqlQuerySqlGenerator(
4041 _typeMappingSource = typeMappingSource ;
4142 _reverseNullOrderingEnabled = reverseNullOrderingEnabled ;
4243 _postgresVersion = postgresVersion ;
44+ _useNewJsonFunctions = postgresVersion >= new Version ( 17 , 0 ) ;
4345 }
4446
4547 /// <summary>
@@ -1057,58 +1059,161 @@ protected virtual Expression VisitILike(PgILikeExpression likeExpression, bool n
10571059 /// </summary>
10581060 protected override Expression VisitJsonScalar ( JsonScalarExpression jsonScalarExpression )
10591061 {
1060- // TODO: Stop producing empty JsonScalarExpressions , #30768
1062+ // TODO: Stop producing empty JsonValueExpressions , #30768
10611063 var path = jsonScalarExpression . Path ;
10621064 if ( path . Count == 0 )
10631065 {
10641066 Visit ( jsonScalarExpression . Json ) ;
10651067 return jsonScalarExpression ;
10661068 }
10671069
1070+ if ( _useNewJsonFunctions )
1071+ {
1072+ switch ( jsonScalarExpression . TypeMapping )
1073+ {
1074+ case NpgsqlOwnedJsonTypeMapping :
1075+ GenerateJsonValueQuery ( isJsonQuery : true , jsonScalarExpression . Json , jsonScalarExpression . Path , returningType : null ) ;
1076+ return jsonScalarExpression ;
1077+
1078+ // Arrays cannot be extracted with JSON_VALUE(), JSON_QUERY() must be used; but we still use the RETURNING clause
1079+ // to get the value out as a PostgreSQL array rather than as a jsonb.
1080+ case NpgsqlArrayTypeMapping :
1081+ GenerateJsonValueQuery (
1082+ isJsonQuery : true , jsonScalarExpression . Json , jsonScalarExpression . Path ,
1083+ jsonScalarExpression . TypeMapping ! . StoreType ) ;
1084+ return jsonScalarExpression ;
1085+
1086+ // Unfortunately, JSON_VALUE() with RETURNING bytea doesn't seem to perform base64 decoding,
1087+ // see https://www.postgresql.org/message-id/CADT4RqB9y5A58CAxMgWQpKG2QA1pzk3dzAUmNH8bJ9SwMP%3DZnA%40mail.gmail.com
1088+ // So we manually add decoding.
1089+ case NpgsqlByteArrayTypeMapping :
1090+ Sql . Append ( "decode(" ) ;
1091+ GenerateJsonValueQuery ( isJsonQuery : false , jsonScalarExpression . Json , jsonScalarExpression . Path , returningType : null ) ;
1092+ Sql . Append ( ", 'base64')" ) ;
1093+ return jsonScalarExpression ;
1094+
1095+ default :
1096+ GenerateJsonValueQuery (
1097+ isJsonQuery : false , jsonScalarExpression . Json , jsonScalarExpression . Path ,
1098+ jsonScalarExpression . TypeMapping ! . StoreType ) ;
1099+ return jsonScalarExpression ;
1100+ }
1101+ }
1102+
1103+ // We're targeting a PostgreSQL version under 17, so JSON_VALUE() doesn't exist yet. We need to use the legacy JSON path syntax.
10681104 switch ( jsonScalarExpression . TypeMapping )
10691105 {
10701106 // This case is for when a nested JSON entity is being accessed. We want the json/jsonb fragment in this case (not text),
10711107 // so we can perform further JSON operations on it.
10721108 case NpgsqlOwnedJsonTypeMapping :
1073- GenerateJsonPath ( returnsText : false ) ;
1074- break ;
1109+ GenerateLegacyJsonPath ( returnsText : false ) ;
1110+ return jsonScalarExpression ;
10751111
10761112 // No need to cast the output when we expect a string anyway
10771113 case StringTypeMapping :
1078- GenerateJsonPath ( returnsText : true ) ;
1079- break ;
1114+ GenerateLegacyJsonPath ( returnsText : true ) ;
1115+ return jsonScalarExpression ;
10801116
10811117 // bytea requires special handling, since we encode the binary data as base64 inside the JSON, but that requires a special
10821118 // conversion function to be extracted out to a PG bytea.
10831119 case NpgsqlByteArrayTypeMapping :
10841120 Sql . Append ( "decode(" ) ;
1085- GenerateJsonPath ( returnsText : true ) ;
1121+ GenerateLegacyJsonPath ( returnsText : true ) ;
10861122 Sql . Append ( ", 'base64')" ) ;
1087- break ;
1123+ return jsonScalarExpression ;
10881124
10891125 // Arrays require special handling; we cannot simply cast a JSON array (as text) to a PG array ([1,2,3] isn't a valid PG array
10901126 // representation). We use jsonb_array_elements_text to extract the array elements as a set, cast them to their PG element type
10911127 // and then build an array from that.
10921128 case NpgsqlArrayTypeMapping arrayMapping :
10931129 Sql . Append ( "(ARRAY(SELECT CAST(element AS " ) . Append ( arrayMapping . ElementTypeMapping . StoreType )
10941130 . Append ( ") FROM jsonb_array_elements_text(" ) ;
1095- GenerateJsonPath ( returnsText : false ) ;
1131+ GenerateLegacyJsonPath ( returnsText : false ) ;
10961132 Sql . Append ( ") WITH ORDINALITY AS t(element) ORDER BY ordinality))" ) ;
1097- break ;
1133+ return jsonScalarExpression ;
10981134
10991135 default :
11001136 Sql . Append ( "CAST(" ) ;
1101- GenerateJsonPath ( returnsText : true ) ;
1137+ GenerateLegacyJsonPath ( returnsText : true ) ;
11021138 Sql . Append ( " AS " ) ;
11031139 Sql . Append ( jsonScalarExpression . TypeMapping ! . StoreType ) ;
11041140 Sql . Append ( ")" ) ;
1105- break ;
1141+ return jsonScalarExpression ;
11061142 }
11071143
1108- return jsonScalarExpression ;
1144+ void GenerateJsonValueQuery ( bool isJsonQuery , SqlExpression json , IReadOnlyList < PathSegment > path , string ? returningType )
1145+ {
1146+ List < ( string Name , Expression Expression ) > ? parameters = null ;
1147+ var unnamedParameterIndex = 0 ;
1148+
1149+ Sql . Append ( isJsonQuery ? "JSON_QUERY(" : "JSON_VALUE(" ) ;
1150+ Visit ( json ) ;
1151+ Sql . Append ( ", '$" ) ;
1152+
1153+ foreach ( var pathSegment in path )
1154+ {
1155+ switch ( pathSegment )
1156+ {
1157+ case { PropertyName : string propertyName } :
1158+ Sql . Append ( "." ) . Append ( Dependencies . SqlGenerationHelper . DelimitJsonPathElement ( propertyName ) ) ;
1159+ break ;
1160+
1161+ case { ArrayIndex : SqlExpression arrayIndex } :
1162+ Sql . Append ( "[" ) ;
1163+
1164+ if ( arrayIndex is SqlConstantExpression )
1165+ {
1166+ Visit ( arrayIndex ) ;
1167+ }
1168+ else
1169+ {
1170+ parameters ??= new ( ) ;
1171+ var parameterName = arrayIndex is SqlParameterExpression p ? p . InvariantName : ( "p" + ++ unnamedParameterIndex ) ;
1172+ parameters . Add ( ( parameterName , arrayIndex ) ) ;
1173+ Sql . Append ( "$" ) . Append ( parameterName ) ;
1174+ }
1175+
1176+ Sql . Append ( "]" ) ;
1177+ break ;
11091178
1110- void GenerateJsonPath ( bool returnsText )
1111- => this . GenerateJsonPath (
1179+ default :
1180+ throw new ArgumentOutOfRangeException ( ) ;
1181+ }
1182+ }
1183+
1184+ Sql . Append ( "'" ) ;
1185+
1186+ if ( parameters is not null )
1187+ {
1188+ Sql . Append ( " PASSING " ) ;
1189+
1190+ var isFirst = true ;
1191+ foreach ( var ( name , expression ) in parameters )
1192+ {
1193+ if ( isFirst )
1194+ {
1195+ isFirst = false ;
1196+ }
1197+ else
1198+ {
1199+ Sql . Append ( ", " ) ;
1200+ }
1201+
1202+ Visit ( expression ) ;
1203+ Sql . Append ( " AS " ) . Append ( name ) ;
1204+ }
1205+ }
1206+
1207+ if ( returningType is not null )
1208+ {
1209+ Sql . Append ( " RETURNING " ) . Append ( returningType ) ;
1210+ }
1211+
1212+ Sql . Append ( ")" ) ;
1213+ }
1214+
1215+ void GenerateLegacyJsonPath ( bool returnsText )
1216+ => this . GenerateLegacyJsonPath (
11121217 jsonScalarExpression . Json ,
11131218 returnsText : returnsText ,
11141219 jsonScalarExpression . Path . Select (
@@ -1130,11 +1235,12 @@ void GenerateJsonPath(bool returnsText)
11301235 /// </returns>
11311236 protected virtual Expression VisitJsonPathTraversal ( PgJsonTraversalExpression expression )
11321237 {
1133- GenerateJsonPath ( expression . Expression , expression . ReturnsText , expression . Path ) ;
1238+ // TODO: Consider also implementing via JsonValueExpression and using JSON_VALUE?
1239+ GenerateLegacyJsonPath ( expression . Expression , expression . ReturnsText , expression . Path ) ;
11341240 return expression ;
11351241 }
11361242
1137- private void GenerateJsonPath ( SqlExpression expression , bool returnsText , IReadOnlyList < SqlExpression > path )
1243+ private void GenerateLegacyJsonPath ( SqlExpression expression , bool returnsText , IReadOnlyList < SqlExpression > path )
11381244 {
11391245 Visit ( expression ) ;
11401246
@@ -1451,6 +1557,12 @@ protected override bool RequiresParentheses(SqlExpression outerExpression, SqlEx
14511557 case PgUnknownBinaryExpression :
14521558 return true ;
14531559
1560+ // In PG 17 or above, we translate JsonScalarExpression to JSON_VALUE() which does not require parentheses.
1561+ // Before that, we translate to x ->> y which does.
1562+ // Note that we also add parentheses when the outer is an index operation, since e.g. JSON_QUERY(...)[0] is invalid.
1563+ case JsonScalarExpression when outerExpression is not PgArrayIndexExpression and not PgArraySliceExpression :
1564+ return ! _useNewJsonFunctions ;
1565+
14541566 default :
14551567 return base . RequiresParentheses ( outerExpression , innerExpression ) ;
14561568 }
0 commit comments