Skip to content

Commit 9b2a90c

Browse files
committed
Correct LTree type mapping logic
Fixes #2487 (cherry picked from commit ddc63fc)
1 parent 6780f7e commit 9b2a90c

2 files changed

Lines changed: 66 additions & 21 deletions

File tree

src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,32 +70,32 @@ public NpgsqlLTreeTranslator(
7070
nameof(LTree.IsAncestorOf)
7171
=> new PostgresBinaryExpression(
7272
PostgresExpressionType.Contains,
73-
_sqlExpressionFactory.ApplyTypeMapping(instance!, _ltreeTypeMapping),
74-
_sqlExpressionFactory.ApplyTypeMapping(arguments[0], _ltreeTypeMapping),
73+
ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping),
74+
ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping),
7575
typeof(bool),
7676
_boolTypeMapping),
7777

7878
nameof(LTree.IsDescendantOf)
7979
=> new PostgresBinaryExpression(
8080
PostgresExpressionType.ContainedBy,
81-
_sqlExpressionFactory.ApplyTypeMapping(instance!, _ltreeTypeMapping),
82-
_sqlExpressionFactory.ApplyTypeMapping(arguments[0], _ltreeTypeMapping),
81+
ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping),
82+
ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping),
8383
typeof(bool),
8484
_boolTypeMapping),
8585

8686
nameof(LTree.MatchesLQuery)
8787
=> new PostgresBinaryExpression(
8888
PostgresExpressionType.LTreeMatches,
89-
_sqlExpressionFactory.ApplyTypeMapping(instance!, _ltreeTypeMapping),
90-
_sqlExpressionFactory.ApplyTypeMapping(arguments[0], _lqueryTypeMapping),
89+
ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping),
90+
ApplyTypeMappingOrConvert(arguments[0], _lqueryTypeMapping),
9191
typeof(bool),
9292
_boolTypeMapping),
9393

9494
nameof(LTree.MatchesLTxtQuery)
9595
=> new PostgresBinaryExpression(
9696
PostgresExpressionType.LTreeMatches,
97-
_sqlExpressionFactory.ApplyTypeMapping(instance!, _ltreeTypeMapping),
98-
_sqlExpressionFactory.ApplyTypeMapping(arguments[0], _ltxtqueryTypeMapping),
97+
ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping),
98+
ApplyTypeMappingOrConvert(arguments[0], _ltxtqueryTypeMapping),
9999
typeof(bool),
100100
_boolTypeMapping),
101101

