Skip to content

修复:MySQL 等方言下 WHERE/FROM 补全被字符串、注释或特殊字符标识符误判 - #6774

Merged
t8y2 merged 3 commits into
t8y2:mainfrom
q396921921:fix/mysql-completion-where-special-names
Aug 27, 2026
Merged

修复:MySQL 等方言下 WHERE/FROM 补全被字符串、注释或特殊字符标识符误判#6774
t8y2 merged 3 commits into
t8y2:mainfrom
q396921921:fix/mysql-completion-where-special-names

Conversation

@q396921921

Copy link
Copy Markdown
Contributor

问题

SQL 补全在提取"当前语句引用了哪些表"、以及判断"光标是否在 WHERE/FROM/JOIN 等关键字之后"时,是靠正则在原始文本上直接扫描 from/join/where 等关键字,会被字符串字面量、注释、带特殊字符的引号标识符里恰好出现的同名文本骗过去(例如 WHERE note = 'from 测试表' 会被误判为引用了 测试表 表)。

对应 #6496:MySQL 下数据库/表/字段名为中文(非 ASCII)且不带反引号时,WHERE 后按空格或 Tab 补全不提示字段——表引用提取的正则只支持 ASCII 无引号标识符,中文库名/表名匹配不上。

修复

  • 表引用提取、以及文件里另外 7 处做关键字扫描的调用点,统一改用已有的 tokenizeSqlSemantic 方言感知词法器先屏蔽字符串字面量和注释,再扫描关键字,而不是在原始文本上盲扫。
  • 无引号标识符匹配扩展支持非 ASCII 字符(中文/日文/韩文等),修复中文库名/表名在 WHERE 子句补全里匹配不上的问题。
  • 双引号 "..." 在 MySQL 系方言下到底算字符串还是标识符取决于运行时 sql_mode,代码本身不追踪这个设置,因此不用一个全局开关强行二选一,而是按语法位置消歧:只在紧跟比较运算符(=/</<>等)之后才当字符串屏蔽,其余位置(含紧跟 FROM/JOIN 之后)保留为标识符,修复了 FROM "orders" 这类双引号表名无法补全的问题。
  • 修复 MySQL 反斜杠转义字符串、#/-- 注释处理、反引号/方括号标识符内含特殊字符导致扫描错位等一系列相关问题(按方言区分是否识别反斜杠转义,避免误伤 Postgres 等本身不支持反斜杠转义的方言)。

