diff --git a/src/EFCore.Ydb/src/Extensions/YdbDbFunctionsExtension.cs b/src/EFCore.Ydb/src/Extensions/YdbDbFunctionsExtension.cs new file mode 100644 index 00000000..3f6971f9 --- /dev/null +++ b/src/EFCore.Ydb/src/Extensions/YdbDbFunctionsExtension.cs @@ -0,0 +1,11 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace EntityFrameworkCore.Ydb.Extensions; + +public static class YdbDbFunctionsExtension +{ + public const string InvalidCallMessage = "This function is designed only for LINQ queries"; + public static bool ILike(this DbFunctions dbFunctions, string match, string pattern) + => throw new NotSupportedException(InvalidCallMessage); +} diff --git a/src/EFCore.Ydb/src/Query/Expressions/YdbILikeExpression.cs b/src/EFCore.Ydb/src/Query/Expressions/YdbILikeExpression.cs new file mode 100644 index 00000000..2ec28819 --- /dev/null +++ b/src/EFCore.Ydb/src/Query/Expressions/YdbILikeExpression.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.Ydb.Query.Expressions; + +public class YdbILikeExpression: SqlExpression +{ + public const string ILikeConst = "ILIKE"; + public const string ILikeWithSpacesConst = " " + ILikeConst + " "; + + public virtual SqlExpression Match { get; } + + public virtual SqlExpression Pattern { get; } + + public YdbILikeExpression(SqlExpression match, SqlExpression pattern, RelationalTypeMapping? typeMapping) + : base(typeof(bool), typeMapping) + { + Match = match; + Pattern = pattern; + } + + public override Expression Quote() => new YdbILikeExpression((SqlExpression)Match.Quote(), (SqlExpression)Pattern.Quote(), TypeMapping); + + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update( + (SqlExpression)visitor.Visit(Match), + (SqlExpression)visitor.Visit(Pattern)); + + public YdbILikeExpression Update( + SqlExpression match, + SqlExpression pattern) + => match == Match && pattern == Pattern + ? this + : new YdbILikeExpression(match, pattern, TypeMapping); + + public override bool Equals(object? obj) + => obj is YdbILikeExpression other && Equals(other); + + public virtual bool Equals(YdbILikeExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && Equals(Match, other.Match) + && Equals(Pattern, other.Pattern); + + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Match, Pattern); + + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Match); + expressionPrinter.Append(ILikeWithSpacesConst); + expressionPrinter.Visit(Pattern); + } + + public override string ToString() => $"{Match} ILIKE {Pattern}"; +} diff --git a/src/EFCore.Ydb/src/Query/Internal/Translators/YdbILikeMethodTranslator.cs b/src/EFCore.Ydb/src/Query/Internal/Translators/YdbILikeMethodTranslator.cs new file mode 100644 index 00000000..1c563c4d --- /dev/null +++ b/src/EFCore.Ydb/src/Query/Internal/Translators/YdbILikeMethodTranslator.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Reflection; +using EntityFrameworkCore.Ydb.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace EntityFrameworkCore.Ydb.Query.Internal.Translators; + +public class YdbILikeMethodTranslator: IMethodCallTranslator +{ + private static readonly MethodInfo ILike = + typeof(YdbDbFunctionsExtension).GetRuntimeMethod( + nameof(YdbDbFunctionsExtension.ILike), + [typeof(DbFunctions), typeof(string), typeof(string)]); + + private readonly YdbSqlExpressionFactory _sqlExpressionFactory; + + public YdbILikeMethodTranslator(YdbSqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + public SqlExpression? Translate(SqlExpression? instance, MethodInfo method, IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method == ILike) + { + return _sqlExpressionFactory.ILike(arguments[1], arguments[2]); + } + + return null; + } +} diff --git a/src/EFCore.Ydb/src/Query/Internal/YdbMethodCallTranslatorProvider.cs b/src/EFCore.Ydb/src/Query/Internal/YdbMethodCallTranslatorProvider.cs index 3d0caeba..dc22955c 100644 --- a/src/EFCore.Ydb/src/Query/Internal/YdbMethodCallTranslatorProvider.cs +++ b/src/EFCore.Ydb/src/Query/Internal/YdbMethodCallTranslatorProvider.cs @@ -3,7 +3,7 @@ namespace EntityFrameworkCore.Ydb.Query.Internal; -public sealed class YdbMethodCallTranslatorProvider : RelationalMethodCallTranslatorProvider +public class YdbMethodCallTranslatorProvider : RelationalMethodCallTranslatorProvider { public YdbMethodCallTranslatorProvider(RelationalMethodCallTranslatorProviderDependencies dependencies) : base(dependencies) @@ -14,7 +14,8 @@ public YdbMethodCallTranslatorProvider(RelationalMethodCallTranslatorProviderDep [ new YdbDateTimeMethodTranslator(sqlExpressionFactory), new YdbMathTranslator(sqlExpressionFactory), - new YdbByteArrayMethodTranslator(sqlExpressionFactory) + new YdbByteArrayMethodTranslator(sqlExpressionFactory), + new YdbILikeMethodTranslator(sqlExpressionFactory) ] ); } diff --git a/src/EFCore.Ydb/src/Query/Internal/YdbParameterBasedSqlProcessor.cs b/src/EFCore.Ydb/src/Query/Internal/YdbParameterBasedSqlProcessor.cs index 9d581ab1..246e0f80 100644 --- a/src/EFCore.Ydb/src/Query/Internal/YdbParameterBasedSqlProcessor.cs +++ b/src/EFCore.Ydb/src/Query/Internal/YdbParameterBasedSqlProcessor.cs @@ -1,8 +1,18 @@ +using System.Collections.Generic; +using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Query; namespace EntityFrameworkCore.Ydb.Query.Internal; -public class YdbParameterBasedSqlProcessor( - RelationalParameterBasedSqlProcessorDependencies dependencies, - RelationalParameterBasedSqlProcessorParameters parameters -) : RelationalParameterBasedSqlProcessor(dependencies, parameters); +public class YdbParameterBasedSqlProcessor : RelationalParameterBasedSqlProcessor +{ + public YdbParameterBasedSqlProcessor(RelationalParameterBasedSqlProcessorDependencies dependencies, + RelationalParameterBasedSqlProcessorParameters parameters) : base(dependencies, parameters) + { + } + + protected override Expression ProcessSqlNullability(Expression queryExpression, + IReadOnlyDictionary parametersValues, out bool canCache) => + new YdbSqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersValues, + out canCache); +} diff --git a/src/EFCore.Ydb/src/Query/Internal/YdbQuerySqlGenerator.cs b/src/EFCore.Ydb/src/Query/Internal/YdbQuerySqlGenerator.cs index b04c92e1..660856f3 100644 --- a/src/EFCore.Ydb/src/Query/Internal/YdbQuerySqlGenerator.cs +++ b/src/EFCore.Ydb/src/Query/Internal/YdbQuerySqlGenerator.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using EntityFrameworkCore.Ydb.Query.Expressions; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; @@ -320,4 +321,39 @@ protected override Expression VisitCase(CaseExpression caseExpression) return caseExpression; } + + + protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression) + { + if (sqlUnaryExpression.OperatorType == ExpressionType.Not && sqlUnaryExpression.Type == typeof(bool) + && + sqlUnaryExpression.Operand is YdbILikeExpression iLikeExpression) + { + VisitILike(iLikeExpression, true); + return sqlUnaryExpression; + } + + return base.VisitSqlUnary(sqlUnaryExpression); + } + + protected override Expression VisitExtension(Expression extensionExpression) => + extensionExpression switch + { + YdbILikeExpression ilikeExpression => VisitILike(ilikeExpression, false), + _ => base.VisitExtension(extensionExpression) + }; + + + + private Expression VisitILike(YdbILikeExpression ilikeExpression, bool not) + { + Visit(ilikeExpression.Match); + if (not) + { + Sql.Append(" NOT"); + } + Sql.Append(" ILIKE "); + Visit(ilikeExpression.Pattern); + return ilikeExpression; + } } diff --git a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs index bdf3eb3c..ca5ee020 100644 --- a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs +++ b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs @@ -1,4 +1,6 @@ +using System; using System.Diagnostics.CodeAnalysis; +using EntityFrameworkCore.Ydb.Query.Expressions; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; @@ -7,7 +9,25 @@ namespace EntityFrameworkCore.Ydb.Query.Internal; public class YdbSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) : SqlExpressionFactory(dependencies) { - [return: NotNullIfNotNull("sqlExpression")] - public override SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, RelationalTypeMapping? typeMapping) => - base.ApplyTypeMapping(sqlExpression, typeMapping); + [return: NotNullIfNotNull("sqlExpression")] + public override SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, RelationalTypeMapping? typeMapping) => + sqlExpression switch + { + YdbILikeExpression ilikeExpression => ApplyILikeExpression(ilikeExpression, typeMapping), + _ => base.ApplyTypeMapping(sqlExpression, typeMapping) + }; + + + protected SqlExpression ApplyILikeExpression(YdbILikeExpression ilikeExpression, RelationalTypeMapping? typeMapping) + { + var inferredTypeMapping = ExpressionExtensions.InferTypeMapping(ilikeExpression.Match, ilikeExpression.Pattern); + + return new YdbILikeExpression(ApplyTypeMapping(ilikeExpression.Match, inferredTypeMapping), + ApplyTypeMapping(ilikeExpression.Pattern, inferredTypeMapping), typeMapping); + } + + public YdbILikeExpression ILike( + SqlExpression match, + SqlExpression pattern) + => (YdbILikeExpression)ApplyDefaultTypeMapping(new YdbILikeExpression(match, pattern, null)); } diff --git a/src/EFCore.Ydb/src/Query/Internal/YdbSqlNullabilityProcessor.cs b/src/EFCore.Ydb/src/Query/Internal/YdbSqlNullabilityProcessor.cs new file mode 100644 index 00000000..92aeb991 --- /dev/null +++ b/src/EFCore.Ydb/src/Query/Internal/YdbSqlNullabilityProcessor.cs @@ -0,0 +1,33 @@ +using EntityFrameworkCore.Ydb.Query.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace EntityFrameworkCore.Ydb.Query.Internal; + +public class YdbSqlNullabilityProcessor: SqlNullabilityProcessor +{ + private readonly ISqlExpressionFactory _sqlExpressionFactory; + public YdbSqlNullabilityProcessor(RelationalParameterBasedSqlProcessorDependencies dependencies, RelationalParameterBasedSqlProcessorParameters parameters) : base(dependencies, parameters) + { + _sqlExpressionFactory = dependencies.SqlExpressionFactory; + } + + protected override SqlExpression VisitCustomSqlExpression(SqlExpression sqlExpression, bool allowOptimizedExpansion, + out bool nullable) => sqlExpression switch + { + YdbILikeExpression ilikeExpression => VisitILikeExpression(ilikeExpression, allowOptimizedExpansion, + out nullable), + _ => base.VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable) + }; + + protected SqlExpression VisitILikeExpression(YdbILikeExpression ilikeExpression, bool allowOptimizedExpansion, out bool nullable) + { + var match = Visit(ilikeExpression.Match, out var matchNullable); + var pattern = Visit(ilikeExpression.Pattern, out var patternNullable); + + nullable = matchNullable || patternNullable; + SqlExpression result = ilikeExpression.Update(match, pattern); + + return result; + } +}