@@ -22,11 +22,12 @@ public class NpgsqlQuerySqlGenerator : QuerySqlGenerator
22
22
/// </summary>
23
23
private readonly bool _reverseNullOrderingEnabled ;
24
24
25
+ private readonly Version _postgresVersion ;
26
+
25
27
/// <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)
28
29
/// </summary>
29
- private readonly Version _postgresVersion ;
30
+ private readonly bool _useNewJsonFunctions ;
30
31
31
32
/// <inheritdoc />
32
33
public NpgsqlQuerySqlGenerator (
@@ -40,6 +41,7 @@ public NpgsqlQuerySqlGenerator(
40
41
_typeMappingSource = typeMappingSource ;
41
42
_reverseNullOrderingEnabled = reverseNullOrderingEnabled ;
42
43
_postgresVersion = postgresVersion ;
44
+ _useNewJsonFunctions = postgresVersion >= new Version ( 17 , 0 ) ;
43
45
}
44
46
45
47
/// <summary>
@@ -1057,58 +1059,161 @@ protected virtual Expression VisitILike(PgILikeExpression likeExpression, bool n
1057
1059
/// </summary>
1058
1060
protected override Expression VisitJsonScalar ( JsonScalarExpression jsonScalarExpression )
1059
1061
{
1060
- // TODO: Stop producing empty JsonScalarExpressions , #30768
1062
+ // TODO: Stop producing empty JsonValueExpressions , #30768
1061
1063
var path = jsonScalarExpression . Path ;
1062
1064
if ( path . Count == 0 )
1063
1065
{
1064
1066
Visit ( jsonScalarExpression . Json ) ;
1065
1067
return jsonScalarExpression ;
1066
1068
}
1067
1069
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.
1068
1104
switch ( jsonScalarExpression . TypeMapping )
1069
1105
{
1070
1106
// This case is for when a nested JSON entity is being accessed. We want the json/jsonb fragment in this case (not text),
1071
1107
// so we can perform further JSON operations on it.
1072
1108
case NpgsqlOwnedJsonTypeMapping :
1073
- GenerateJsonPath ( returnsText : false ) ;
1074
- break ;
1109
+ GenerateLegacyJsonPath ( returnsText : false ) ;
1110
+ return jsonScalarExpression ;
1075
1111
1076
1112
// No need to cast the output when we expect a string anyway
1077
1113
case StringTypeMapping :
1078
- GenerateJsonPath ( returnsText : true ) ;
1079
- break ;
1114
+ GenerateLegacyJsonPath ( returnsText : true ) ;
1115
+ return jsonScalarExpression ;
1080
1116
1081
1117
// bytea requires special handling, since we encode the binary data as base64 inside the JSON, but that requires a special
1082
1118
// conversion function to be extracted out to a PG bytea.
1083
1119
case NpgsqlByteArrayTypeMapping :
1084
1120
Sql . Append ( "decode(" ) ;
1085
- GenerateJsonPath ( returnsText : true ) ;
1121
+ GenerateLegacyJsonPath ( returnsText : true ) ;
1086
1122
Sql . Append ( ", 'base64')" ) ;
1087
- break ;
1123
+ return jsonScalarExpression ;
1088
1124
1089
1125
// 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
1090
1126
// representation). We use jsonb_array_elements_text to extract the array elements as a set, cast them to their PG element type
1091
1127
// and then build an array from that.
1092
1128
case NpgsqlArrayTypeMapping arrayMapping :
1093
1129
Sql . Append ( "(ARRAY(SELECT CAST(element AS " ) . Append ( arrayMapping . ElementTypeMapping . StoreType )
1094
1130
. Append ( ") FROM jsonb_array_elements_text(" ) ;
1095
- GenerateJsonPath ( returnsText : false ) ;
1131
+ GenerateLegacyJsonPath ( returnsText : false ) ;
1096
1132
Sql . Append ( ") WITH ORDINALITY AS t(element) ORDER BY ordinality))" ) ;
1097
- break ;
1133
+ return jsonScalarExpression ;
1098
1134
1099
1135
default :
1100
1136
Sql . Append ( "CAST(" ) ;
1101
- GenerateJsonPath ( returnsText : true ) ;
1137
+ GenerateLegacyJsonPath ( returnsText : true ) ;
1102
1138
Sql . Append ( " AS " ) ;
1103
1139
Sql . Append ( jsonScalarExpression . TypeMapping ! . StoreType ) ;
1104
1140
Sql . Append ( ")" ) ;
1105
- break ;
1141
+ return jsonScalarExpression ;
1106
1142
}
1107
1143
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 ;
1109
1178
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 (
1112
1217
jsonScalarExpression . Json ,
1113
1218
returnsText : returnsText ,
1114
1219
jsonScalarExpression . Path . Select (
@@ -1130,11 +1235,12 @@ void GenerateJsonPath(bool returnsText)
1130
1235
/// </returns>
1131
1236
protected virtual Expression VisitJsonPathTraversal ( PgJsonTraversalExpression expression )
1132
1237
{
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 ) ;
1134
1240
return expression ;
1135
1241
}
1136
1242
1137
- private void GenerateJsonPath ( SqlExpression expression , bool returnsText , IReadOnlyList < SqlExpression > path )
1243
+ private void GenerateLegacyJsonPath ( SqlExpression expression , bool returnsText , IReadOnlyList < SqlExpression > path )
1138
1244
{
1139
1245
Visit ( expression ) ;
1140
1246
@@ -1451,6 +1557,12 @@ protected override bool RequiresParentheses(SqlExpression outerExpression, SqlEx
1451
1557
case PgUnknownBinaryExpression :
1452
1558
return true ;
1453
1559
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
+
1454
1566
default :
1455
1567
return base . RequiresParentheses ( outerExpression , innerExpression ) ;
1456
1568
}
0 commit comments