Skip to content

Wrap a table-valued function in a derived table after LATERAL. Fix #1277 - #1278

Open
azabluda wants to merge 2 commits into
FirebirdSQL:masterfrom
azabluda:fix/1277-lateral-table-valued-function
Open

Wrap a table-valued function in a derived table after LATERAL. Fix #1277#1278
azabluda wants to merge 2 commits into
FirebirdSQL:masterfrom
azabluda:fix/1277-lateral-table-valued-function

Conversation

@azabluda

@azabluda azabluda commented Sep 7, 2026

Copy link
Copy Markdown

Fixes #1277.

The defect

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 came out as a bare call and the statement did not parse.

JOIN LATERAL "GetCustomerOrderCountByYear"("c"."Id") AS "g" ON TRUE
-- Dynamic SQL Error, SQL error code = -104, 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.

Two commits, and the first is red on purpose

1. Un-skip the 14 correlated queryable-function tests, and create what they need.
Passed 82 -> 84, Failed 0 -> 12, Skipped 24 -> 10.

2. The fix. Failed 12 -> 0, Passed 84 -> 96.

Split this way so the fix is measured against a failing suite rather than asserted, and so each half can be reviewed on its own.

Two things you may not expect in here

The fixture was missing three objects. AddValues, GetCustomerOrderCountByYear and GetCustomerOrderCountByYearOnlyFrom2000 were never created, and nothing noticed, because every test that uses them was skipped. With the SQL fixed, those ten tests stop failing on Token unknown and start failing on Procedure unknown. They are ported from the definitions in EF Core's own UdfDbFunctionSqlServerTests fixture, in Firebird form: the two table-valued ones become selectable procedures, as the two already in this fixture are, and year(...) becomes extract(year from ...).

Two of the fourteen skips were already stale. QF_Select_Correlated_Subquery_In_Anonymous and QF_Correlated_Func_Call_With_Navigation pass with no product change at all.

Firebird versions

A correlated queryable function reaches the store as a LATERAL derived table, and LATERAL is Firebird 4 and later, so both the 14 tests and the three fixture objects are gated on ServerLessThan4() — the same early return NavigationsCollectionFbTest and ComplexTypeQueryFbTest already use.

The fixture guard is required rather than tidy. GetCustomerOrderCountByYearOnlyFrom2000 is 39 characters and Firebird 3 caps identifiers at 31, so creating it there fails the seed with Name longer than database column size and takes every test in the class down with it, including ones that pass today.

What was run

UdfDbFunctionFbTests, run locally:

Firebird
3.0 Passed 96, Failed 0, Skipped 10
5.0.3 Passed 96, Failed 0, Skipped 10

Full suites on 5.0.3: FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests Passed 14167, Failed 0, Skipped 1113, Total 15280; FirebirdSql.EntityFrameworkCore.Firebird.Tests Passed 102, Failed 0.

FB30, FB40 and FB50 are all green against Tests-EFCore and Tests-EFCore-Functional, run in my fork: https://github.com/azabluda/NETProvider/actions/runs/34167549531

Both wrapped and implicit forms were also run directly against the store by hand before the issue was filed.

AI usage

Per CONTRIBUTING: this change was written with Claude Code. I reviewed it, ran it and stand behind it. Both commits carry a Co-Authored-By trailer.

@azabluda
azabluda force-pushed the fix/1277-lateral-table-valued-function branch from 90f0088 to 2459a34 Compare September 7, 2026 22:02
azabluda and others added 2 commits September 8, 2026 00:41
…hey need

Committed red on purpose, so the fix that follows is measured against a failing
suite rather than asserted.

All 14 carry [NotSupportedOnFirebirdFact]. The store supports every one of them;
only the generated SQL is malformed, which is FirebirdSQL#1277.

A correlated queryable function reaches the store as a LATERAL derived table, and
LATERAL is Firebird 4 and later, so both the tests and the objects they need are
gated on ServerLessThan4(). The gate is the same early return the Associations and
ComplexType tests here already use, behind one helper rather than repeated
fourteen times.

The fixture guard is required rather than tidy. "GetCustomerOrderCountByYearOnlyFrom2000"
is 39 characters and Firebird 3 caps identifiers at 31, so creating it there fails
the seed with "Name longer than database column size" and takes every test in the
class down with it, including ones that pass today.

That second gap was hidden behind the first. The fixture never created "AddValues",
"GetCustomerOrderCountByYear" or "GetCustomerOrderCountByYearOnlyFrom2000", and
nothing noticed, because every test that uses them was skipped. They are ported
from the definitions in EF Core's own UdfDbFunctionSqlServerTests fixture, in
Firebird form: the two table-valued ones become selectable procedures, as the two
already in this fixture are, and year(...) becomes extract(year from ...).

UdfDbFunctionFbTests on Firebird 5.0.3:
Passed 82 -> 84, Failed 0 -> 12, Skipped 24 -> 10, Total 106.

Twelve fail at the LATERAL source with the store's own parse error:

  FbException: Dynamic SQL Error
  Token unknown - line 3, column 14

Two pass with no product change at all, so their skips were already stale:

  QF_Select_Correlated_Subquery_In_Anonymous
  QF_Correlated_Func_Call_With_Navigation

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rebirdSQL#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EF Core: a table-valued function is not wrapped in a derived table after LATERAL, so every correlated queryable function fails

1 participant