@@ -185,7 +185,7 @@ arguments[1] is LambdaExpression wherePredicate &&
185185
{
186186
return new PostgresBinaryExpression(
187187
PostgresExpressionType.LTreeMatchesAny,
188-
_sqlExpressionFactory.ApplyTypeMapping(Visit(predicateInstance), _ltreeTypeMapping),
188+
ApplyTypeMappingOrConvert(Visit(predicateInstance), _ltreeTypeMapping),
189189
_sqlExpressionFactory.ApplyTypeMapping(Visit(array), _lqueryArrayTypeMapping),
190190
typeof(bool),
191191
_boolTypeMapping);
@@ -198,7 +198,7 @@ arguments[1] is LambdaExpression wherePredicate &&
198198
return new PostgresBinaryExpression(
199199
PostgresExpressionType.Contains,
200200
_sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping),
201-
_sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltreeTypeMapping),
201+
ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping),
202202
typeof(bool),
203203
_boolTypeMapping);
204204
}
@@ -210,7 +210,7 @@ arguments[1] is LambdaExpression wherePredicate &&
210210
return new PostgresBinaryExpression(
211211
PostgresExpressionType.ContainedBy,
212212
_sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping),
213-
_sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltreeTypeMapping),
213+
ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping),
214214
typeof(bool),
215215
_boolTypeMapping);
216216
}
@@ -222,7 +222,7 @@ arguments[1] is LambdaExpression wherePredicate &&
222222
return new PostgresBinaryExpression(
223223
PostgresExpressionType.LTreeMatches,
224224
_sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping),
225-
_sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _lqueryTypeMapping),
225+
ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _lqueryTypeMapping),
226226
typeof(bool),
227227
_boolTypeMapping);
228228
}
@@ -234,7 +234,7 @@ arguments[1] is LambdaExpression wherePredicate &&
234234
return new PostgresBinaryExpression(
235235
PostgresExpressionType.LTreeMatches,
236236
_sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping),
237-
_sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltxtqueryTypeMapping),
237+
ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltxtqueryTypeMapping),
238238
typeof(bool),
239239
_boolTypeMapping);
240240
}
@@ -281,7 +281,7 @@ arguments[1] is LambdaExpression wherePredicate &&
281281
return new PostgresBinaryExpression(
282282
PostgresExpressionType.LTreeFirstAncestor,
283283
_sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping),
284-
_sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltreeTypeMapping),
284+
ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping),
285285
typeof(LTree),
286286
_ltreeTypeMapping);
287287
}
@@ -293,7 +293,7 @@ arguments[1] is LambdaExpression wherePredicate &&
293293
return new PostgresBinaryExpression(
294294
PostgresExpressionType.LTreeFirstDescendent,
295295
_sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping),
296-
_sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltreeTypeMapping),
296+
ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping),
297297
typeof(LTree),
298298
_ltreeTypeMapping);
299299
}
@@ -305,7 +305,7 @@ arguments[1] is LambdaExpression wherePredicate &&
305305
return new PostgresBinaryExpression(
306306
PostgresExpressionType.LTreeFirstMatches,
307307
_sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping),
308-
_sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _lqueryTypeMapping),
308+
ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _lqueryTypeMapping),
309309
typeof(LTree),
310310
_ltreeTypeMapping);
311311
}
@@ -317,7 +317,7 @@ arguments[1] is LambdaExpression wherePredicate &&
317317
return new PostgresBinaryExpression(
318318
PostgresExpressionType.LTreeFirstMatches,
319319
_sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping),
320-
_sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltxtqueryTypeMapping),
320+
ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltxtqueryTypeMapping),
321321
typeof(string),
322322
_ltreeTypeMapping);
323323
}
@@ -329,4 +329,18 @@ arguments[1] is LambdaExpression wherePredicate &&
329329
SqlExpression Visit(Expression expression)
330330
=> (SqlExpression)sqlTranslatingExpressionVisitor.Visit(expression);
331331
}
332+
333+
// Applying e.g. the LQuery type mapping on a function operator is a bit tricky.
334+
// If it's a constant, we can just apply the mapping: the constant will get rendered as an untyped string literal, and PG will
335+
// coerce it as the function parameter.
336+
// If it's a parameter, we can also just apply the mapping (which causes NpgsqlDbType to be set to LQuery).
337+
// For anything else, we may need an explicit cast to LQuery, e.g. a plain text column or a concatenation between strings;
338+
// apply the default type mapping and then apply an additional Convert node if the resulting mapping isn't what we need.
339+
private SqlExpression ApplyTypeMappingOrConvert(SqlExpression sqlExpression, RelationalTypeMapping typeMapping)
340+
=> sqlExpression is SqlConstantExpression or SqlParameterExpression
341+
? _sqlExpressionFactory.ApplyTypeMapping(sqlExpression, typeMapping)
342+
: _sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpression) is var expressionWithDefaultTypeMapping
343+
&& expressionWithDefaultTypeMapping.TypeMapping!.StoreType == typeMapping.StoreType
344+
? expressionWithDefaultTypeMapping
345+
: _sqlExpressionFactory.Convert(expressionWithDefaultTypeMapping, typeMapping.ClrType, typeMapping);
332346
}

test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,39 @@ public void LTree_matches_LQuery()
120120

