1
1
using System ;
2
2
using System . Collections . Generic ;
3
+ using System . Diagnostics ;
3
4
using System . Linq ;
4
5
using System . Reflection ;
5
6
using JetBrains . Annotations ;
8
9
using Microsoft . EntityFrameworkCore . Query ;
9
10
using Microsoft . EntityFrameworkCore . Query . SqlExpressions ;
10
11
using Microsoft . EntityFrameworkCore . Storage ;
12
+ using Npgsql . EntityFrameworkCore . PostgreSQL . Internal ;
11
13
using Npgsql . EntityFrameworkCore . PostgreSQL . Query . Expressions . Internal ;
12
14
using Npgsql . EntityFrameworkCore . PostgreSQL . Storage . Internal . Mapping ;
13
15
using static Npgsql . EntityFrameworkCore . PostgreSQL . Utilities . Statics ;
@@ -26,7 +28,7 @@ public class NpgsqlArrayTranslator : IMethodCallTranslator, IMemberTranslator
26
28
typeof ( Enumerable ) . GetTypeInfo ( ) . GetMethods ( BindingFlags . Public | BindingFlags . Static | BindingFlags . DeclaredOnly )
27
29
. Single ( m => m . Name == nameof ( Enumerable . SequenceEqual ) && m . GetParameters ( ) . Length == 2 ) ;
28
30
29
- static readonly MethodInfo Contains =
31
+ static readonly MethodInfo EnumerableContains =
30
32
typeof ( Enumerable ) . GetTypeInfo ( ) . GetMethods ( BindingFlags . Public | BindingFlags . Static | BindingFlags . DeclaredOnly )
31
33
. Single ( m => m . Name == nameof ( Enumerable . Contains ) && m . GetParameters ( ) . Length == 2 ) ;
32
34
@@ -54,108 +56,117 @@ public virtual SqlExpression Translate(
54
56
IReadOnlyList < SqlExpression > arguments ,
55
57
IDiagnosticsLogger < DbLoggerCategory . Query > logger )
56
58
{
57
- if ( instance != null && instance . Type . IsGenericList ( ) && method . Name == "get_Item" && arguments . Count == 1 )
59
+ if ( instance ? . Type . IsGenericList ( ) == true && ! IsMappedToNonArray ( instance ) )
58
60
{
59
- return
60
- // Try translating indexing inside json column
61
- _jsonPocoTranslator . TranslateMemberAccess ( instance , arguments [ 0 ] , method . ReturnType ) ??
62
- // Other types should be subscriptable - but PostgreSQL arrays are 1-based, so adjust the index.
63
- _sqlExpressionFactory . ArrayIndex ( instance , GenerateOneBasedIndexExpression ( arguments [ 0 ] ) ) ;
64
- }
65
-
66
- if ( arguments . Count == 0 )
67
- return null ;
61
+ // Translate list[i]. Note that array[i] is translated by NpgsqlSqlTranslatingExpressionVisitor.VisitBinary (ArrayIndex)
62
+ if ( method . Name == "get_Item" && arguments . Count == 1 )
63
+ {
64
+ return
65
+ // Try translating indexing inside json column
66
+ _jsonPocoTranslator . TranslateMemberAccess ( instance , arguments [ 0 ] , method . ReturnType ) ??
67
+ // Other types should be subscriptable - but PostgreSQL arrays are 1-based, so adjust the index.
68
+ _sqlExpressionFactory . ArrayIndex ( instance , GenerateOneBasedIndexExpression ( arguments [ 0 ] ) ) ;
69
+ }
68
70
69
- var array = arguments [ 0 ] ;
70
- if ( ! array . Type . TryGetElementType ( out var elementType ) )
71
- return null ; // Not an array/list
71
+ return TranslateCommon ( instance , arguments ) ;
72
+ }
72
73
73
- // The array/list CLR type may be mapped to a non-array database type (e.g. byte[] to bytea, or just
74
- // value converters). Make sure we're dealing with an array
75
- // Regardless of CLR type, we may be dealing with a non-array database type (e.g. via value converters).
76
- if ( array . TypeMapping is RelationalTypeMapping typeMapping &&
77
- ! ( typeMapping is NpgsqlArrayTypeMapping ) && ! ( typeMapping is NpgsqlJsonTypeMapping ) )
74
+ if ( instance is null && arguments . Count > 0 && arguments [ 0 ] . Type . IsArrayOrGenericList ( ) && ! IsMappedToNonArray ( arguments [ 0 ] ) )
78
75
{
79
- return null ;
76
+ // Extension method over an array or list
77
+ if ( method . IsClosedFormOf ( SequenceEqual ) && arguments [ 1 ] . Type . IsArray )
78
+ return _sqlExpressionFactory . Equal ( arguments [ 0 ] , arguments [ 1 ] ) ;
79
+
80
+ return TranslateCommon ( arguments [ 0 ] , arguments . Slice ( 1 ) ) ;
80
81
}
81
82
82
- if ( method . IsClosedFormOf ( SequenceEqual ) && arguments [ 1 ] . Type . IsArray )
83
- return _sqlExpressionFactory . Equal ( array , arguments [ 1 ] ) ;
83
+ // Not an array/list
84
+ return null ;
84
85
85
- // Predicate-less Any - translate to a simple length check.
86
- if ( method . IsClosedFormOf ( EnumerableAnyWithoutPredicate ) )
87
- {
88
- return _sqlExpressionFactory . GreaterThan (
89
- _jsonPocoTranslator . TranslateArrayLength ( array ) ??
90
- _sqlExpressionFactory . Function (
91
- "cardinality" ,
92
- arguments ,
93
- nullable : true ,
94
- argumentsPropagateNullability : TrueArrays [ 1 ] ,
95
- typeof ( int ) ) ,
96
- _sqlExpressionFactory . Constant ( 0 ) ) ;
97
- }
86
+ // The array/list CLR type may be mapped to a non-array database type (e.g. byte[] to bytea, or just
87
+ // value converters) - we don't want to translate for those cases.
88
+ static bool IsMappedToNonArray ( SqlExpression arrayOrList )
89
+ => arrayOrList . TypeMapping is RelationalTypeMapping typeMapping &&
90
+ typeMapping is not ( NpgsqlArrayTypeMapping or NpgsqlJsonTypeMapping ) ;
98
91
99
- // Note that .Where(e => new[] { "a", "b", "c" }.Any(p => e.SomeText == p)))
100
- // is pattern-matched in AllAnyToContainsRewritingExpressionVisitor, which transforms it to
101
- // new[] { "a", "b", "c" }.Contains(e.Some Text).
102
-
103
- if ( method . IsClosedFormOf ( Contains ) &&
104
- (
105
- // Handle either parameters (no mapping but supported CLR type), or array columns. We specifically
106
- // don't want to translate if the type mapping is bytea (CLR type is array, but not an array in
107
- // the database).
108
- array . TypeMapping == null && _typeMappingSource . FindMapping ( array . Type ) != null ||
109
- array . TypeMapping is NpgsqlArrayTypeMapping
110
- ) &&
111
- // Exclude arrays/lists over Nullable<T> since the ADO layer doesn't handle them (but will in 5.0)
112
- Nullable . GetUnderlyingType ( elementType ) == null )
92
+ SqlExpression TranslateCommon ( SqlExpression arrayOrList , IReadOnlyList < SqlExpression > arguments )
113
93
{
114
- var item = arguments [ 1 ] ;
94
+ // Predicate-less Any - translate to a simple length check.
95
+ if ( method . IsClosedFormOf ( EnumerableAnyWithoutPredicate ) )
96
+ {
97
+ return _sqlExpressionFactory . GreaterThan (
98
+ _jsonPocoTranslator . TranslateArrayLength ( arrayOrList ) ??
99
+ _sqlExpressionFactory . Function (
100
+ "cardinality" ,
101
+ new [ ] { arrayOrList } ,
102
+ nullable : true ,
103
+ argumentsPropagateNullability : TrueArrays [ 1 ] ,
104
+ typeof ( int ) ) ,
105
+ _sqlExpressionFactory . Constant ( 0 ) ) ;
106
+ }
115
107
116
- switch ( array )
108
+ // Note that .Where(e => new[] { "a", "b", "c" }.Any(p => e.SomeText == p)))
109
+ // is pattern-matched in AllAnyToContainsRewritingExpressionVisitor, which transforms it to
110
+ // new[] { "a", "b", "c" }.Contains(e.Some Text).
111
+
112
+ if ( ( method . IsClosedFormOf ( EnumerableContains ) || // Enumerable.Contains extension method
113
+ method . Name == nameof ( List < int > . Contains ) && method . DeclaringType . IsGenericList ( ) &&
114
+ method . GetParameters ( ) . Length == 1 )
115
+ &&
116
+ (
117
+ // Handle either parameters (no mapping but supported CLR type), or array columns. We specifically
118
+ // don't want to translate if the type mapping is bytea (CLR type is array, but not an array in
119
+ // the database).
120
+ arrayOrList . TypeMapping == null && _typeMappingSource . FindMapping ( arrayOrList . Type ) != null ||
121
+ arrayOrList . TypeMapping is NpgsqlArrayTypeMapping
122
+ ) )
117
123
{
118
- // When the array is a column, we translate to array @> ARRAY[item]. GIN indexes
119
- // on array are used, but null semantics is impossible without preventing index use.
120
- case ColumnExpression _:
121
- if ( item is SqlConstantExpression constant && constant . Value is null )
124
+ var item = arguments [ 0 ] ;
125
+
126
+ switch ( arrayOrList )
122
127
{
123
- // We special-case null constant item and use array_position instead, since it does
124
- // nulls correctly (but doesn't use indexes)
125
- // TODO: once lambda-based caching is implemented, move this to NpgsqlSqlNullabilityProcessor
126
- // (https://github.com/dotnet/efcore/issues/17598) and do for parameters as well.
127
- return _sqlExpressionFactory . IsNotNull (
128
- _sqlExpressionFactory . Function (
129
- "array_position" ,
130
- new [ ] { array , item } ,
131
- nullable : true ,
132
- argumentsPropagateNullability : FalseArrays [ 2 ] ,
133
- typeof ( int ) ) ) ;
128
+ // When the array is a column, we translate to array @> ARRAY[item]. GIN indexes
129
+ // on array are used, but null semantics is impossible without preventing index use.
130
+ case ColumnExpression _:
131
+ if ( item is SqlConstantExpression constant && constant . Value is null )
132
+ {
133
+ // We special-case null constant item and use array_position instead, since it does
134
+ // nulls correctly (but doesn't use indexes)
135
+ // TODO: once lambda-based caching is implemented, move this to NpgsqlSqlNullabilityProcessor
136
+ // (https://github.com/dotnet/efcore/issues/17598) and do for parameters as well.
137
+ return _sqlExpressionFactory . IsNotNull (
138
+ _sqlExpressionFactory . Function (
139
+ "array_position" ,
140
+ new [ ] { arrayOrList , item } ,
141
+ nullable : true ,
142
+ argumentsPropagateNullability : FalseArrays [ 2 ] ,
143
+ typeof ( int ) ) ) ;
144
+ }
145
+
146
+ return _sqlExpressionFactory . Contains ( arrayOrList ,
147
+ _sqlExpressionFactory . NewArrayOrConstant ( new [ ] { item } , arrayOrList . Type ) ) ;
148
+
149
+ // Don't do anything PG-specific for constant arrays since the general EF Core mechanism is fine
150
+ // for that case: item IN (1, 2, 3).
151
+ // After https://github.com/aspnet/EntityFrameworkCore/issues/16375 is done we may not need the
152
+ // check any more.
153
+ case SqlConstantExpression _:
154
+ return null ;
155
+
156
+ // For ParameterExpression, and for all other cases - e.g. array returned from some function -
157
+ // translate to e.SomeText = ANY (@p). This is superior to the general solution which will expand
158
+ // parameters to constants, since non-PG SQL does not support arrays.
159
+ // Note that this will allow indexes on the item to be used.
160
+ default :
161
+ return _sqlExpressionFactory . Any ( item , arrayOrList , PostgresAnyOperatorType . Equal ) ;
134
162
}
135
-
136
- return _sqlExpressionFactory . Contains ( array ,
137
- _sqlExpressionFactory . NewArrayOrConstant ( new [ ] { item } , array . Type ) ) ;
138
-
139
- // Don't do anything PG-specific for constant arrays since the general EF Core mechanism is fine
140
- // for that case: item IN (1, 2, 3).
141
- // After https://github.com/aspnet/EntityFrameworkCore/issues/16375 is done we may not need the
142
- // check any more.
143
- case SqlConstantExpression _:
144
- return null ;
145
-
146
- // For ParameterExpression, and for all other cases - e.g. array returned from some function -
147
- // translate to e.SomeText = ANY (@p). This is superior to the general solution which will expand
148
- // parameters to constants, since non-PG SQL does not support arrays.
149
- // Note that this will allow indexes on the item to be used.
150
- default :
151
- return _sqlExpressionFactory . Any ( item , array , PostgresAnyOperatorType . Equal ) ;
152
163
}
153
- }
154
164
155
- // Note: we also translate .Where(e => new[] { "a", "b", "c" }.Any(p => EF.Functions.Like(e.SomeText, p)))
156
- // to LIKE ANY (...). See NpgsqlSqlTranslatingExpressionVisitor.VisitArrayMethodCall.
165
+ // Note: we also translate .Where(e => new[] { "a", "b", "c" }.Any(p => EF.Functions.Like(e.SomeText, p)))
166
+ // to LIKE ANY (...). See NpgsqlSqlTranslatingExpressionVisitor.VisitArrayMethodCall.
157
167
158
- return null ;
168
+ return null ;
169
+ }
159
170
}
160
171
161
172
public virtual SqlExpression Translate ( SqlExpression instance ,
0 commit comments