This is an automated email from the ASF dual-hosted git repository.

fhueske pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 6fc280fb163 [FLINK-39780] Make TABLE keyword optional in LATERAL 
context (#28276)
6fc280fb163 is described below

commit 6fc280fb16372f23f2aa4ab6af3f18dbdda6285a
Author: Fabian Hueske <[email protected]>
AuthorDate: Fri Aug 28 09:59:33 2026 +0200

    [FLINK-39780] Make TABLE keyword optional in LATERAL context (#28276)
    
    Extend the SQL parser accept the implicit table-function-call form (e.g. 
"FROM t, LATERAL fn(args)") in addition to the explicit "LATERAL 
TABLE(fn(args))" form.
    Forward ports the Calcite change CALCITE-7183 to Flink.
    
    * Apply CALCITE-7183 Parser.jj change
    * extend FlinkSqlParserImplTest
    * adjust documentation to favor LATERAL table function calls without TABLE 
keyword
    
    Co-Generatedy: Claude Opus 4.8 (1M context)>
---
 .../sql-table-concepts/temporal_table_function.md  |   2 +-
 docs/content.zh/docs/dev/table/functions/udfs.md   |  16 +--
 .../content.zh/docs/sql/reference/queries/joins.md |  25 ++--
 .../docs/sql/reference/queries/overview.md         |   3 +-
 .../docs/sql/reference/queries/vector-search.md    |  16 +--
 .../sql-table-concepts/temporal_table_function.md  |   2 +-
 docs/content/docs/dev/table/functions/udfs.md      |  16 +--
 docs/content/docs/sql/reference/queries/joins.md   |  25 ++--
 .../content/docs/sql/reference/queries/overview.md |   3 +-
 .../docs/sql/reference/queries/vector-search.md    |  16 +--
 .../src/main/codegen/templates/Parser.jj           |   6 +
 .../flink/sql/parser/FlinkSqlParserImplTest.java   | 140 +++++++++++++++++++++
 12 files changed, 208 insertions(+), 62 deletions(-)

diff --git 
a/docs/content.zh/docs/concepts/sql-table-concepts/temporal_table_function.md 
b/docs/content.zh/docs/concepts/sql-table-concepts/temporal_table_function.md
index c0ef280ff06..c4d3c405280 100644
--- 
a/docs/content.zh/docs/concepts/sql-table-concepts/temporal_table_function.md
+++ 
b/docs/content.zh/docs/concepts/sql-table-concepts/temporal_table_function.md
@@ -112,7 +112,7 @@ SELECT
   SUM(amount * rate) AS amount
 FROM
   orders,
-  LATERAL TABLE (rates(order_time))
+  LATERAL rates(order_time)
 WHERE
   rates.currency = orders.currency
 ```
diff --git a/docs/content.zh/docs/dev/table/functions/udfs.md 
b/docs/content.zh/docs/dev/table/functions/udfs.md
index 273bd8c57c5..79fd1ba19ff 100644
--- a/docs/content.zh/docs/dev/table/functions/udfs.md
+++ b/docs/content.zh/docs/dev/table/functions/udfs.md
@@ -1088,7 +1088,7 @@ env.sqlQuery("SELECT GetBeverageName(beverageId) FROM 
Beverages");
 
 在 Table API 中,表值函数是通过 `.joinLateral(...)` 或者 `.leftOuterJoinLateral(...)` 
来使用的。`joinLateral` 算子会把外表(算子左侧的表)的每一行跟跟表值函数返回的所有行(位于算子右侧)进行 
(cross)join。`leftOuterJoinLateral` 
算子也是把外表(算子左侧的表)的每一行跟表值函数返回的所有行(位于算子右侧)进行(cross)join,并且如果表值函数返回 0 行也会保留外表的这一行。
 
-在 SQL 里面用 `JOIN` 或者 以 `ON TRUE` 为条件的 `LEFT JOIN` 来配合 `LATERAL 
TABLE(<TableFunction>)` 的使用。
+在 SQL 里面用 `JOIN` 或者 以 `ON TRUE` 为条件的 `LEFT JOIN` 来配合 `LATERAL 
<TableFunction>(...)` 的使用(等价的写法为 `LATERAL TABLE(<TableFunction>(...))`)。
 
 下面的例子展示了如何实现一个分隔函数并在查询里调用它,详情可参考[开发指南](#开发指南):
 
@@ -1147,17 +1147,17 @@ env
 // 在 SQL 里调用注册好的函数
 env.sqlQuery(
   "SELECT myField, word, length " +
-  "FROM MyTable, LATERAL TABLE(SplitFunction(myField))");
+  "FROM MyTable, LATERAL SplitFunction(myField)");
 env.sqlQuery(
   "SELECT myField, word, length " +
   "FROM MyTable " +
-  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE");
+  "LEFT JOIN LATERAL SplitFunction(myField) ON TRUE");
 
 // 在 SQL 里重命名函数字段
 env.sqlQuery(
   "SELECT myField, newWord, newLength " +
   "FROM MyTable " +
-  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON 
TRUE");
+  "LEFT JOIN LATERAL SplitFunction(myField) AS T(newWord, newLength) ON TRUE");
 
 ```
 {{< /tab >}}
@@ -1212,17 +1212,17 @@ env
 // 在 SQL 里调用注册好的函数
 env.sqlQuery(
   "SELECT myField, word, length " +
-  "FROM MyTable, LATERAL TABLE(SplitFunction(myField))");
+  "FROM MyTable, LATERAL SplitFunction(myField)");
 env.sqlQuery(
   "SELECT myField, word, length " +
   "FROM MyTable " +
-  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE")
+  "LEFT JOIN LATERAL SplitFunction(myField) ON TRUE")
 
 // 在 SQL 里重命名函数字段
 env.sqlQuery(
   "SELECT myField, newWord, newLength " +
   "FROM MyTable " +
-  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON 
TRUE")
+  "LEFT JOIN LATERAL SplitFunction(myField) AS T(newWord, newLength) ON TRUE")
 
 ```
 {{< /tab >}}
@@ -1312,7 +1312,7 @@ env.from("MyTable")
         .select($("*"))
 
 // 在 SQL 中调用已注册的函数
-env.sqlQuery("SELECT * FROM MyTable, LATERAL 
TABLE(BackgroundFunction(myField))");
+env.sqlQuery("SELECT * FROM MyTable, LATERAL BackgroundFunction(myField)");
 
 ```
 
diff --git a/docs/content.zh/docs/sql/reference/queries/joins.md 
b/docs/content.zh/docs/sql/reference/queries/joins.md
index a97312e9100..2aad89d7588 100644
--- a/docs/content.zh/docs/sql/reference/queries/joins.md
+++ b/docs/content.zh/docs/sql/reference/queries/joins.md
@@ -259,7 +259,7 @@ SELECT
   o_amount, r_rate
 FROM
   Orders,
-  LATERAL TABLE (Rates(o_proctime))
+  LATERAL Rates(o_proctime)
 WHERE
   r_currency = o_currency
 ```
@@ -285,7 +285,7 @@ SELECT
   o_amount, r_rate
 FROM
   Orders,
-  LATERAL TABLE (Rates(o_proctime))
+  LATERAL Rates(o_proctime)
 WHERE
   r_currency = o_currency
 ```
@@ -323,7 +323,7 @@ For example, the following query enriches an append-only 
stream of `orders` (the
 
 SELECT o.order_id, o.currency, o.amount, r.rate
 FROM orders AS o
-JOIN LATERAL TABLE(SNAPSHOT(input => TABLE currency_rates)) AS r
+JOIN LATERAL SNAPSHOT(input => TABLE currency_rates) AS r
 ON o.currency = r.currency;
 
 order_id  currency  amount  rate
@@ -360,19 +360,18 @@ The load phase is what distinguishes a `LATERAL SNAPSHOT` 
join from the [process
 
 **Syntax**
 
-The build side is wrapped in the `SNAPSHOT` table function inside a `LATERAL 
TABLE` clause. The outer (probe-side) table must be an append-only table. 
+The build side is wrapped in the `SNAPSHOT` table function, which is called 
with `LATERAL`. The outer (probe-side) table must be an append-only table. 
 Both `INNER JOIN` and `LEFT [OUTER] JOIN` are supported. The join requires at 
least one conjunctive equality predicate; additional non-equi predicates are 
allowed in the `ON` clause.
 
 ```sql
 SELECT [column_list]
 FROM probe_table
-[LEFT] JOIN LATERAL TABLE(
-    SNAPSHOT(
-        input                        => TABLE build_table,
-        [ load_completed_condition   => <'compile_time' | 'user_time'>, ]
-        [ load_completed_time        => <timestamp_ltz>, ]
-        [ load_completed_idle_timeout => <interval>, ]
-        [ state_ttl                  => <interval> ])) AS s
+[LEFT] JOIN LATERAL SNAPSHOT(
+    input                        => TABLE build_table,
+    [ load_completed_condition   => <'compile_time' | 'user_time'>, ]
+    [ load_completed_time        => <timestamp_ltz>, ]
+    [ load_completed_idle_timeout => <interval>, ]
+    [ state_ttl                  => <interval> ]) AS s
 ON probe_table.col = s.col
 ```
 
@@ -455,7 +454,7 @@ Table Function
 ```sql
 SELECT order_id, res
 FROM Orders,
-LATERAL TABLE(table_func(order_id)) t(res)
+LATERAL table_func(order_id) t(res)
 ```
 
 ### LEFT OUTER JOIN
@@ -465,7 +464,7 @@ LATERAL TABLE(table_func(order_id)) t(res)
 ```sql
 SELECT order_id, res
 FROM Orders
-LEFT OUTER JOIN LATERAL TABLE(table_func(order_id)) t(res)
+LEFT OUTER JOIN LATERAL table_func(order_id) t(res)
   ON TRUE
 ```
 
diff --git a/docs/content.zh/docs/sql/reference/queries/overview.md 
b/docs/content.zh/docs/sql/reference/queries/overview.md
index 7373700ab52..18aba399c61 100644
--- a/docs/content.zh/docs/sql/reference/queries/overview.md
+++ b/docs/content.zh/docs/sql/reference/queries/overview.md
@@ -316,7 +316,8 @@ tableReference:
 
 tablePrimary:
     [ TABLE ] tablePath [ dynamicTableOptions ] [systemTimePeriod] [[AS] 
correlationName]
-  | LATERAL TABLE '(' functionName '(' expression [, expression ]* ')' ')'
+  | [ LATERAL ] functionName '(' expression [, expression ]* ')'
+  | [ LATERAL ] TABLE '(' functionName '(' expression [, expression ]* ')' ')'
   | [ LATERAL ] '(' query ')'
   | UNNEST '(' expression ')'
 
diff --git a/docs/content.zh/docs/sql/reference/queries/vector-search.md 
b/docs/content.zh/docs/sql/reference/queries/vector-search.md
index 415004cd5dc..978568932b9 100644
--- a/docs/content.zh/docs/sql/reference/queries/vector-search.md
+++ b/docs/content.zh/docs/sql/reference/queries/vector-search.md
@@ -35,13 +35,13 @@ Flink SQL 提供了 `VECTOR_SEARCH` 表值函数 (TVF) 来在 SQL 查询中执
 ### 语法
 
 ```sql
-SELECT * FROM input_table, LATERAL TABLE(VECTOR_SEARCH(
+SELECT * FROM input_table, LATERAL VECTOR_SEARCH(
    TABLE vector_table, 
    input_table.vector_column, 
    DESCRIPTOR(index_column),
    top_k,
    [CONFIG => MAP['key', 'value']]
-   ))
+   )
 ```
 
 ### 参数
@@ -64,32 +64,32 @@ SELECT * FROM input_table, LATERAL TABLE(VECTOR_SEARCH(
 ```sql
 -- 基本用法
 SELECT * FROM 
-input_table, LATERAL TABLE(VECTOR_SEARCH(
+input_table, LATERAL VECTOR_SEARCH(
   TABLE vector_table,
   input_table.vector_column,
   DESCRIPTOR(index_column),
   10
-));
+);
 
 -- 带配置选项
 SELECT * FROM 
-input_table, LATERAL TABLE(VECTOR_SEARCH(
+input_table, LATERAL VECTOR_SEARCH(
   TABLE vector_table,
   input_table.vector_column,
   DESCRIPTOR(index_column),
   10,
   MAP['async', 'true', 'timeout', '100s']
-));
+);
 
 -- 使用命名参数
 SELECT * FROM 
-input_table, LATERAL TABLE(VECTOR_SEARCH(
+input_table, LATERAL VECTOR_SEARCH(
   SEARCH_TABLE => TABLE vector_table,
   COLUMN_TO_QUERY => input_table.vector_column,
   COLUMN_TO_SEARCH => DESCRIPTOR(index_column),
   TOP_K => 10,
   CONFIG => MAP['async', 'true', 'timeout', '100s']
-));
+);
 
 -- 使用常量值搜索
 SELECT * FROM TABLE(VECTOR_SEARCH(
diff --git 
a/docs/content/docs/concepts/sql-table-concepts/temporal_table_function.md 
b/docs/content/docs/concepts/sql-table-concepts/temporal_table_function.md
index 90ae717f988..1f87a57512f 100644
--- a/docs/content/docs/concepts/sql-table-concepts/temporal_table_function.md
+++ b/docs/content/docs/concepts/sql-table-concepts/temporal_table_function.md
@@ -112,7 +112,7 @@ SELECT
   SUM(amount * rate) AS amount
 FROM
   orders,
-  LATERAL TABLE (rates(order_time))
+  LATERAL rates(order_time)
 WHERE
   rates.currency = orders.currency
 ```
diff --git a/docs/content/docs/dev/table/functions/udfs.md 
b/docs/content/docs/dev/table/functions/udfs.md
index 405e0631e49..bd600e16cb3 100644
--- a/docs/content/docs/dev/table/functions/udfs.md
+++ b/docs/content/docs/dev/table/functions/udfs.md
@@ -1130,7 +1130,7 @@ In order to define a table function, one has to extend 
the base class `TableFunc
 
 In the Table API, a table function is used with `.joinLateral(...)` or 
`.leftOuterJoinLateral(...)`. The `joinLateral` operator (cross) joins each row 
from the outer table (table on the left of the operator) with all rows produced 
by the table-valued function (which is on the right side of the operator). The 
`leftOuterJoinLateral` operator joins each row from the outer table (table on 
the left of the operator) with all rows produced by the table-valued function 
(which is on the right sid [...]
 
-In SQL, use `LATERAL TABLE(<TableFunction>)` with `JOIN` or `LEFT JOIN` with 
an `ON TRUE` join condition.
+In SQL, use `LATERAL <TableFunction>(...)` (or the equivalent `LATERAL 
TABLE(<TableFunction>(...))`) with `JOIN` or `LEFT JOIN` with an `ON TRUE` join 
condition.
 
 The following example shows how to define your own split function and call it 
in a query. See the [Implementation Guide](#implementation-guide) for more 
details.
 
@@ -1189,17 +1189,17 @@ env
 // call registered function in SQL
 env.sqlQuery(
   "SELECT myField, word, length " +
-  "FROM MyTable, LATERAL TABLE(SplitFunction(myField))");
+  "FROM MyTable, LATERAL SplitFunction(myField)");
 env.sqlQuery(
   "SELECT myField, word, length " +
   "FROM MyTable " +
-  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE");
+  "LEFT JOIN LATERAL SplitFunction(myField) ON TRUE");
 
 // rename fields of the function in SQL
 env.sqlQuery(
   "SELECT myField, newWord, newLength " +
   "FROM MyTable " +
-  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON 
TRUE");
+  "LEFT JOIN LATERAL SplitFunction(myField) AS T(newWord, newLength) ON TRUE");
 
 ```
 {{< /tab >}}
@@ -1254,17 +1254,17 @@ env
 // call registered function in SQL
 env.sqlQuery(
   "SELECT myField, word, length " +
-  "FROM MyTable, LATERAL TABLE(SplitFunction(myField))")
+  "FROM MyTable, LATERAL SplitFunction(myField)")
 env.sqlQuery(
   "SELECT myField, word, length " +
   "FROM MyTable " +
-  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE")
+  "LEFT JOIN LATERAL SplitFunction(myField) ON TRUE")
 
 // rename fields of the function in SQL
 env.sqlQuery(
   "SELECT myField, newWord, newLength " +
   "FROM MyTable " +
-  "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON 
TRUE")
+  "LEFT JOIN LATERAL SplitFunction(myField) AS T(newWord, newLength) ON TRUE")
 
 ```
 {{< /tab >}}
@@ -1354,7 +1354,7 @@ env.from("MyTable")
         .select($("*"))
 
 // call registered function in SQL
-env.sqlQuery("SELECT * FROM MyTable, LATERAL 
TABLE(BackgroundFunction(myField))");
+env.sqlQuery("SELECT * FROM MyTable, LATERAL BackgroundFunction(myField)");
 
 ```
 
diff --git a/docs/content/docs/sql/reference/queries/joins.md 
b/docs/content/docs/sql/reference/queries/joins.md
index c323554d37b..c5a5123d96a 100644
--- a/docs/content/docs/sql/reference/queries/joins.md
+++ b/docs/content/docs/sql/reference/queries/joins.md
@@ -264,7 +264,7 @@ SELECT
   o_amount, r_rate
 FROM
   Orders,
-  LATERAL TABLE (Rates(o_proctime))
+  LATERAL Rates(o_proctime)
 WHERE
   r_currency = o_currency
 ```
@@ -290,7 +290,7 @@ SELECT
   o_amount, r_rate
 FROM
   Orders,
-  LATERAL TABLE (Rates(o_proctime))
+  LATERAL Rates(o_proctime)
 WHERE
   r_currency = o_currency
 ```
@@ -328,7 +328,7 @@ For example, the following query enriches an append-only 
stream of `orders` (the
 
 SELECT o.order_id, o.currency, o.amount, r.rate
 FROM orders AS o
-JOIN LATERAL TABLE(SNAPSHOT(input => TABLE currency_rates)) AS r
+JOIN LATERAL SNAPSHOT(input => TABLE currency_rates) AS r
 ON o.currency = r.currency;
 
 order_id  currency  amount  rate
@@ -369,19 +369,18 @@ By first loading the build side, a `LATERAL SNAPSHOT` 
join avoids this problem.
 
 **Syntax**
 
-The build side is wrapped in the `SNAPSHOT` table function inside a `LATERAL 
TABLE` clause. The outer (probe-side) table must be an append-only table. 
+The build side is wrapped in the `SNAPSHOT` table function, which is called 
with `LATERAL`. The outer (probe-side) table must be an append-only table. 
 Both `INNER JOIN` and `LEFT [OUTER] JOIN` are supported. The join requires at 
least one conjunctive equality predicate; additional non-equi predicates are 
allowed in the `ON` clause.
 
 ```sql
 SELECT [column_list]
 FROM probe_table
-[LEFT] JOIN LATERAL TABLE(
-    SNAPSHOT(
-        input                        => TABLE build_table,
-        [ load_completed_condition   => <'compile_time' | 'user_time'>, ]
-        [ load_completed_time        => <timestamp_ltz>, ]
-        [ load_completed_idle_timeout => <interval>, ]
-        [ state_ttl                  => <interval> ])) AS s
+[LEFT] JOIN LATERAL SNAPSHOT(
+    input                        => TABLE build_table,
+    [ load_completed_condition   => <'compile_time' | 'user_time'>, ]
+    [ load_completed_time        => <timestamp_ltz>, ]
+    [ load_completed_idle_timeout => <interval>, ]
+    [ state_ttl                  => <interval> ]) AS s
 ON probe_table.col = s.col
 ```
 
@@ -546,7 +545,7 @@ The row of the left (outer) table is dropped, if its table 
function call returns
 ```sql
 SELECT order_id, res
 FROM Orders,
-LATERAL TABLE(table_func(order_id)) t(res)
+LATERAL table_func(order_id) t(res)
 ```
 
 ### LEFT OUTER JOIN
@@ -556,7 +555,7 @@ If a table function call returns an empty result, the 
corresponding outer row is
 ```sql
 SELECT order_id, res
 FROM Orders
-LEFT OUTER JOIN LATERAL TABLE(table_func(order_id)) t(res)
+LEFT OUTER JOIN LATERAL table_func(order_id) t(res)
   ON TRUE
 ```
 
diff --git a/docs/content/docs/sql/reference/queries/overview.md 
b/docs/content/docs/sql/reference/queries/overview.md
index 1b607d96815..e5346941eab 100644
--- a/docs/content/docs/sql/reference/queries/overview.md
+++ b/docs/content/docs/sql/reference/queries/overview.md
@@ -316,7 +316,8 @@ tableReference:
 
 tablePrimary:
     [ TABLE ] tablePath [ dynamicTableOptions ] [systemTimePeriod] [[AS] 
correlationName]
-  | LATERAL TABLE '(' functionName '(' expression [, expression ]* ')' ')'
+  | [ LATERAL ] functionName '(' expression [, expression ]* ')'
+  | [ LATERAL ] TABLE '(' functionName '(' expression [, expression ]* ')' ')'
   | [ LATERAL ] '(' query ')'
   | UNNEST '(' expression ')'
 
diff --git a/docs/content/docs/sql/reference/queries/vector-search.md 
b/docs/content/docs/sql/reference/queries/vector-search.md
index 1ffac76125a..4f800ac356f 100644
--- a/docs/content/docs/sql/reference/queries/vector-search.md
+++ b/docs/content/docs/sql/reference/queries/vector-search.md
@@ -37,13 +37,13 @@ The `VECTOR_SEARCH` uses a processing-time attribute to 
correlate rows to the la
 
 ```sql
 SELECT * 
-FROM input_table, LATERAL TABLE(VECTOR_SEARCH(
+FROM input_table, LATERAL VECTOR_SEARCH(
    TABLE vector_table, 
    input_table.vector_column, 
    DESCRIPTOR(index_column),
    top_k,
    [CONFIG => MAP['key', 'value']]
-   ))
+   )
 ```
 
 ### Parameters
@@ -66,32 +66,32 @@ The following configuration options can be specified in the 
config map:
 ```sql
 -- Basic usage
 SELECT * FROM 
-input_table, LATERAL TABLE(VECTOR_SEARCH(
+input_table, LATERAL VECTOR_SEARCH(
   TABLE vector_table,
   input_table.vector_column,
   DESCRIPTOR(index_column),
   10
-));
+);
 
 -- With configuration options
 SELECT * FROM 
-input_table, LATERAL TABLE(VECTOR_SEARCH(
+input_table, LATERAL VECTOR_SEARCH(
   TABLE vector_table,
   input_table.vector_column,
   DESCRIPTOR(index_column),
   10,
   MAP['async', 'true', 'timeout', '100s']
-));
+);
 
 -- Using named parameters
 SELECT * FROM 
-input_table, LATERAL TABLE(VECTOR_SEARCH(
+input_table, LATERAL VECTOR_SEARCH(
   SEARCH_TABLE => TABLE vector_table,
   COLUMN_TO_QUERY => input_table.vector_column,
   COLUMN_TO_SEARCH => DESCRIPTOR(index_column),
   TOP_K => 10,
   CONFIG => MAP['async', 'true', 'timeout', '100s']
-));
+);
 
 -- Searching with contant value
 SELECT * 
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 a031434e8ca..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
@@ -2284,6 +2284,12 @@ SqlNode TableRef3(ExprContext exprContext, boolean 
lateral) :
         {
             tableRef = unnestOp.createCall(s.end(this), (List<SqlNode>) args);
         }
+    |
+        LOOKAHEAD(<LATERAL> CompoundTableIdentifier() <LPAREN>)
+        <LATERAL> { lateral = true; }
+        tableName = CompoundTableIdentifier()
+        tableRef = ImplicitTableFunctionCallArgs(tableName)
+        tableRef = addLateral(tableRef, lateral)
     |
         [ <LATERAL> { lateral = true; } ]
         tableRef = TableFunctionCall()
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 92f8e34c770..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
@@ -4049,6 +4049,146 @@ class FlinkSqlParserImplTest extends SqlParserTest {
                 .fails("(?s).*Encountered \"\\)\" at .*");
     }
 
+    /**
+     * 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() {
+        // Differs from Calcite: LATERAL <identifier> without an argument list 
fails at the
+        // identifier.
+        sql("select * from lateral ^emp^").fails("(?s)Encountered \"emp 
<EOF>\" at .*");
+
+        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 .*");
+
+        final String expected = "SELECT *\n" + "FROM LATERAL TABLE(`RAMP`(1))";
+
+        // Good: LATERAL TABLE function(arg, arg)
+        sql("select * from lateral table(ramp(1))").ok(expected);
+        sql("select * from lateral table(ramp(1)) as t").ok(expected + " AS 
`T`");
+        sql("select * from lateral table(ramp(1)) as t(x)").ok(expected + " AS 
`T` (`X`)");
+        // Bad: Parentheses make it look like a sub-query
+        sql("select * from lateral (^table (ramp(1))^)").fails("Expected query 
or join");
+
+        // Good: LATERAL (subQuery)
+        final String expected2 = "SELECT *\n" + "FROM LATERAL (SELECT *\n" + 
"FROM `EMP`)";
+        sql("select * from lateral (select * from emp)").ok(expected2);
+        sql("select * from lateral (select * from emp) as t").ok(expected2 + " 
AS `T`");
+        sql("select * from lateral (select * from emp) as t(x)").ok(expected2 
+ " AS `T` (`X`)");
+    }
+
+    /**
+     * 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() {
+        final String sql0 =
+                "select stream * from orders, products\n"
+                        + "for system_time as of TIMESTAMP '2011-01-02 
00:00:00'";
+        final String expected0 =
+                "SELECT STREAM *\n"
+                        + "FROM `ORDERS`,\n"
+                        + "`PRODUCTS` FOR SYSTEM_TIME AS OF TIMESTAMP 
'2011-01-02 00:00:00'";
+        sql(sql0).ok(expected0);
+
+        // 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 \"products_temporal for\" at line 
.*");
+
+        // Inner join with a specific timestamp
+        final String sql2 =
+                "select stream * from orders join products_temporal\n"
+                        + "for system_time as of timestamp '2011-01-02 
00:00:00'\n"
+                        + "on orders.productid = products_temporal.productid";
+        final String expected2 =
+                "SELECT STREAM *\n"
+                        + "FROM `ORDERS`\n"
+                        + "INNER JOIN `PRODUCTS_TEMPORAL` "
+                        + "FOR SYSTEM_TIME AS OF TIMESTAMP '2011-01-02 
00:00:00' "
+                        + "ON (`ORDERS`.`PRODUCTID` = 
`PRODUCTS_TEMPORAL`.`PRODUCTID`)";
+        sql(sql2).ok(expected2);
+
+        // Left join with a timestamp field
+        final String sql3 =
+                "select stream * from orders left join products_temporal\n"
+                        + "for system_time as of orders.rowtime "
+                        + "on orders.productid = products_temporal.productid";
+        final String expected3 =
+                "SELECT STREAM *\n"
+                        + "FROM `ORDERS`\n"
+                        + "LEFT JOIN `PRODUCTS_TEMPORAL` "
+                        + "FOR SYSTEM_TIME AS OF `ORDERS`.`ROWTIME` "
+                        + "ON (`ORDERS`.`PRODUCTID` = 
`PRODUCTS_TEMPORAL`.`PRODUCTID`)";
+        sql(sql3).ok(expected3);
+
+        // Left join with a timestamp expression
+        final String sql4 =
+                "select stream * from orders left join products_temporal\n"
+                        + "for system_time as of orders.rowtime - INTERVAL '3' 
DAY "
+                        + "on orders.productid = products_temporal.productid";
+        final String expected4 =
+                "SELECT STREAM *\n"
+                        + "FROM `ORDERS`\n"
+                        + "LEFT JOIN `PRODUCTS_TEMPORAL` "
+                        + "FOR SYSTEM_TIME AS OF (`ORDERS`.`ROWTIME` - 
INTERVAL '3' DAY) "
+                        + "ON (`ORDERS`.`PRODUCTID` = 
`PRODUCTS_TEMPORAL`.`PRODUCTID`)";
+        sql(sql4).ok(expected4);
+    }
+
+    @Test
+    void testLateralImplicitTableFunction() {
+        // 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`))");
+
+        // 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`))");
+
+        // CROSS JOIN form.
+        sql("select * from t cross join lateral ramp(t.x) on true")
+                .ok(
+                        "SELECT *\n"
+                                + "FROM `T`\n"
+                                + "CROSS JOIN LATERAL TABLE(`RAMP`(`T`.`X`)) 
ON TRUE");
+
+        // LEFT JOIN form with ON condition.
+        sql("select * from t left join lateral ramp(t.x) on t.a = 1")
+                .ok(
+                        "SELECT *\n"
+                                + "FROM `T`\n"
+                                + "LEFT JOIN LATERAL TABLE(`RAMP`(`T`.`X`)) ON 
(`T`.`A` = 1)");
+
+        // Named arguments and TABLE-typed arg passed to the function.
+        sql("select * from t, lateral snapshot("
+                        + "input => table s, "
+                        + "load_completed_condition => 'on_time')")
+                .ok(
+                        "SELECT *\n"
+                                + "FROM `T`,\n"
+                                + "LATERAL TABLE(`SNAPSHOT`("
+                                + "`INPUT` => (TABLE `S`), "
+                                + "`LOAD_COMPLETED_CONDITION` => 'on_time'))");
+
+        // 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
     void testVariantType() {
         sql("CREATE TABLE t (\n" + "v variant" + "\n)")

Reply via email to