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

Reply via email to