diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/UdfDbFunctionFbTests.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/UdfDbFunctionFbTests.cs index c524c8b30..bc4c0cc2b 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/UdfDbFunctionFbTests.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/Query/UdfDbFunctionFbTests.cs @@ -33,96 +33,32 @@ public UdfDbFunctionFbTests(Fb fixture) : base(fixture) { } - [NotSupportedOnFirebirdFact] - public override void QF_CrossApply_Correlated_Select_Anonymous() - { - base.QF_CrossApply_Correlated_Select_Anonymous(); - } - - [NotSupportedOnFirebirdFact] - public override void QF_OuterApply_Correlated_Select_QF() - { - base.QF_OuterApply_Correlated_Select_QF(); - } - - [NotSupportedOnFirebirdFact] - public override void Udf_with_argument_being_comparison_of_nullable_columns() - { - base.Udf_with_argument_being_comparison_of_nullable_columns(); - } - [Fact] public override void QF_Select_Correlated_Subquery_In_Anonymous_MultipleCollections() { base.QF_Select_Correlated_Subquery_In_Anonymous_MultipleCollections(); } - [NotSupportedOnFirebirdFact] - public override void QF_CrossApply_Correlated_Select_Result() - { - base.QF_CrossApply_Correlated_Select_Result(); - } - - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_Select_Correlated_Subquery_In_Anonymous() { + // that needs LATERAL which is Firebird 4+ + var fbTestStore = (FbTestStore)Fixture.TestStore; + if (fbTestStore.ServerLessThan4()) + return; base.QF_Select_Correlated_Subquery_In_Anonymous(); } - [NotSupportedOnFirebirdFact] + [Fact] public override void QF_Correlated_Func_Call_With_Navigation() { + // that needs LATERAL which is Firebird 4+ + var fbTestStore = (FbTestStore)Fixture.TestStore; + if (fbTestStore.ServerLessThan4()) + return; base.QF_Correlated_Func_Call_With_Navigation(); } - [NotSupportedOnFirebirdFact] - 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(); - } - - [NotSupportedOnFirebirdFact] - public override void QF_OuterApply_Correlated_Select_Entity() - { - base.QF_OuterApply_Correlated_Select_Entity(); - } - - [NotSupportedOnFirebirdFact] - public override void QF_Correlated_Nested_Func_Call() - { - base.QF_Correlated_Nested_Func_Call(); - } - - [NotSupportedOnFirebirdFact] - public override void QF_OuterApply_Correlated_Select_Anonymous() - { - base.QF_OuterApply_Correlated_Select_Anonymous(); - } - - [NotSupportedOnFirebirdFact] - public override void QF_Select_Correlated_Subquery_In_Anonymous_Nested_With_QF() - { - base.QF_Select_Correlated_Subquery_In_Anonymous_Nested_With_QF(); - } - - [NotSupportedOnFirebirdFact] - public override void QF_Correlated_Select_In_Anonymous() - { - base.QF_Correlated_Select_In_Anonymous(); - } - - [NotSupportedOnFirebirdFact] - public override void QF_CrossApply_Correlated_Select_QF_Type() - { - base.QF_CrossApply_Correlated_Select_QF_Type(); - } - - [NotSupportedOnFirebirdFact] - public override void Udf_with_argument_being_comparison_to_null_parameter() - { - base.Udf_with_argument_being_comparison_to_null_parameter(); - } - [DoesNotHaveTheDataFact] public override void QF_CrossJoin_Not_Correlated() { @@ -208,6 +144,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasName("GetCustWithMostOrdersAfterDate"); modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetCustomerWithMostOrdersAfterDateInstance))) .HasName("GetCustWithMostOrdersAfterDate"); + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetCustomerOrderCountByYearOnlyFrom2000))) + .HasName("GetCustOrderCountByYearFrom2000"); modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(IdentityString))) .HasSchema(null); @@ -349,6 +287,58 @@ having count(""ProductId"") > 1 end end"); + 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 ""GetCustOrderCountByYearFrom2000"" (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..bb2f29f17 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs @@ -285,24 +285,8 @@ protected override void GenerateOrderings(SelectExpression selectExpression) // Copyright (c) 2002-2021, Npgsql protected override Expression VisitCrossApply(CrossApplyExpression crossApplyExpression) { - Sql.Append("JOIN LATERAL "); - - if (crossApplyExpression.Table is TableExpression table) - { - // Firebird doesn't support LATERAL JOIN over table, and it doesn't really make sense to do it - but EF Core - // will sometimes generate that. - Sql - .Append("(SELECT * FROM ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table.Name, table.Schema)) - .Append(")") - .Append(AliasSeparator) - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table.Alias)); - } - else - { - Visit(crossApplyExpression.Table); - } - + Sql.Append("JOIN "); + GenerateApplySource(crossApplyExpression.Table); Sql.Append(" ON TRUE"); return crossApplyExpression; } @@ -312,14 +296,28 @@ protected override Expression VisitCrossApply(CrossApplyExpression crossApplyExp // Copyright (c) 2002-2021, Npgsql protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExpression) { - Sql.Append("LEFT JOIN LATERAL "); + Sql.Append("LEFT JOIN "); + GenerateApplySource(outerApplyExpression.Table); + Sql.Append(" ON TRUE"); + return outerApplyExpression; + } - if (outerApplyExpression.Table is TableExpression table) + // LATERAL is only allowed in front of a derived table, so anything that is not one has to + // become one - except a selectable procedure, which is implicitly lateral already. + void GenerateApplySource(TableExpressionBase source) + { + if (source is TableValuedFunctionExpression function) + { + // A selectable procedure takes its arguments from streams earlier in the FROM clause + // without LATERAL, so the call is emitted as it is anywhere else. + Visit(function); + } + else if (source is TableExpression table) { // Firebird doesn't support LATERAL JOIN over table, and it doesn't really make sense to do it - but EF Core // will sometimes generate that. Sql - .Append("(SELECT * FROM ") + .Append("LATERAL (SELECT * FROM ") .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table.Name, table.Schema)) .Append(")") .Append(AliasSeparator) @@ -327,11 +325,9 @@ protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExp } else { - Visit(outerApplyExpression.Table); + Sql.Append("LATERAL "); + Visit(source); } - - Sql.Append(" ON TRUE"); - return outerApplyExpression; } protected override void GeneratePseudoFromClause()