diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/UdfDbFunctionFbTests.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/UdfDbFunctionFbTests.cs index c524c8b30..c0afc2179 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/UdfDbFunctionFbTests.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/UdfDbFunctionFbTests.cs @@ -33,23 +33,17 @@ public UdfDbFunctionFbTests(Fb fixture) : base(fixture) { } - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_CrossApply_Correlated_Select_Anonymous() - { - base.QF_CrossApply_Correlated_Select_Anonymous(); - } + => RunIfLateralIsSupported(base.QF_CrossApply_Correlated_Select_Anonymous); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_OuterApply_Correlated_Select_QF() - { - base.QF_OuterApply_Correlated_Select_QF(); - } + => RunIfLateralIsSupported(base.QF_OuterApply_Correlated_Select_QF); - [NotSupportedOnFirebirdFact] + [Fact] public override void Udf_with_argument_being_comparison_of_nullable_columns() - { - base.Udf_with_argument_being_comparison_of_nullable_columns(); - } + => RunIfLateralIsSupported(base.Udf_with_argument_being_comparison_of_nullable_columns); [Fact] public override void QF_Select_Correlated_Subquery_In_Anonymous_MultipleCollections() @@ -57,71 +51,49 @@ public override void QF_Select_Correlated_Subquery_In_Anonymous_MultipleCollecti base.QF_Select_Correlated_Subquery_In_Anonymous_MultipleCollections(); } - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_CrossApply_Correlated_Select_Result() - { - base.QF_CrossApply_Correlated_Select_Result(); - } + => RunIfLateralIsSupported(base.QF_CrossApply_Correlated_Select_Result); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_Select_Correlated_Subquery_In_Anonymous() - { - base.QF_Select_Correlated_Subquery_In_Anonymous(); - } + => RunIfLateralIsSupported(base.QF_Select_Correlated_Subquery_In_Anonymous); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_Correlated_Func_Call_With_Navigation() - { - base.QF_Correlated_Func_Call_With_Navigation(); - } + => RunIfLateralIsSupported(base.QF_Correlated_Func_Call_With_Navigation); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_Select_Correlated_Direct_With_Function_Query_Parameter_Correlated_In_Anonymous() - { - base.QF_Select_Correlated_Direct_With_Function_Query_Parameter_Correlated_In_Anonymous(); - } + => RunIfLateralIsSupported(base.QF_Select_Correlated_Direct_With_Function_Query_Parameter_Correlated_In_Anonymous); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_OuterApply_Correlated_Select_Entity() - { - base.QF_OuterApply_Correlated_Select_Entity(); - } + => RunIfLateralIsSupported(base.QF_OuterApply_Correlated_Select_Entity); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_Correlated_Nested_Func_Call() - { - base.QF_Correlated_Nested_Func_Call(); - } + => RunIfLateralIsSupported(base.QF_Correlated_Nested_Func_Call); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_OuterApply_Correlated_Select_Anonymous() - { - base.QF_OuterApply_Correlated_Select_Anonymous(); - } + => RunIfLateralIsSupported(base.QF_OuterApply_Correlated_Select_Anonymous); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_Select_Correlated_Subquery_In_Anonymous_Nested_With_QF() - { - base.QF_Select_Correlated_Subquery_In_Anonymous_Nested_With_QF(); - } + => RunIfLateralIsSupported(base.QF_Select_Correlated_Subquery_In_Anonymous_Nested_With_QF); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_Correlated_Select_In_Anonymous() - { - base.QF_Correlated_Select_In_Anonymous(); - } + => RunIfLateralIsSupported(base.QF_Correlated_Select_In_Anonymous); - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_CrossApply_Correlated_Select_QF_Type() - { - base.QF_CrossApply_Correlated_Select_QF_Type(); - } + => RunIfLateralIsSupported(base.QF_CrossApply_Correlated_Select_QF_Type); - [NotSupportedOnFirebirdFact] + [Fact] public override void Udf_with_argument_being_comparison_to_null_parameter() - { - base.Udf_with_argument_being_comparison_to_null_parameter(); - } + => RunIfLateralIsSupported(base.Udf_with_argument_being_comparison_to_null_parameter); [DoesNotHaveTheDataFact] public override void QF_CrossJoin_Not_Correlated() @@ -217,6 +189,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } + // A correlated queryable function needs LATERAL, which is Firebird 4 and later. + void RunIfLateralIsSupported(Action test) + { + var fbTestStore = (FbTestStore)Fixture.TestStore; + if (fbTestStore.ServerLessThan4()) + return; + test(); + } + public class Fb : UdfFixtureBase { protected override string StoreName { get; } = nameof(UdfDbFunctionFbTests); @@ -349,6 +330,62 @@ having count(""ProductId"") > 1 end end"); + // Used only by the FB4+ tests, and the last name is 39 characters, which FB3 rejects. + if (!((FbTestStore)TestStore).ServerLessThan4()) + { + await context.Database.ExecuteSqlRawAsync( + @"create function ""AddValues"" (a int, b int) + returns int + as + begin + return :a + :b; + end"); + + await context.Database.ExecuteSqlRawAsync( + @"create procedure ""GetCustomerOrderCountByYear"" (customerId int) + returns + ( + ""CustomerId"" int not null, + ""Count"" int not null, + ""Year"" int not null + ) + as + begin + for select :customerId, count(""Id""), extract(year from ""OrderDate"") + from ""Orders"" + where ""CustomerId"" = :customerId + group by ""CustomerId"", extract(year from ""OrderDate"") + order by extract(year from ""OrderDate"") + into :""CustomerId"", :""Count"", :""Year"" do + begin + suspend; + end + end"); + + await context.Database.ExecuteSqlRawAsync( + @"create procedure ""GetCustomerOrderCountByYearOnlyFrom2000"" (customerId int, onlyFrom2000 boolean) + returns + ( + ""CustomerId"" int not null, + ""Count"" int not null, + ""Year"" int not null + ) + as + begin + for select :customerId, count(""Id""), extract(year from ""OrderDate"") + from ""Orders"" + where ""CustomerId"" = 1 + and (:onlyFrom2000 = false or :onlyFrom2000 is null + or (:onlyFrom2000 = true and extract(year from ""OrderDate"") = 2000)) + group by ""CustomerId"", extract(year from ""OrderDate"") + order by extract(year from ""OrderDate"") + into :""CustomerId"", :""Count"", :""Year"" do + begin + suspend; + end + end"); + } + await context.SaveChangesAsync(); } } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs index 2e899cea6..8ca68fcf5 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs @@ -298,6 +298,12 @@ protected override Expression VisitCrossApply(CrossApplyExpression crossApplyExp .Append(AliasSeparator) .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table.Alias)); } + else if (crossApplyExpression.Table is TableValuedFunctionExpression function) + { + // Same for a table-valued function. The alias goes on the derived table rather than + // on the call, so the rest of the statement keeps referring to it unchanged. + GenerateLateralFunction(function); + } else { Visit(crossApplyExpression.Table); @@ -325,6 +331,12 @@ protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExp .Append(AliasSeparator) .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table.Alias)); } + else if (outerApplyExpression.Table is TableValuedFunctionExpression function) + { + // Same for a table-valued function. The alias goes on the derived table rather than + // on the call, so the rest of the statement keeps referring to it unchanged. + GenerateLateralFunction(function); + } else { Visit(outerApplyExpression.Table); @@ -334,6 +346,23 @@ protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExp return outerApplyExpression; } + // Firebird will not take a bare table-valued function after LATERAL either, so it is + // wrapped the same way a table is: (SELECT * FROM "Func"(args)) AS "alias". + void GenerateLateralFunction(TableValuedFunctionExpression function) + { + Sql + .Append("(SELECT * FROM ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(function.Name, function.Schema)) + .Append("("); + + GenerateList(function.Arguments, e => Visit(e)); + + Sql + .Append("))") + .Append(AliasSeparator) + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(function.Alias)); + } + protected override void GeneratePseudoFromClause() { Sql.Append(" FROM RDB$DATABASE");