This is an automated email from the ASF dual-hosted git repository. fhueske pushed a commit to branch fhueske-FLINK-39780-Make-TABLE-keyword-optional-in-LATERAL-context in repository https://gitbox.apache.org/repos/asf/flink.git
commit a29a18d959a10887eb5594e7d148ff076109d821 Author: Fabian Hueske <[email protected]> AuthorDate: Thu Aug 27 10:50:10 2026 +0200 [FLINK-39780][table] Make TABLE keyword optional in LATERAL context Align the implicit-LATERAL grammar with Calcite CALCITE-7183 (apache/calcite#5214): use the syntactic LOOKAHEAD(<LATERAL> CompoundTableIdentifier() <LPAREN>) instead of LOOKAHEAD(2), and adjust the overridden testLateral/testTemporalTable error expectations accordingly. Also trim the verbose test javadocs/comments per review feedback, and cover the documented 'JOIN LATERAL SNAPSHOT(input => TABLE ...) AS r ON ...' form with a test. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --- .../src/main/codegen/templates/Parser.jj | 6 +-- .../flink/sql/parser/FlinkSqlParserImplTest.java | 54 ++++++++++------------ 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/flink-table/flink-sql-parser/src/main/codegen/templates/Parser.jj b/flink-table/flink-sql-parser/src/main/codegen/templates/Parser.jj index a99a39179a8..2a23b4982b7 100644 --- a/flink-table/flink-sql-parser/src/main/codegen/templates/Parser.jj +++ b/flink-table/flink-sql-parser/src/main/codegen/templates/Parser.jj @@ -2285,11 +2285,7 @@ SqlNode TableRef3(ExprContext exprContext, boolean lateral) : tableRef = unnestOp.createCall(s.end(this), (List<SqlNode>) args); } | - // LATERAL with implicit table function call syntax, - // e.g. "FROM t, LATERAL fn(...)" instead of "FROM t, LATERAL TABLE(fn(...))". - // The non-LATERAL implicit form is handled by the CompoundTableIdentifier - // branch above. - LOOKAHEAD(2) + LOOKAHEAD(<LATERAL> CompoundTableIdentifier() <LPAREN>) <LATERAL> { lateral = true; } tableName = CompoundTableIdentifier() tableRef = ImplicitTableFunctionCallArgs(tableName) diff --git a/flink-table/flink-sql-parser/src/test/java/org/apache/flink/sql/parser/FlinkSqlParserImplTest.java b/flink-table/flink-sql-parser/src/test/java/org/apache/flink/sql/parser/FlinkSqlParserImplTest.java index 7786f425119..01f8c61541d 100644 --- a/flink-table/flink-sql-parser/src/test/java/org/apache/flink/sql/parser/FlinkSqlParserImplTest.java +++ b/flink-table/flink-sql-parser/src/test/java/org/apache/flink/sql/parser/FlinkSqlParserImplTest.java @@ -4050,20 +4050,15 @@ class FlinkSqlParserImplTest extends SqlParserTest { } /** - * Overrides the parent {@link org.apache.calcite.sql.parser.SqlParserTest#testLateral()} - * because Flink makes the {@code TABLE} keyword optional inside {@code LATERAL}. With this - * change, {@code LATERAL <identifier>} is the start of an implicit table-function call; the - * parser expects an argument list next, so the error position shifts. + * Overrides {@link org.apache.calcite.sql.parser.SqlParserTest#testLateral()}: making {@code + * TABLE} optional in {@code LATERAL} shifts the error position of the first (invalid) case. */ @Test void testLateral() { - // This is the only test case that differs from Calcite's SqlParserTest.testLateral(). - // LATERAL <identifier> is now interpreted as an implicit table function - // call; the error moves to where the argument list (LPAREN) is missing. - sql("select * from lateral em^p^").fails("(?s)Encountered \"<EOF>\" at .*"); + // Differs from Calcite: LATERAL <identifier> without an argument list fails at the + // identifier. + sql("select * from lateral ^emp^").fails("(?s)Encountered \"emp <EOF>\" at .*"); - // All other test cases are identical to Calcite's SqlParserTest.testLateral(). - // LATERAL TABLE <identifier> still fails at the identifier (no LPAREN). sql("select * from lateral table ^emp^ as e").fails("(?s)Encountered \"emp\" at .*"); sql("select * from lateral table ^scott^.emp").fails("(?s)Encountered \"scott\" at .*"); @@ -4084,14 +4079,12 @@ class FlinkSqlParserImplTest extends SqlParserTest { } /** - * Overrides the parent {@link org.apache.calcite.sql.parser.SqlParserTest#testTemporalTable()} - * for the same reason as {@link #testLateral()}: with the implicit table-function call form, - * {@code LATERAL products_temporal} now parses successfully and the error shifts to the next - * unexpected token ({@code for}). + * Overrides {@link org.apache.calcite.sql.parser.SqlParserTest#testTemporalTable()} for the + * same reason as {@link #testLateral()}: the shifted error position of the explicit-LATERAL + * case. */ @Test void testTemporalTable() { - // This test case is identical to Calcite's SqlParserTest.testTemporalTable(). final String sql0 = "select stream * from orders, products\n" + "for system_time as of TIMESTAMP '2011-01-02 00:00:00'"; @@ -4101,15 +4094,12 @@ class FlinkSqlParserImplTest extends SqlParserTest { + "`PRODUCTS` FOR SYSTEM_TIME AS OF TIMESTAMP '2011-01-02 00:00:00'"; sql(sql0).ok(expected0); - // This is the only test case that differs from Calcite's SqlParserTest.testTemporalTable(). - // Cannot use explicit LATERAL keyword. Error now points to "for" - // (the token after the implicit-table-function-call name). + // Differs from Calcite: explicit LATERAL fails at the identifier (no argument list). final String sql1 = - "select stream * from orders, LATERAL products_temporal\n" - + "^for^ system_time as of TIMESTAMP '2011-01-02 00:00:00'"; - sql(sql1).fails("(?s)Encountered \"for\" at line .*"); + "select stream * from orders, LATERAL ^products_temporal^\n" + + "for system_time as of TIMESTAMP '2011-01-02 00:00:00'"; + sql(sql1).fails("(?s)Encountered \"products_temporal for\" at line .*"); - // All following test cases are identical to Calcite's SqlParserTest.testTemporalTable(). // Inner join with a specific timestamp final String sql2 = "select stream * from orders join products_temporal\n" @@ -4152,13 +4142,11 @@ class FlinkSqlParserImplTest extends SqlParserTest { @Test void testLateralImplicitTableFunction() { - // LATERAL allows the implicit table-function-call form (no outer - // TABLE(...) wrapper). The non-LATERAL form was already backported - // in FLINK-36824. + // Implicit form: LATERAL fn(...) without the TABLE(...) wrapper. sql("select * from t, lateral ramp(t.x)") .ok("SELECT *\n" + "FROM `T`,\n" + "LATERAL TABLE(`RAMP`(`T`.`X`))"); - // Backward-compatible: explicit TABLE wrapper still works. + // Explicit TABLE wrapper still works. sql("select * from t, lateral table(ramp(t.x))") .ok("SELECT *\n" + "FROM `T`,\n" + "LATERAL TABLE(`RAMP`(`T`.`X`))"); @@ -4187,10 +4175,18 @@ class FlinkSqlParserImplTest extends SqlParserTest { + "`INPUT` => (TABLE `S`), " + "`LOAD_COMPLETED_CONDITION` => 'on_time'))"); - // LATERAL fn(...) as the very first FROM entry (no preceding table). The function call - // doesn't reference any outer column but the LATERAL keyword is still permitted by the - // grammar. + // LATERAL fn(...) as the first FROM entry (no preceding table). sql("select * from lateral ramp(3)").ok("SELECT *\n" + "FROM LATERAL TABLE(`RAMP`(3))"); + + // Documented LATERAL SNAPSHOT join form: JOIN LATERAL fn(named TABLE arg) AS alias ON ... + sql("select o.order_id, r.rate from orders as o " + + "join lateral snapshot(input => table currency_rates) as r " + + "on o.currency = r.currency") + .ok( + "SELECT `O`.`ORDER_ID`, `R`.`RATE`\n" + + "FROM `ORDERS` AS `O`\n" + + "INNER JOIN LATERAL TABLE(`SNAPSHOT`(`INPUT` => (TABLE `CURRENCY_RATES`))) AS `R` " + + "ON (`O`.`CURRENCY` = `R`.`CURRENCY`)"); } @Test
