diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/NorthwindAggregateOperatorsQueryFbTest.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/NorthwindAggregateOperatorsQueryFbTest.cs index 8cd9f61a0..e1e860de0 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/NorthwindAggregateOperatorsQueryFbTest.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/NorthwindAggregateOperatorsQueryFbTest.cs @@ -16,8 +16,10 @@ //$Authors = Jiri Cincura (jiri@cincura.net) using System; +using System.Linq; using System.Threading.Tasks; using FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -26,9 +28,13 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.Query; public class NorthwindAggregateOperatorsQueryFbTest : NorthwindAggregateOperatorsQueryRelationalTestBase> { + private readonly NorthwindQueryFbFixture _fixture; + public NorthwindAggregateOperatorsQueryFbTest(NorthwindQueryFbFixture fixture) : base(fixture) - { } + { + _fixture = fixture; + } [NotSupportedOnFirebirdTheory] [MemberData(nameof(IsAsyncData))] @@ -96,4 +102,29 @@ public override Task Average_over_nested_subquery(bool async) { return base.Average_over_nested_subquery(async); } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public override async Task Contains_with_local_collection_sql_injection(bool async) + { + using var context = CreateContext(); + + var ids = new[] { "ALFKI", "ANATR" }; + + var query = context.Customers + .Where(c => ids.Contains(c.CustomerID)); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var sql = query.ToQueryString(); + + Assert.Contains("IN", sql); + Assert.Contains("@ids1", sql); + Assert.Contains("@ids2", sql); + + Assert.DoesNotContain("ALFKI, ANATR", sql); + + } } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/NorthwindMiscellaneousQueryFbTest.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/NorthwindMiscellaneousQueryFbTest.cs index 978f24f83..9b76de228 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/NorthwindMiscellaneousQueryFbTest.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/NorthwindMiscellaneousQueryFbTest.cs @@ -16,9 +16,11 @@ //$Authors = Jiri Cincura (jiri@cincura.net) using System; +using System.Linq; using System.Threading.Tasks; using FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.Helpers; using FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.TestUtilities; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -27,9 +29,13 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.Query; public class NorthwindMiscellaneousQueryFbTest : NorthwindMiscellaneousQueryRelationalTestBase> { + private readonly NorthwindQueryFbFixture _fixture; + public NorthwindMiscellaneousQueryFbTest(NorthwindQueryFbFixture fixture) : base(fixture) - { } + { + _fixture = fixture; + } [Theory] [MemberData(nameof(IsAsyncData))] @@ -159,4 +165,24 @@ public override Task Where_nanosecond_and_microsecond_component(bool async) { return base.Where_nanosecond_and_microsecond_component(async); } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public override async Task Contains_over_concatenated_columns_both_fixed_length(bool async) + { + using var context = CreateContext(); + + var query = context.Customers + .Where(c => (c.ContactName + c.ContactTitle).Contains("Owner")); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var sql = query.ToQueryString(); + + Assert.Contains("POSITION", sql); + Assert.Contains("||", sql); + Assert.Contains("COALESCE", sql); + } } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/SqlQueryFbTest.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/SqlQueryFbTest.cs index dd0a61306..fea06ccb7 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/SqlQueryFbTest.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/SqlQueryFbTest.cs @@ -15,10 +15,13 @@ //$Authors = Jiri Cincura (jiri@cincura.net) +using System; using System.Data.Common; +using System.Linq; using System.Threading.Tasks; using FirebirdSql.Data.FirebirdClient; using FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -151,6 +154,37 @@ public override Task SqlQueryRaw_queryable_simple_different_cased_columns_and_no return base.SqlQueryRaw_queryable_simple_different_cased_columns_and_not_enough_columns_throws(async); } + [Fact] + public async Task Where_datetime_between_translated_correctly() + { + using var context = CreateContext(); + + var inicio = new DateTime(2026, 1, 1); + var fim = new DateTime(2026, 1, 31); + + var query = context.Orders.Where(o => o.OrderDate >= inicio && o.OrderDate <= fim); + + var sql = query.ToQueryString(); + + Assert.Contains("BETWEEN", sql); + Assert.Contains("AND", sql); + } + + [Fact] + public async Task Where_OrderDate_and_Id_not_translated_to_between() + { + using var context = CreateContext(); + + var inicio = new DateTime(1997, 1, 1); + + var query = context.Orders.Where(o => o.OrderDate >= inicio && o.OrderID <= 10); + + var sql = query.ToQueryString(); + + Assert.DoesNotContain("BETWEEN", sql); + } + + protected override DbParameter CreateDbParameter(string name, object value) => new FbParameter { ParameterName = name, Value = value }; } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs index 2e899cea6..663186daa 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs @@ -143,17 +143,25 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpres Sql.Append(")"); return sqlBinaryExpression; } - else + else if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso) { - return base.VisitSqlBinary(sqlBinaryExpression); - } + if (sqlBinaryExpression.Left is SqlBinaryExpression left && sqlBinaryExpression.Right is SqlBinaryExpression right) + { + if (left.OperatorType == ExpressionType.GreaterThanOrEqual && right.OperatorType == ExpressionType.LessThanOrEqual && + left.Left is ColumnExpression leftColumn && right.Left is ColumnExpression rightColumn && + leftColumn.Name == rightColumn.Name) + { + Visit(left.Left); + Sql.Append(" BETWEEN "); + Visit(left.Right); + Sql.Append(" AND "); + Visit(right.Right); + return sqlBinaryExpression; + } - void BooleanToIntegralAndVisit(SqlExpression expression) - { - Sql.Append("IIF("); - Visit(expression); - Sql.Append(", 1, 0)"); + } } + return base.VisitSqlBinary(sqlBinaryExpression); } protected override Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression) @@ -170,7 +178,9 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame if (sqlParameterExpression.Type == typeof(string)) { var isUnicode = FbTypeMappingSource.IsUnicode(sqlParameterExpression.TypeMapping); - Sql.Append(((IFbSqlGenerationHelper)Dependencies.SqlGenerationHelper).StringParameterQueryType(isUnicode)); + var storeTypeNameBase = sqlParameterExpression.TypeMapping.StoreTypeNameBase; + var size = sqlParameterExpression.TypeMapping.Size ?? 0; + Sql.Append(((IFbSqlGenerationHelper)Dependencies.SqlGenerationHelper).StringParameterQueryType(isUnicode, storeTypeNameBase, size)); } else { @@ -192,8 +202,11 @@ protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstant if (shouldExplicitStringLiteralTypes) { var isUnicode = FbTypeMappingSource.IsUnicode(sqlConstantExpression.TypeMapping); + var storeTypeNameBase = sqlConstantExpression.TypeMapping.StoreTypeNameBase; + var size = sqlConstantExpression.TypeMapping.Size ?? 0; + Sql.Append(" AS "); - Sql.Append(((IFbSqlGenerationHelper)Dependencies.SqlGenerationHelper).StringLiteralQueryType(sqlConstantExpression.Value as string, isUnicode)); + Sql.Append(((IFbSqlGenerationHelper)Dependencies.SqlGenerationHelper).StringLiteralQueryType(sqlConstantExpression.Value as string, isUnicode, storeTypeNameBase, size)); Sql.Append(")"); } return sqlConstantExpression; @@ -413,4 +426,11 @@ void GenerateList(IReadOnlyList items, Action generationAction, Action< generationAction(items[i]); } } + + void BooleanToIntegralAndVisit(SqlExpression expression) + { + Sql.Append("IIF("); + Visit(expression); + Sql.Append(", 1, 0)"); + } } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbSqlGenerationHelper.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbSqlGenerationHelper.cs index 78bbff57d..ead4bb4d3 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbSqlGenerationHelper.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbSqlGenerationHelper.cs @@ -27,17 +27,38 @@ public FbSqlGenerationHelper(RelationalSqlGenerationHelperDependencies dependenc : base(dependencies) { } - public virtual string StringLiteralQueryType(string s, bool isUnicode = true) + public virtual string StringLiteralQueryType(string s, bool isUnicode = true, string storeTypeNameBase = "", int size = 0) { - var length = MinimumStringQueryTypeLength(s); + var maxSize = MinimumStringQueryTypeLength(s); + string typeName; + if (storeTypeNameBase.Equals("BLOB SUB_TYPE TEXT", StringComparison.OrdinalIgnoreCase)) + { + typeName = "VARCHAR"; + } + else + { + typeName = IsEmpty(storeTypeNameBase) ? "VARCHAR" : storeTypeNameBase; + } + var charset = isUnicode ? " CHARACTER SET UTF8" : string.Empty; - return $"VARCHAR({length}){charset}"; + return $"{typeName}({maxSize}){charset}"; } - public virtual string StringParameterQueryType(bool isUnicode) + public virtual string StringParameterQueryType(bool isUnicode, string storeTypeNameBase = "", int size = 0) { - var size = isUnicode ? FbTypeMappingSource.UnicodeVarcharMaxSize : FbTypeMappingSource.VarcharMaxSize; - return $"VARCHAR({size})"; + int maxSize; + string typeName; + if (storeTypeNameBase.Equals("BLOB SUB_TYPE TEXT", StringComparison.OrdinalIgnoreCase)) + { + maxSize = (isUnicode ? FbTypeMappingSource.UnicodeVarcharMaxSize : FbTypeMappingSource.VarcharMaxSize); + typeName = "VARCHAR"; + } + else + { + maxSize = size > 0 ? size : (isUnicode ? FbTypeMappingSource.UnicodeVarcharMaxSize : FbTypeMappingSource.VarcharMaxSize); + typeName = IsEmpty(storeTypeNameBase) ? "VARCHAR" : storeTypeNameBase; + } + return $"{typeName}({maxSize})"; } public virtual void GenerateBlockParameterName(StringBuilder builder, string name) @@ -47,7 +68,7 @@ public virtual void GenerateBlockParameterName(StringBuilder builder, string nam public virtual string AlternativeStatementTerminator => "~"; - static int MinimumStringQueryTypeLength(string s) + private int MinimumStringQueryTypeLength(string s) { var length = s?.Length ?? 0; if (length == 0) @@ -55,9 +76,8 @@ static int MinimumStringQueryTypeLength(string s) return length; } - static void EnsureStringLiteralQueryTypeLength(int length) + private bool IsEmpty(string storeTypeNameBase) { - if (length > FbTypeMappingSource.UnicodeVarcharMaxSize) - throw new ArgumentOutOfRangeException(nameof(length)); + return (storeTypeNameBase == null || string.IsNullOrEmpty(storeTypeNameBase) || string.IsNullOrWhiteSpace(storeTypeNameBase)); } } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/IFbSqlGenerationHelper.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/IFbSqlGenerationHelper.cs index 56c4c1f57..bb6af11c5 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/IFbSqlGenerationHelper.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/IFbSqlGenerationHelper.cs @@ -22,8 +22,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal; public interface IFbSqlGenerationHelper : ISqlGenerationHelper { - string StringLiteralQueryType(string s, bool isUnicode); - string StringParameterQueryType(bool isUnicode); + string StringLiteralQueryType(string s, bool isUnicode, string storeTypeNameBase = "", int size = 0); + string StringParameterQueryType(bool isUnicode, string storeTypeNameBase = "", int size = 0); void GenerateBlockParameterName(StringBuilder builder, string name); string AlternativeStatementTerminator { get; } }