Skip to content

feat: #445 support CTE entity collection - #495

Open
Cythia828 wants to merge 1 commit into
DTStack:nextfrom
Cythia828:feat/support_cet_entity
Open

Cythia828 wants to merge 1 commit into
DTStack:nextfrom
Cythia828:feat/support_cet_entity

Conversation

@Cythia828

@Cythia828 Cythia828 commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

关联 Issue

Closes #445

效果预览

https://cythia828.github.io/monaco-sql-languages/

在预览地址选择对应方言后,可直接粘贴文末 测试 SQL 中各场景的用例验证补全效果。

问题背景

WITH cte AS (SELECT ...) SELECT * FROM cte 这类 CTE 语句,解析器把 CTE 名当成物理表收集:

  1. 类型错误:CTE 定义和引用都被标记为 TableDeclareType.LITERAL,与真实表无法区分,下游无法判断该表是虚拟表。
  2. 拿不到输出 schema:CTE 的 relatedEntities 为空,补全 FROM cte 后拿不到 cte 的列。
  3. 可见性错误:
    • CTE 在自己的 body 内可见(WITH a AS (SELECT * FROM a) 会自引用);
    • 多个同名 CTE 时引用绑定到错误的那个;
    • CTE 名匹配大小写敏感,WITH Sales_Data AS (...) 后 FROM sales_data 无法识别;
    • SELECT ... FROM 未写完(caret 停在 FROM 后)时 CTE 不可见,补全列表为空。
  4. caret 场景建议丢失:JOIN t1 ON 、WHERE orders. 等位置由于语法无法空匹配,语法建议和表实体可见性都不正确。

方案

在 EntityCollector 基类引入 「收集 → 延迟绑定」 两阶段机制,各方言只需在对应的 CTE 规则 exit 回调里调用一次 collectCte()。

1. 收集:collectCte() + CteDefinition

把 CTE 名 push 为 TABLE 实体、declareType = EXPRESSION,并记录:

  • endTokenIndex:本 CTE 项的结束 token(不是后续 CTE 的),语义为「该名字在此 token 及之前不可见」;
  • scopeEndTokenIndex:所属查询的结束 token,即该 CTE 的作用域上界;
  • body / columnNameCtxs / columnListCtx:用于推导输出 schema。

2. 作用域计算:cteScopeEndTokenIndex() / extendUnclosedScope()

沿父链查找 WithClauseContext / WithStatementContext / CtesContext / WithContext,取其所属查询的 stop 作为作用域上界;找不到则回退到当前语句范围。

针对 SELECT ... FROM (未闭合的 FROM/JOIN 导致解析在 caret 前中断)额外做 extendUnclosedScope():从 scopeEnd 向后扫描,跳过 hidden channel 与 EOF,遇到 ; 或「深度为 0 的 )」停止,把作用域延伸过去,保证 caret 处 CTE 仍然可见。

3. 输出 schema:finalizeCteDefinitions()

优先级:显式列清单 > body 查询的 QUERY_RESULT 实体。

  • 有列清单时,构造一个独立的 QUERY_RESULT(wrapOutwardColumns),不与实体集共享对象,避免污染 body 查询自身的列信息;
  • 无列清单时,用 findOutwardQueryResult() 在实体集中找被 body token 区间完全覆盖的 QUERY_RESULT,同深度取最靠前的那个;
  • body 结果默认直接复用,方言可通过 adaptOutwardQueryResult() 覆盖——Generic 就用 selectListText 把整条语句文本替换为 select 列表文本;
  • 结果挂到 entity.relatedEntities,使 CTE 定义与引用共享同一份 schema。

4. 引用绑定:bindCteReferences()

遍历实体集中 declareType === LITERAL 的 TABLE 实体,用 resolveCte(name, tokenIndex) 找最近的、位于该 token 之后、且作用域覆盖该 token 的同名 CTE,命中则改写为 EXPRESSION 并关联同一个 outward。名字比较经 normalizeIdent() 处理:去反引号/双引号/方括号,统一小写。

5. 可见性:markCteAccessibility()

caret 存在时,用 getPrevNonHiddenTokenIndex() 求有效 token 位置,CTE 定义实体仅在 resolveCte(自己的名字, caretToken) === 自己 时 isAccessible = true。由此天然得到正确语义:CTE 在自己 body 内不可见、定义之后可见、后续同名 CTE 覆盖前者。

6. 触发时机

exitProgram() 中执行一次;语句级 walk(不走 program)则在 exitStmt() 检测到 _stmtStack 为空时执行一次,保证两种遍历方式下都完成 finalize + bind。

方言接入

方言 规则 备注
Flink exitWithItem withItemName / columnName
Hive exitCteStatement columnNameList().columnName()
Impala exitNamedQuery columnAliases()
Spark exitNamedQuery identifierList().identifierSeq().errorCapturingIdentifier()
Trino exitNamedQuery columnAliases().identifier()
Generic exitNamedQuery 新增 cteColumnList 语法 + AliasedRelationContext.name 别名
PostgreSQL exitCommonTableExpr optColumnList().columnList().columnName()
MySQL exitCommonTableExpressions 该规则右递归((',' commonTableExpressions)?),ctx.stop 是后续 CTE 的结尾,因此取 uid()[0] 为名字、其余为列名,并单独定位 body 之后的第一个 ) 作为 endTokenIndex

顺带修复

Generic 收集器

  • exitColumnDefinition:原先 push 整个 columnDefinition,导致列名补全标签变成 age int 并把类型插入 SQL。改为 push columnRef,再用 dataType 的源码切片单独填 AttrName.colType(含 endIndex / endColumn 精确位置,支持 varchar(50) 这类带参数的完整类型)。
  • exitSelectItem:别名直接从 ctx._alias 取,避免向上搜父节点时串到后一个 select item 的别名、导致所有列被标成同一个名字。
  • index.ts:qualifiedName 在 expression 上下文(且不属于 tableName / tableNameCreate)时识别为 COLUMN 建议,使 JOIN t1 ON 能给出列建议。

