Skip to content

Commit 23c3cf5

Browse files
azabludaclaude
andcommitted
Wrap a table-valued function in a derived table after LATERAL. Fix #1277
VisitCrossApply and VisitOuterApply already know that Firebird will not take a bare source after LATERAL: both special-case a TableExpression and emit (SELECT * FROM "T") AS "t". The same branch was missing for a TableValuedFunctionExpression, so a correlated queryable function was emitted as a bare call and the statement did not parse: JOIN LATERAL "GetCustomerOrderCountByYear"("c"."Id") AS "g" ON TRUE -> Dynamic SQL Error, Token unknown It now emits the wrapped form, with the alias on the derived table so the rest of the statement keeps referring to it unchanged: JOIN LATERAL (SELECT * FROM "GetCustomerOrderCountByYear"("c"."Id")) AS "g" ON TRUE Argument rendering goes through the existing GenerateList helper rather than a new loop. UdfDbFunctionFbTests: Failed 12 -> 0, Passed 84 -> 96, Skipped 10, Total 106. No other test moved. The whole functional suite is Passed 14167, Failed 0, Skipped 1113, Total 15280, and FirebirdSql.EntityFrameworkCore.Firebird.Tests is Passed 102, Failed 0. Verified against Firebird 5.0.3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 869554d commit 23c3cf5

1 file changed

Lines changed: 29 additions & 0 deletions

File tree

src/FirebirdSql.EntityFrameworkCore.Firebird/Query/Internal/FbQuerySqlGenerator.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,12 @@ protected override Expression VisitCrossApply(CrossApplyExpression crossApplyExp
298298
.Append(AliasSeparator)
299299
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table.Alias));
300300
}
301+
else if (crossApplyExpression.Table is TableValuedFunctionExpression function)
302+
{
303+
// Same for a table-valued function. The alias goes on the derived table rather than
304+
// on the call, so the rest of the statement keeps referring to it unchanged.
305+
GenerateLateralFunction(function);
306+
}
301307
else
302308
{
303309
Visit(crossApplyExpression.Table);
@@ -325,6 +331,12 @@ protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExp
325331
.Append(AliasSeparator)
326332
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table.Alias));
327333
}
334+
else if (outerApplyExpression.Table is TableValuedFunctionExpression function)
335+
{
336+
// Same for a table-valued function. The alias goes on the derived table rather than
337+
// on the call, so the rest of the statement keeps referring to it unchanged.
338+
GenerateLateralFunction(function);
339+
}
328340
else
329341
{
330342
Visit(outerApplyExpression.Table);
@@ -334,6 +346,23 @@ protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExp
334346
return outerApplyExpression;
335347
}
336348

349+
// Firebird will not take a bare table-valued function after LATERAL either, so it is
350+
// wrapped the same way a table is: (SELECT * FROM "Func"(args)) AS "alias".
351+
void GenerateLateralFunction(TableValuedFunctionExpression function)
352+
{
353+
Sql
354+
.Append("(SELECT * FROM ")
355+
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(function.Name, function.Schema))
356+
.Append("(");
357+
358+
GenerateList(function.Arguments, e => Visit(e));
359+
360+
Sql
361+
.Append("))")
362+
.Append(AliasSeparator)
363+
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(function.Alias));
364+
}
365+
337366
protected override void GeneratePseudoFromClause()
338367
{
339368
Sql.Append(" FROM RDB$DATABASE");

0 commit comments

Comments
 (0)