121121
Assert.Equal(4, entity.Id);
122122
AssertSql(
123-
@"SELECT l.""Id"", l.""Path"", l.""PathAsString""
123+
@"SELECT l.""Id"", l.""Path"", l.""PathAsString"", l.""SomeString""
124124
FROM ""LTreeEntities"" AS l
125125
WHERE l.""Path"" ~ '*.Astrophysics'
126126
LIMIT 2");
127127
}
128128

129+
[ConditionalFact] // #2487
130+
public void LTree_matches_LQuery_with_string_column()
131+
{
132+
using var ctx = CreateContext();
133+
var entity = ctx.LTreeEntities.Single(l => l.Path.MatchesLQuery(l.SomeString));
134+
135+
Assert.Equal(4, entity.Id);
136+
AssertSql(
137+
@"SELECT l.""Id"", l.""Path"", l.""PathAsString"", l.""SomeString""
138+
FROM ""LTreeEntities"" AS l
139+
WHERE l.""Path"" ~ l.""SomeString""::lquery
140+
LIMIT 2");
141+
}
142+
143+
[ConditionalFact] // #2487
144+
public void LTree_matches_LQuery_with_concat()
145+
{
146+
using var ctx = CreateContext();
147+
var count = ctx.LTreeEntities.Count(l => l.Path.MatchesLQuery("*.Astrophysics." + l.Id));
148+
149+
Assert.Equal(0, count);
150+
AssertSql(
151+
@"SELECT COUNT(*)::INT
152+
FROM ""LTreeEntities"" AS l
153+
WHERE l.""Path"" ~ CAST(('*.Astrophysics.' || l.""Id""::text) AS lquery)");
154+
}
155+
129156
[ConditionalFact]
130157
public void LTree_matches_any_LQuery()
131158
{
@@ -137,7 +164,7 @@ public void LTree_matches_any_LQuery()
137164
AssertSql(
138165
@"@__lqueries_0={ '*.Astrophysics', '*.Geology' } (DbType = Object)
139166
140-
SELECT l.""Id"", l.""Path"", l.""PathAsString""
167+
SELECT l.""Id"", l.""Path"", l.""PathAsString"", l.""SomeString""
141168
FROM ""LTreeEntities"" AS l
142169
WHERE l.""Path"" ? @__lqueries_0
143170
LIMIT 2");
@@ -164,7 +191,7 @@ public void LTree_concat()
164191

165192
Assert.Equal(2, entity.Id);
166193
AssertSql(
167-
@"SELECT l.""Id"", l.""Path"", l.""PathAsString""
194+
@"SELECT l.""Id"", l.""Path"", l.""PathAsString"", l.""SomeString""
168195
FROM ""LTreeEntities"" AS l
169196
WHERE (l.""Path""::text || '.Astronomy') = 'Top.Science.Astronomy'
170197
LIMIT 2");
@@ -357,7 +384,7 @@ public void Subpath2()
357384

358385
Assert.Equal(4, result.Id);
359386
AssertSql(
360-
@"SELECT l.""Id"", l.""Path"", l.""PathAsString""
387+
@"SELECT l.""Id"", l.""Path"", l.""PathAsString"", l.""SomeString""
361388
FROM ""LTreeEntities"" AS l
362389
WHERE (nlevel(l.""Path"") > 2) AND (subpath(l.""Path"", 2) = 'Astronomy.Astrophysics')
363390
LIMIT 2");
@@ -451,6 +478,7 @@ public static void Seed(LTreeQueryContext context)
451478
foreach (var ltreeEntity in ltreeEntities)
452479
{
453480
ltreeEntity.PathAsString = ltreeEntity.Path;
481+
ltreeEntity.SomeString = "*.Astrophysics";
454482
}
455483

456484
context.LTreeEntities.AddRange(ltreeEntities);
@@ -468,6 +496,9 @@ public class LTreeEntity
468496
[Required]
469497
[Column(TypeName = "ltree")]
470498
public string PathAsString { get; set; }
499+
500+
[Required]
501+
public string SomeString { get; set; }
471502
}
472503

473504
public class LTreeQueryFixture : SharedStoreFixtureBase<LTreeQueryContext>

0 commit comments

Comments
 (0)