验证

  • pnpm exec vitest run apps/desktop/src/lib/__tests__/sql/(1034 个测试全部通过,含新增/更新的用例)
  • pnpm exec tsc --noEmit -p apps/desktop/tsconfig.json
  • pnpm exec vitest run apps/desktop/src(全仓库 7221 个测试全部通过)
  • 针对本 PR 修复的所有具体场景(中文表名、双引号表名、反斜杠转义、#/-- 注释、反引号/方括号内含特殊字符等)逐条手工复现确认,而非仅依赖测试通过

Fixes #6496

@github-actions github-actions Bot added the area/desktop Desktop application or Tauri shell label Aug 20, 2026

@t8y2 t8y2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 tokenizer 对 MySQL 双引号字符串的处理仍有 correctness blocker:只有位于比较运算符后的双引号内容会被当作字符串屏蔽;函数参数或 CASE 返回值中的双引号字符串仍会被识别成 quoted identifier。

例如下面两个场景都会错误产生 ghost_table / ghost_case 表引用:

SELECT CONCAT("from ghost_table", name) FROM real_table WHERE
SELECT CASE WHEN enabled THEN "from ghost_case" ELSE "ok" END FROM real_table WHERE

PR 现有 111 个 completion 测试通过,但新增的这两个定向探针均失败。请根据当前 MySQL SQL mode 明确区分双引号字符串与标识符,不要依赖前一个 token 是否为比较运算符,并为函数参数、CASE、嵌套表达式及 ANSI_QUOTES 模式增加回归测试。

q396921921 pushed a commit to q396921921/dbx that referenced this pull request Aug 20, 2026
…not just after operators

Problem: maskSqlLiteralsAndComments only masked a double-quoted "..." span
as a value when it immediately followed a comparison operator (col = "x"),
leaving it readable as a quoted identifier everywhere else. Under MySQL's
actual default sql_mode (ANSI_QUOTES disabled), "..." is a plain string
literal everywhere, not just after "=" -- so a double-quoted string inside
a function argument (CONCAT("from ghost_table", name)) or a CASE branch
(THEN "from ghost_case") still misread its text as a real table reference.

Solution: tokenizeSqlSemantic gains an opt-in mysqlDoubleQuoteIsString
option that tokenizes "..." as a string (same escaping rules as '...')
instead of a quoted identifier. maskSqlLiteralsAndComments now enables it
for MySQL-family dialects (mysql, doris, starrocks -- the same scope
already used for MySQL's other sql_mode-governed lexical quirks), so
double-quoted strings are masked unconditionally rather than only in the
narrow after-operator position. Non-MySQL dialects (Postgres, SQL Server,
generic), where "..." unconditionally means identifier quoting, keep the
prior operator-adjacency masking for the col = "value" shape.

This is a deliberate tradeoff: a MySQL connection actually running with
ANSI_QUOTES enabled would no longer resolve a double-quoted FROM target
(e.g. FROM "orders") as a table, since that mode can't be observed at
completion time. This matches MySQL's actual default and removes a class
of false-positive ghost-table misdetections that affect every default-
sql_mode MySQL user.

Addresses review feedback on t8y2#6774.
@q396921921

Copy link
Copy Markdown
Contributor Author

已修复,思路调整为:不再依赖"前一个 token 是否为比较运算符"这种位置启发式判断,而是直接在 tokenizer 层面为 MySQL 系方言(mysql/doris/starrocks)新增 mysqlDoubleQuoteIsString 选项,让 "..." 在这些方言下默认按 MySQL 实际默认 sql_mode(未开启 ANSI_QUOTES)的语义,无条件当作字符串字面量处理(转义规则与 '...' 完全一致),不再局限于运算符之后的位置。这样函数参数、CASE 分支、嵌套表达式里的双引号字符串都会被正确屏蔽:

SELECT CONCAT("from ghost_table", name) FROM real_table WHERE
SELECT CASE WHEN enabled THEN "from ghost_case" ELSE "ok" END FROM real_table WHERE

两个探针用例复测均只识别出 real_table,不再产生 ghost_table/ghost_case

对于 Postgres/SQL Server 等 "..." 恒为标识符的方言,保留原有的"紧跟运算符即屏蔽"逻辑不变。

已知取舍:由于完成层无法感知连接实际的运行时 sql_mode,若某个 MySQL 连接确实开启了 ANSI_QUOTES,FROM "orders" 这类写法将不再被识别为表名(相应测试已更新为验证默认 sql_mode 下的行为)。tokenizer 仍保留识别符模式作为默认值,并新增了针对该模式的回归测试(tokens.spec.ts),供后续接入真实 sql_mode 探测时复用。

新增/调整了 CONCAT、CASE、嵌套表达式及 ANSI_QUOTES 模式相关的回归测试,全部通过。

@t8y2 t8y2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

原来的默认 MySQL ghost-table 问题已经修复,但当前实现明确引入了一个新的兼容性回归:maskSqlLiteralsAndComments() 对所有 MySQL-family 连接都把 "..." 当字符串,因此启用 ANSI_QUOTES 的连接不再能从 FROM "orders" 中识别 orders

MySQL 官方说明,启用 ANSI_QUOTES 后双引号是合法的 identifier quote,并且双引号内容不再是字符串:
https://dev.mysql.com/doc/refman/8.4/en/identifiers.html

Exact-head 探针结果:

  • CONCAT("from ghost_table", ...) 和 CASE 中的默认模式字符串:通过,不再产生 ghost table。
  • SELECT * FROM "orders" WHERE(MySQL ANSI_QUOTES):失败,referencedTables 返回空数组。
  • PR 原有 tokenizer/completion 测试:122/122 通过;新增兼容探针 1/2 失败。

代码注释中将这个回归描述为“accepted tradeoff”,但 DBX 不能用常见默认模式静默破坏已有的合法 SQL 模式。请让 completion 能根据实际 session sql_mode 选择语义,或者采用同时保留 table-identifier 位置并屏蔽函数参数、CASE/value 位置的解析策略,并补充默认模式与 ANSI_QUOTES 两套回归测试。

q396921921 pushed a commit to q396921921/dbx that referenced this pull request Aug 26, 2026
…n, not dialect default

Addresses review feedback on t8y2#6774: unconditionally masking "..." as a
string literal for every MySQL-family connection broke ANSI_QUOTES
connections, where "..." is legitimate identifier quoting (FROM "orders"
stopped resolving orders as a table). sql_mode isn't observable at parse
time, so instead of guessing a dialect-wide default, "..." right after a
table-reference introducer (FROM/JOIN/STRAIGHT_JOIN/UPDATE/APPLY, or a
dotted qualifier continuation) is now left unmasked as a potential
identifier, while every other position (function args, CASE branches,
operator-adjacent values) still masks it as a value. This is correct
under both sql_modes at once, without needing to know which one is
actually in effect.

Also decouples how a "..." span is scanned (backslash-escape-aware when
mysqlBackslashEscape is set) from what kind it's tagged as, fixing a
latent desync for Hive/Impala/Spark identifiers containing an escaped
quote.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@q396921921

Copy link
Copy Markdown
Contributor Author

已修复该兼容性回归,思路调整为按 token 位置解析,而不是按方言猜一个全局默认:

"..." 紧跟在 FROM/JOIN/STRAIGHT_JOIN/UPDATE/APPLY 之后,或作为其 . 限定符的延续(如 "db"."orders")时,保留为潜在标识符——兼容 ANSI_QUOTES 开启的连接;其余位置(函数参数、CASE 分支、运算符相邻的 value)仍按 MySQL 默认 sql_mode 当字符串屏蔽。由于 completion 层确实拿不到连接运行时的实际 sql_mode,这个按位置区分的策略可以让两种 sql_mode 同时被正确处理,不用二选一。

SELECT * FROM "orders" WHERE            -- ANSI_QUOTES: orders 仍被识别为表
SELECT CONCAT("from ghost_table", name) FROM real_table WHERE   -- 默认模式:不再产生 ghost_table

新增回归测试覆盖:FROM "orders"、注释夹在 FROM 和标识符之间(FROM /* c */ "orders")、schema 限定("db"."orders")、JOIN 中 quoted + unquoted 混用、UPDATE 目标 vs SET 中的双引号 value、以及反斜杠转义双引号字符串的 token 流不错位(mysql / hive 各一个用例,验证之前依赖 mysqlDoubleQuoteIsString 判断"读法"的隐藏耦合已解开)。

复测结果:sqlCompletion.context.spec.ts 121/121,全量 apps/desktop/src/lib/__tests__/sql/ 46 files / 1047 tests 全部通过。

q396921921 pushed a commit to q396921921/dbx that referenced this pull request Aug 26, 2026
…not just after operators

Problem: maskSqlLiteralsAndComments only masked a double-quoted "..." span
as a value when it immediately followed a comparison operator (col = "x"),
leaving it readable as a quoted identifier everywhere else. Under MySQL's
actual default sql_mode (ANSI_QUOTES disabled), "..." is a plain string
literal everywhere, not just after "=" -- so a double-quoted string inside
a function argument (CONCAT("from ghost_table", name)) or a CASE branch
(THEN "from ghost_case") still misread its text as a real table reference.

Solution: tokenizeSqlSemantic gains an opt-in mysqlDoubleQuoteIsString
option that tokenizes "..." as a string (same escaping rules as '...')
instead of a quoted identifier. maskSqlLiteralsAndComments now enables it
for MySQL-family dialects (mysql, doris, starrocks -- the same scope
already used for MySQL's other sql_mode-governed lexical quirks), so
double-quoted strings are masked unconditionally rather than only in the
narrow after-operator position. Non-MySQL dialects (Postgres, SQL Server,
generic), where "..." unconditionally means identifier quoting, keep the
prior operator-adjacency masking for the col = "value" shape.

This is a deliberate tradeoff: a MySQL connection actually running with
ANSI_QUOTES enabled would no longer resolve a double-quoted FROM target
(e.g. FROM "orders") as a table, since that mode can't be observed at
completion time. This matches MySQL's actual default and removes a class
of false-positive ghost-table misdetections that affect every default-
sql_mode MySQL user.

Addresses review feedback on t8y2#6774.
q396921921 pushed a commit to q396921921/dbx that referenced this pull request Aug 26, 2026
…n, not dialect default

Addresses review feedback on t8y2#6774: unconditionally masking "..." as a
string literal for every MySQL-family connection broke ANSI_QUOTES
connections, where "..." is legitimate identifier quoting (FROM "orders"
stopped resolving orders as a table). sql_mode isn't observable at parse
time, so instead of guessing a dialect-wide default, "..." right after a
table-reference introducer (FROM/JOIN/STRAIGHT_JOIN/UPDATE/APPLY, or a
dotted qualifier continuation) is now left unmasked as a potential
identifier, while every other position (function args, CASE branches,
operator-adjacent values) still masks it as a value. This is correct
under both sql_modes at once, without needing to know which one is
actually in effect.

Also decouples how a "..." span is scanned (backslash-escape-aware when
mysqlBackslashEscape is set) from what kind it's tagged as, fixing a
latent desync for Hive/Impala/Spark identifiers containing an escaped
quote.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@q396921921
q396921921 force-pushed the fix/mysql-completion-where-special-names branch from 4646555 to c6e88d1 Compare August 26, 2026 15:06
t8y2 pushed a commit to q396921921/dbx that referenced this pull request Aug 27, 2026
…not just after operators

Problem: maskSqlLiteralsAndComments only masked a double-quoted "..." span
as a value when it immediately followed a comparison operator (col = "x"),
leaving it readable as a quoted identifier everywhere else. Under MySQL's
actual default sql_mode (ANSI_QUOTES disabled), "..." is a plain string
literal everywhere, not just after "=" -- so a double-quoted string inside
a function argument (CONCAT("from ghost_table", name)) or a CASE branch
(THEN "from ghost_case") still misread its text as a real table reference.

Solution: tokenizeSqlSemantic gains an opt-in mysqlDoubleQuoteIsString
option that tokenizes "..." as a string (same escaping rules as '...')
instead of a quoted identifier. maskSqlLiteralsAndComments now enables it
for MySQL-family dialects (mysql, doris, starrocks -- the same scope
already used for MySQL's other sql_mode-governed lexical quirks), so
double-quoted strings are masked unconditionally rather than only in the
narrow after-operator position. Non-MySQL dialects (Postgres, SQL Server,
generic), where "..." unconditionally means identifier quoting, keep the
prior operator-adjacency masking for the col = "value" shape.

This is a deliberate tradeoff: a MySQL connection actually running with
ANSI_QUOTES enabled would no longer resolve a double-quoted FROM target
(e.g. FROM "orders") as a table, since that mode can't be observed at
completion time. This matches MySQL's actual default and removes a class
of false-positive ghost-table misdetections that affect every default-
sql_mode MySQL user.

Addresses review feedback on t8y2#6774.
t8y2 pushed a commit to q396921921/dbx that referenced this pull request Aug 27, 2026
…n, not dialect default

Addresses review feedback on t8y2#6774: unconditionally masking "..." as a
string literal for every MySQL-family connection broke ANSI_QUOTES
connections, where "..." is legitimate identifier quoting (FROM "orders"
stopped resolving orders as a table). sql_mode isn't observable at parse
time, so instead of guessing a dialect-wide default, "..." right after a
table-reference introducer (FROM/JOIN/STRAIGHT_JOIN/UPDATE/APPLY, or a
dotted qualifier continuation) is now left unmasked as a potential
identifier, while every other position (function args, CASE branches,
operator-adjacent values) still masks it as a value. This is correct
under both sql_modes at once, without needing to know which one is
actually in effect.

Also decouples how a "..." span is scanned (backslash-escape-aware when
mysqlBackslashEscape is set) from what kind it's tagged as, fixing a
latent desync for Hive/Impala/Spark identifiers containing an escaped
quote.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@t8y2
t8y2 force-pushed the fix/mysql-completion-where-special-names branch from c6e88d1 to d24c035 Compare August 27, 2026 00:59

@t8y2 t8y2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

维护者冲突代解决说明(已推送):

  • 旧 head:c6e88d1a → 新 head:d24c0350(rebase 到 main f41b5dbd,备份 ref 已留)
  • 冲突原因:main 侧 #7329(SQL Server datepart 补全)与本 PR 在 sqlCompletion.ts 两处相邻改动:
    1. getSqlCompletionContext 内 main 插入的 sqlServerDatepartCompletionValues 调用行,与本 PR 给 isCreateTableColumnTypeContext/preferredKeywordsForCompletionoptions.databaseType 参数的行相邻 —— 解法为逐字并集,双方都保留;
    2. main 在 isCreateTableColumnTypeContext 上方新增的 sqlServerDatepartCompletionValues 函数,与本 PR 修改该函数签名(maskSqlLiteralsAndComments)相邻 —— 解法为保留 main 新函数 + 本 PR 的新签名。
  • 语义交互核查:datepart 路径走 findActiveFunctionCall(原始 beforeCursor),sqlserver 不在 MySQL 屏蔽方言列表内,与本 PR 的双引号屏蔽策略无语义交叠。
  • 验证:rebase 后 git diff --check 干净、无冲突标记、变更仍为原 4 个文件、无依赖/lockfile 变更;range-diff 逐提交映射一致;全量 sql 测试目录 47 文件 1144 用例连续两轮全过(含 #7329 的 datepart 用例);tsc --noEmit 0 错误。
  • 两个此前的 CHANGES_REQUESTED blocker(函数参数/CASE 双引号字符串误判、ANSI_QUOTES 兼容)已在 c6e88d1a 验证解决,本次 rebase 未改动相关逻辑。

zhuxuesong and others added 3 commits August 27, 2026 09:12
… table or clause keywords

Problem: extractReferencedTables and several WHERE/FROM/JOIN-keyword
detectors scanned raw SQL text for keywords without masking string
literals, comments, or special-character identifiers first. Text
that merely appeared inside a string ('from 测试表'), a comment
(-- from ghost_table), or a backtick/bracket-quoted identifier could
be misread as a real table reference or clause keyword. Unquoted
non-ASCII (e.g. Chinese) schema/table names also failed to match the
table-reference pattern used by WHERE-clause column completion.

Solution: mask string literals and comments via the existing
dialect-aware tokenizeSqlSemantic lexer before scanning for
keywords/table references, instead of blind regex/character scans.
Double-quoted spans are disambiguated by syntactic position (masked
as a value only right after a comparison operator) rather than a
single global per-dialect flag, since MySQL's own interpretation of
`"..."` depends on the runtime sql_mode this code can't observe.
Extended unquoted-identifier matching to support non-ASCII characters
for dialects that allow them (MySQL, Postgres, SQL Server, ...),
fixing WHERE-clause column completion for Chinese schema/table names.

Also fixes: MySQL backslash-escaped string literals (dialect-gated
to avoid misreading a trailing backslash in dialects that don't use
it, e.g. a Postgres Windows-path literal), MySQL `#`/`--` comment
handling, backtick/bracket-quoted identifiers containing special
characters, and generalizes the same masking to every other
keyword-detection call site in sqlCompletion.ts that had the same
exposure (isColumnCompletionExpressionStart, hasSelectListExpression,
isAfterSelectBodyExpression, isAfterConditionExpression,
isAfterJoinModifierContext, isInTableListContext,
detectOracleTableFunctionContext, isCreateTableColumnTypeContext).

Fixes t8y2#6496
…not just after operators

Problem: maskSqlLiteralsAndComments only masked a double-quoted "..." span
as a value when it immediately followed a comparison operator (col = "x"),
leaving it readable as a quoted identifier everywhere else. Under MySQL's
actual default sql_mode (ANSI_QUOTES disabled), "..." is a plain string
literal everywhere, not just after "=" -- so a double-quoted string inside
a function argument (CONCAT("from ghost_table", name)) or a CASE branch
(THEN "from ghost_case") still misread its text as a real table reference.

Solution: tokenizeSqlSemantic gains an opt-in mysqlDoubleQuoteIsString
option that tokenizes "..." as a string (same escaping rules as '...')
instead of a quoted identifier. maskSqlLiteralsAndComments now enables it
for MySQL-family dialects (mysql, doris, starrocks -- the same scope
already used for MySQL's other sql_mode-governed lexical quirks), so
double-quoted strings are masked unconditionally rather than only in the
narrow after-operator position. Non-MySQL dialects (Postgres, SQL Server,
generic), where "..." unconditionally means identifier quoting, keep the
prior operator-adjacency masking for the col = "value" shape.

This is a deliberate tradeoff: a MySQL connection actually running with
ANSI_QUOTES enabled would no longer resolve a double-quoted FROM target
(e.g. FROM "orders") as a table, since that mode can't be observed at
completion time. This matches MySQL's actual default and removes a class
of false-positive ghost-table misdetections that affect every default-
sql_mode MySQL user.

Addresses review feedback on t8y2#6774.
…n, not dialect default

Addresses review feedback on t8y2#6774: unconditionally masking "..." as a
string literal for every MySQL-family connection broke ANSI_QUOTES
connections, where "..." is legitimate identifier quoting (FROM "orders"
stopped resolving orders as a table). sql_mode isn't observable at parse
time, so instead of guessing a dialect-wide default, "..." right after a
table-reference introducer (FROM/JOIN/STRAIGHT_JOIN/UPDATE/APPLY, or a
dotted qualifier continuation) is now left unmasked as a potential
identifier, while every other position (function args, CASE branches,
operator-adjacent values) still masks it as a value. This is correct
under both sql_modes at once, without needing to know which one is
actually in effect.

Also decouples how a "..." span is scanned (backslash-escape-aware when
mysqlBackslashEscape is set) from what kind it's tagged as, fixing a
latent desync for Hive/Impala/Spark identifiers containing an escaped
quote.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@t8y2
t8y2 force-pushed the fix/mysql-completion-where-special-names branch from d24c035 to 55a237a Compare August 27, 2026 01:13

@t8y2 t8y2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

两轮 CHANGES_REQUESTED 的 blocker 均已在 c6e88d1a 验证解决,予以通过:

  1. 函数参数/CASE/嵌套表达式中的双引号字符串不再被解析为表引用(CONCAT/CASE 探针有逐字回归测试);
  2. FROM "orders" 在 ANSI_QUOTES 语义下仍识别为表引用(含注释穿插、限定名、JOIN/UPDATE 场景),默认模式与 ANSI_QUOTES 两套回归测试齐备。

维护者已代解与 #7329 的 sqlCompletion.ts 冲突并 rebase 到最新 main(详见此前评论);期间发现并修复了 main 上 #7326 引入的 Keyboard 图标 mock 缺失(caaa893)。当前 head 55a237aa3 本地 sql 全目录 + 相关组件测试 1153/1153 通过、tsc 零错误,CI frontend/changes/label 全绿。

@t8y2
t8y2 merged commit 4a3d63b into t8y2:main Aug 27, 2026
14 checks passed
@t8y2

t8y2 commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Thanks for the contribution! Merged in 4a3d63b, will be released in the next version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/desktop Desktop application or Tauri shell

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] mysql8.3.0 where后按空格或tab字段不提示,无法补全的问题

2 participants