PHP实现:根据光标位置提取多行SQL中光标所在的单条查询语句
如何在Web SQL编辑器中执行光标所在的SQL查询
我之前也踩过类似的坑——直接按分号分割SQL肯定行不通,毕竟字符串、注释甚至存储过程里到处都可能出现分号,这些都不能算作语句结束的标志。你已经拿到了光标前后的SQL片段,接下来的核心就是精准识别前半部分中真正属于SQL语句结束的分号,而非那些“伪分号”。
核心思路:用状态机过滤无效分号
我们可以通过遍历字符,跟踪当前所处的语境(正常SQL、单引号字符串、双引号字符串、单行注释、多行注释),只有在正常SQL语境下的分号才算作有效分隔符。
具体实现步骤
1. 编写函数识别前半部分的最后一个有效分号
这个函数会遍历前半段SQL,跳过所有字符串和注释里的分号,只记录真正的语句结束分号:
function findLastValidSemiColon(sql) { let inSingleQuote = false; let inDoubleQuote = false; let inMultiLineComment = false; let inSingleLineComment = false; let lastValidSemi = -1; for (let i = 0; i < sql.length; i++) { const char = sql[i]; const nextChar = sql[i+1] || ''; // 处理单行注释(-- 开头) if (!inSingleQuote && !inDoubleQuote && !inMultiLineComment) { if (char === '-' && nextChar === '-') { inSingleLineComment = true; i++; // 跳过第二个'-' continue; } } // 处理多行注释(/* ... */) if (!inSingleQuote && !inDoubleQuote && !inSingleLineComment) { if (char === '/' && nextChar === '*') { inMultiLineComment = true; i++; // 跳过'*' continue; } else if (char === '*' && nextChar === '/') { inMultiLineComment = false; i++; // 跳过'/' continue; } } // 单行注释结束(遇到换行) if (inSingleLineComment && char === '\n') { inSingleLineComment = false; continue; } // 处理单引号字符串(跳过转义的单引号) if (!inDoubleQuote && !inMultiLineComment && !inSingleLineComment) { if (char === "'" && (i === 0 || sql[i-1] !== '\\')) { inSingleQuote = !inSingleQuote; continue; } } // 处理双引号字符串(跳过转义的双引号) if (!inSingleQuote && !inMultiLineComment && !inSingleLineComment) { if (char === '"' && (i === 0 || sql[i-1] !== '\\')) { inDoubleQuote = !inDoubleQuote; continue; } } // 记录正常语境下的分号 if (!inSingleQuote && !inDoubleQuote && !inMultiLineComment && !inSingleLineComment && char === ';') { lastValidSemi = i; } } return lastValidSemi; }
2. 找到后半部分的第一个有效分号,确定查询结束位置
同样用状态机思路,找到光标后半段的第一个有效分号,作为当前查询的结束标记:
function findFirstValidSemiColon(sql) { let inSingleQuote = false; let inDoubleQuote = false; let inMultiLineComment = false; let inSingleLineComment = false; for (let i = 0; i < sql.length; i++) { const char = sql[i]; const nextChar = sql[i+1] || ''; // 复用上面的语境判断逻辑,找到第一个有效分号就返回 if (!inSingleQuote && !inDoubleQuote && !inMultiLineComment) { if (char === '-' && nextChar === '-') { inSingleLineComment = true; i++; continue; } } if (!inSingleQuote && !inDoubleQuote && !inSingleLineComment) { if (char === '/' && nextChar === '*') { inMultiLineComment = true; i++; continue; } else if (char === '*' && nextChar === '/') { inMultiLineComment = false; i++; continue; } } if (inSingleLineComment && char === '\n') { inSingleLineComment = false; continue; } if (!inDoubleQuote && !inMultiLineComment && !inSingleLineComment) { if (char === "'" && (i === 0 || sql[i-1] !== '\\')) { inSingleQuote = !inSingleQuote; continue; } } if (!inSingleQuote && !inMultiLineComment && !inSingleLineComment) { if (char === '"' && (i === 0 || sql[i-1] !== '\\')) { inDoubleQuote = !inDoubleQuote; continue; } } if (!inSingleQuote && !inDoubleQuote && !inMultiLineComment && !inSingleLineComment && char === ';') { return i; } } return sql.length; // 如果没找到分号,就取到SQL末尾 }
3. 拼接出光标所在的目标查询
结合上面两个函数,就能提取出要执行的SQL:
// 假设你已获取:fullSql(完整SQL字符串)、cursorPosition(光标在全文的索引) const firstPart = fullSql.slice(0, cursorPosition); const lastValidSemi = findLastValidSemiColon(firstPart); const queryStart = lastValidSemi === -1 ? 0 : lastValidSemi + 1; const secondPart = fullSql.slice(cursorPosition); const firstValidSemi = findFirstValidSemiColon(secondPart); const queryEnd = cursorPosition + firstValidSemi + 1; // +1包含分号 const targetQuery = fullSql.slice(queryStart, queryEnd).trim(); // 现在targetQuery就是光标所在的那条SQL,可以直接执行了
额外注意事项
- 转义规则适配:上面的代码只处理了
\'/\"这种转义方式,如果你的SQL方言支持其他转义(比如PostgreSQL用''表示单引号),需要调整判断逻辑。 - 存储场景处理:如果要支持存储过程/函数(内部包含分号),还需要识别
DELIMITER指令,临时更改语句分隔符,这个场景需要额外的逻辑处理。 - 无分号查询:如果光标所在的查询没有以分号结尾,要确保函数能正确取到SQL末尾。
内容的提问来源于stack exchange,提问作者alecsvaldez
相关产品推荐
相关产品推荐