语法(caret 空匹配,均受 caretTokenIndex >= 0 && shouldMatchEmpty() 语义谓词保护,不影响正常校验)

  • PostgreSqlParser.g4:新增 joinCondition 规则(joinQual | 空匹配),joinQual 的 ON 增加 columnNamePath (EQUAL columnNamePath)? 与 emptyColumn。SELECT * FROM a JOIN b 仍然报错(有测试覆盖)。
  • MySqlParser.g4:joinSpec 的 ON 支持 columnNamePath (EQUAL_SYMBOL columnNamePath)?。
  • TrinoSql.g4:primaryExpression 增加 base '.' 的空匹配 dereference,使 WHERE orders. 时表实体保持可见、能给出列建议。

错误上下文过滤

新增 _errorContexts + visitErrorNode:标记所有含解析错误的祖先上下文,pushEntity 时跳过。原因是错误恢复会把 rule context 拉长到整个子句,ctxToText 会产出 JOIN myOwn\nORDER BY 1 这类脏文本作为实体名。

测试

  • 新增 test/common/cteCases.ts 共用用例注册器,6 个用例覆盖:定义/引用收集与 schema、body 内不可见/之后可见、显式列清单作为输出 schema、同名 CTE 绑定最近者、大小写不敏感匹配、未完成 FROM 仍可见。在 flink / generic / hive / impala / mysql / postgresql / spark / trino 八个方言各注册一份 cte.test.ts。
  • PostgreSQL 增补 CTE + INSERT(DML 主体)用例、JOIN b 无 ON/USING 校验为非法。
  • Trino 增补 WHERE orders. 的实体可见性与 suggestion 用例。
  • Generic 增补 JOIN t1 ON 建议用例与列类型范围用例。
  • 更新既有断言:PostgreSQL 实体数 4 → 5(新增 CTE 实体);Generic 列文本不再包含类型。

其他

  • package.json 版本 4.5.1 → 4.5.2-beta.0。
  • src/lib/** 为 ANTLR 生成产物,随 .g4 改动一并重新生成(占 diff 绝大部分)。

影响面与回归建议

  • 改动落在 EntityCollector 基类,影响全部方言;建议重点回归 CTE 补全、表别名补全、CREATE TABLE 列补全、JOIN ... ON 补全。
  • _errorContexts 跳过逻辑会让含解析错误的 SQL 少收集部分实体——但这些实体文本本就不可用,属预期行为。

测试 SQL

以下用例均取自本 PR 的单元测试(test/common/cteCases.ts 等),已全部通过。在预览地址选择对应方言后粘贴即可验证,光标位置用 | 标注(粘贴时请去掉)。

1. 基础 CTE 收集与列补全(全部方言)

WITH sales_data AS (
    SELECT product_id, SUM(amount) AS total_sales
    FROM orders
),
top_products AS (
    SELECT product_id, total_sales
    FROM sales_data
)
SELECT tp.total_sales
FROM top_products tp
JOIN products p ON tp.product_id = p.id
  • sales_data、top_products 识别为 CTE(declareType = EXPRESSION),orders、products 仍为物理表(LITERAL)
  • top_products 的输出列取自 body 的 select 列表,SELECT tp.| 可补出 product_id、total_sales
  • 光标放到 FROM orders 的 orders 上:sales_data、top_products 均不可见(CTE 在自身 body 内不可见)
  • 光标放到第二个 CTE 的 FROM sales_data 上:sales_data 可见,orders 不可见

2. 显式列清单作为输出 schema

WITH cte (pid, sales) AS (
    SELECT product_id, SUM(amount) AS total_sales FROM orders
)
SELECT | FROM cte

cte 的输出列是列清单的 pid、sales,而非 body 的 product_id、total_sales。

3. 同名 CTE 绑定最近的一个

WITH cte AS (
    SELECT a FROM t1
),
cte AS (
    SELECT b FROM t2
)
SELECT * FROM cte

SELECT * FROM cte 绑定第二个 cte,补出的列是 b 而不是 a。

4. 大小写不敏感匹配

WITH Sales_Data AS (
    SELECT a FROM t1
)
SELECT | FROM sales_data

FROM sales_data 命中上面的 CTE 定义,能补出 a。

5. 未写完的 FROM 仍能补出 CTE 列(光标放在 FROM 之后)

WITH sales_data AS (
    SELECT product_id FROM orders
)
SELECT product_id
FROM |

sales_data 仍然可见,可补出 product_id。

6. JOIN ... ON 后的列补全(GenericSQL / MySQL / PostgreSQL)

SELECT o.age, u.age1 FROM orders JOIN t1 ON |

ON 之后能给出 orders、t1 两表的列建议。

7. WHERE table. 后的列补全(Trino)

SELECT * FROM orders WHERE orders.|

orders. 后能给出 orders 表的列建议,且 orders 实体 isAccessible 为 true。

8. 缺少 ON / USING 的 JOIN 仍报错(PostgreSQL)

SELECT * FROM a JOIN b

报语法错误。

SELECT * FROM a JOIN b ON a.id = b.id

校验通过。

9. CTE 用于 DML 主体(PostgreSQL)

WITH cte AS (
    SELECT a FROM t1
)
INSERT INTO t2
SELECT * FROM cte

cte 被识别为 CTE,SELECT * FROM cte 处能补出 a。

@Cythia828
Cythia828 requested a review from liuxy0551 October 8, 2026 02:39
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.

1 participant