diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs index edb30c447eee814ecf9c0ab3fc723c2dba0a2177..590cc9c4e24c3ade98d2f9b6e6f20079aaad6da5 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs @@ -57,32 +57,32 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator nameof(LTree.IsAncestorOf) => new PostgresBinaryExpression( PostgresExpressionType.Contains, - _sqlExpressionFactory.ApplyTypeMapping(instance!, _ltreeTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(arguments[0], _ltreeTypeMapping), + ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), + ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping), typeof(bool), _boolTypeMapping), nameof(LTree.IsDescendantOf) => new PostgresBinaryExpression( PostgresExpressionType.ContainedBy, - _sqlExpressionFactory.ApplyTypeMapping(instance!, _ltreeTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(arguments[0], _ltreeTypeMapping), + ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), + ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping), typeof(bool), _boolTypeMapping), nameof(LTree.MatchesLQuery) => new PostgresBinaryExpression( PostgresExpressionType.LTreeMatches, - _sqlExpressionFactory.ApplyTypeMapping(instance!, _ltreeTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(arguments[0], _lqueryTypeMapping), + ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), + ApplyTypeMappingOrConvert(arguments[0], _lqueryTypeMapping), typeof(bool), _boolTypeMapping), nameof(LTree.MatchesLTxtQuery) => new PostgresBinaryExpression( PostgresExpressionType.LTreeMatches, - _sqlExpressionFactory.ApplyTypeMapping(instance!, _ltreeTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(arguments[0], _ltxtqueryTypeMapping), + ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), + ApplyTypeMappingOrConvert(arguments[0], _ltxtqueryTypeMapping), typeof(bool), _boolTypeMapping), @@ -171,7 +171,7 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator { return new PostgresBinaryExpression( PostgresExpressionType.LTreeMatchesAny, - _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateInstance), _ltreeTypeMapping), + ApplyTypeMappingOrConvert(Visit(predicateInstance), _ltreeTypeMapping), _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _lqueryArrayTypeMapping), typeof(bool), _boolTypeMapping); @@ -184,7 +184,7 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator return new PostgresBinaryExpression( PostgresExpressionType.Contains, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltreeTypeMapping), + ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping), typeof(bool), _boolTypeMapping); } @@ -196,7 +196,7 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator return new PostgresBinaryExpression( PostgresExpressionType.ContainedBy, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltreeTypeMapping), + ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping), typeof(bool), _boolTypeMapping); } @@ -208,7 +208,7 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator return new PostgresBinaryExpression( PostgresExpressionType.LTreeMatches, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _lqueryTypeMapping), + ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _lqueryTypeMapping), typeof(bool), _boolTypeMapping); } @@ -220,7 +220,7 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator return new PostgresBinaryExpression( PostgresExpressionType.LTreeMatches, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltxtqueryTypeMapping), + ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltxtqueryTypeMapping), typeof(bool), _boolTypeMapping); } @@ -267,7 +267,7 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator return new PostgresBinaryExpression( PostgresExpressionType.LTreeFirstAncestor, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltreeTypeMapping), + ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping), typeof(LTree), _ltreeTypeMapping); } @@ -279,7 +279,7 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator return new PostgresBinaryExpression( PostgresExpressionType.LTreeFirstDescendent, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltreeTypeMapping), + ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping), typeof(LTree), _ltreeTypeMapping); } @@ -291,7 +291,7 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator return new PostgresBinaryExpression( PostgresExpressionType.LTreeFirstMatches, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _lqueryTypeMapping), + ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _lqueryTypeMapping), typeof(LTree), _ltreeTypeMapping); } @@ -303,7 +303,7 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator return new PostgresBinaryExpression( PostgresExpressionType.LTreeFirstMatches, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _ltxtqueryTypeMapping), + ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltxtqueryTypeMapping), typeof(string), _ltreeTypeMapping); } @@ -315,4 +315,18 @@ public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator SqlExpression Visit(Expression expression) => (SqlExpression)sqlTranslatingExpressionVisitor.Visit(expression); } + + // Applying e.g. the LQuery type mapping on a function operator is a bit tricky. + // If it's a constant, we can just apply the mapping: the constant will get rendered as an untyped string literal, and PG will + // coerce it as the function parameter. + // If it's a parameter, we can also just apply the mapping (which causes NpgsqlDbType to be set to LQuery). + // For anything else, we may need an explicit cast to LQuery, e.g. a plain text column or a concatenation between strings; + // apply the default type mapping and then apply an additional Convert node if the resulting mapping isn't what we need. + private SqlExpression ApplyTypeMappingOrConvert(SqlExpression sqlExpression, RelationalTypeMapping typeMapping) + => sqlExpression is SqlConstantExpression or SqlParameterExpression + ? _sqlExpressionFactory.ApplyTypeMapping(sqlExpression, typeMapping) + : _sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpression) is var expressionWithDefaultTypeMapping + && expressionWithDefaultTypeMapping.TypeMapping!.StoreType == typeMapping.StoreType + ? expressionWithDefaultTypeMapping + : _sqlExpressionFactory.Convert(expressionWithDefaultTypeMapping, typeMapping.ClrType, typeMapping); } \ No newline at end of file diff --git a/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs index 6293e18a9bdbc5d5eb4fd634d5deac4f57637731..739f85643ca4d9da1e6377e9ff086a929825c31f 100644 --- a/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs @@ -128,13 +128,44 @@ public void LTree_matches_LQuery() Assert.Equal(4, entity.Id); AssertSql( """ -SELECT l."Id", l."Path", l."PathAsString" +SELECT l."Id", l."Path", l."PathAsString", l."SomeString" FROM "LTreeEntities" AS l WHERE l."Path" ~ '*.Astrophysics' LIMIT 2 """); } + [ConditionalFact] // #2487 + public void LTree_matches_LQuery_with_string_column() + { + using var ctx = CreateContext(); + var entity = ctx.LTreeEntities.Single(l => l.Path.MatchesLQuery(l.SomeString)); + + Assert.Equal(4, entity.Id); + AssertSql( +""" +SELECT l."Id", l."Path", l."PathAsString", l."SomeString" +FROM "LTreeEntities" AS l +WHERE l."Path" ~ l."SomeString"::lquery +LIMIT 2 +"""); + } + + [ConditionalFact] // #2487 + public void LTree_matches_LQuery_with_concat() + { + using var ctx = CreateContext(); + var count = ctx.LTreeEntities.Count(l => l.Path.MatchesLQuery("*.Astrophysics." + l.Id)); + + Assert.Equal(0, count); + AssertSql( +""" +SELECT count(*)::int +FROM "LTreeEntities" AS l +WHERE l."Path" ~ CAST(('*.Astrophysics.' || l."Id"::text) AS lquery) +"""); + } + [ConditionalFact] public void LTree_matches_any_LQuery() { @@ -147,7 +178,7 @@ public void LTree_matches_any_LQuery() """ @__lqueries_0={ '*.Astrophysics', '*.Geology' } (DbType = Object) -SELECT l."Id", l."Path", l."PathAsString" +SELECT l."Id", l."Path", l."PathAsString", l."SomeString" FROM "LTreeEntities" AS l WHERE l."Path" ? @__lqueries_0 LIMIT 2 @@ -178,7 +209,7 @@ public void LTree_concat() Assert.Equal(2, entity.Id); AssertSql( """ -SELECT l."Id", l."Path", l."PathAsString" +SELECT l."Id", l."Path", l."PathAsString", l."SomeString" FROM "LTreeEntities" AS l WHERE (l."Path"::text || '.Astronomy') = 'Top.Science.Astronomy' LIMIT 2 @@ -395,7 +426,7 @@ public void Subpath2() Assert.Equal(4, result.Id); AssertSql( """ -SELECT l."Id", l."Path", l."PathAsString" +SELECT l."Id", l."Path", l."PathAsString", l."SomeString" FROM "LTreeEntities" AS l WHERE nlevel(l."Path") > 2 AND subpath(l."Path", 2) = 'Astronomy.Astrophysics' LIMIT 2 @@ -495,6 +526,7 @@ public static void Seed(LTreeQueryContext context) foreach (var ltreeEntity in ltreeEntities) { ltreeEntity.PathAsString = ltreeEntity.Path; + ltreeEntity.SomeString = "*.Astrophysics"; } context.LTreeEntities.AddRange(ltreeEntities); @@ -512,6 +544,9 @@ public class LTreeEntity [Required] [Column(TypeName = "ltree")] public string PathAsString { get; set; } + + [Required] + public string SomeString { get; set; } } public class LTreeQueryFixture : SharedStoreFixtureBase