Files
MyGoNavi/frontend/src/utils/sqlStatementSelection.ts
Syngnat 1ae44941dd 🐛 fix(sql-editor): 修复脚本执行拆分与元数据只读提示
- Oracle 匿名块:识别 BEGIN/DECLARE...END 块,避免按内部分号错误拆分
- 执行路径:PL/SQL 块跳过批量写入路径,保持单条语句语义
- SQL 文件:同步修复流式 SQL 文件拆分逻辑
- 查询结果:系统元数据表保持只读但不再弹业务表主键提示
- 测试覆盖:补充前后端拆分、执行和 information_schema 回归用例
2026-06-03 17:11:05 +08:00

293 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export interface SqlStatementRange {
start: number;
end: number;
text: string;
}
export type SqlExecutionSelectionSource = 'selection' | 'statement' | 'line';
export interface SqlExecutionSelection {
sql: string;
source: SqlExecutionSelectionSource;
}
const isWhitespace = (ch: string): boolean => (
ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f'
);
const isSqlIdentifierStart = (ch: string): boolean => /^[A-Za-z_]$/.test(ch);
const isSqlIdentifierPart = (ch: string): boolean => /^[A-Za-z0-9_$#]$/.test(ch);
const skipSqlWhitespaceAndComments = (text: string, position: number): number => {
let index = position;
while (index < text.length) {
const ch = text[index];
const next = index + 1 < text.length ? text[index + 1] : '';
if (isWhitespace(ch)) {
index += 1;
continue;
}
if (ch === '-' && next === '-') {
index += 2;
while (index < text.length && text[index] !== '\n') index += 1;
continue;
}
if (ch === '/' && next === '*') {
index += 2;
while (index + 1 < text.length && !(text[index] === '*' && text[index + 1] === '/')) {
index += 1;
}
if (index + 1 < text.length) index += 2;
continue;
}
break;
}
return index;
};
const nextSqlSignificantToken = (text: string, position: number): string => {
const index = skipSqlWhitespaceAndComments(text, position);
if (index >= text.length || !isSqlIdentifierStart(text[index])) return '';
let end = index + 1;
while (end < text.length && isSqlIdentifierPart(text[end])) end += 1;
return text.slice(index, end).toLowerCase();
};
const nextSqlSignificantChar = (text: string, position: number): string => {
const index = skipSqlWhitespaceAndComments(text, position);
return index >= text.length ? '' : text[index];
};
const shouldEnterPlsqlBeginBlock = (text: string, tokenEnd: number): boolean => {
const nextChar = nextSqlSignificantChar(text, tokenEnd);
if (!nextChar || nextChar === ';') return false;
return !['transaction', 'work', 'isolation', 'read', 'write'].includes(nextSqlSignificantToken(text, tokenEnd));
};
const shouldEnterPlsqlDeclareBlock = (text: string, tokenEnd: number): boolean => Boolean(nextSqlSignificantToken(text, tokenEnd));
const isPlsqlControlEnd = (text: string, tokenEnd: number): boolean => (
['if', 'loop', 'case'].includes(nextSqlSignificantToken(text, tokenEnd))
);
const trimStatementRange = (sql: string, start: number, end: number): SqlStatementRange | null => {
let nextStart = Math.max(0, start);
let nextEnd = Math.min(sql.length, Math.max(start, end));
while (nextStart < nextEnd && isWhitespace(sql[nextStart])) {
nextStart++;
}
while (nextEnd > nextStart && isWhitespace(sql[nextEnd - 1])) {
nextEnd--;
}
if (nextStart >= nextEnd) {
return null;
}
return {
start: nextStart,
end: nextEnd,
text: sql.slice(nextStart, nextEnd),
};
};
export const findSqlStatementRanges = (sql: string): SqlStatementRange[] => {
const text = String(sql || '').replace(/\r\n/g, '\n');
const ranges: SqlStatementRange[] = [];
let statementStart = 0;
let inSingle = false;
let inDouble = false;
let inBacktick = false;
let escaped = false;
let inLineComment = false;
let inBlockComment = false;
let dollarTag: string | null = null;
let plsqlDepth = 0;
let plsqlDeclareBeginSkips = 0;
let justClosedPLSQLBlock = false;
const push = (end: number) => {
const range = trimStatementRange(text, statementStart, end);
if (range) {
ranges.push(range);
}
};
for (let index = 0; index < text.length; index++) {
const ch = text[index];
const next = index + 1 < text.length ? text[index + 1] : '';
const prev = index > 0 ? text[index - 1] : '';
const next2 = index + 2 < text.length ? text[index + 2] : '';
if (dollarTag) {
if (text.startsWith(dollarTag, index)) {
index += dollarTag.length - 1;
dollarTag = null;
}
continue;
}
if (inLineComment) {
if (ch === '\n') {
inLineComment = false;
}
continue;
}
if (inBlockComment) {
if (ch === '*' && next === '/') {
index++;
inBlockComment = false;
}
continue;
}
if (!inSingle && !inDouble && !inBacktick) {
if (ch === '/' && next === '*') {
index++;
inBlockComment = true;
continue;
}
if (ch === '#') {
inLineComment = true;
continue;
}
if (ch === '-' && next === '-' && (index === 0 || isWhitespace(prev)) && (next2 === '' || isWhitespace(next2))) {
index++;
inLineComment = true;
continue;
}
if (ch === '$') {
const match = text.slice(index).match(/^\$[A-Za-z0-9_]*\$/);
if (match?.[0]) {
dollarTag = match[0];
index += dollarTag.length - 1;
continue;
}
}
}
if (escaped) {
escaped = false;
continue;
}
if ((inSingle || inDouble) && ch === '\\') {
escaped = true;
continue;
}
if (!inDouble && !inBacktick && ch === "'") {
inSingle = !inSingle;
continue;
}
if (!inSingle && !inBacktick && ch === '"') {
inDouble = !inDouble;
continue;
}
if (!inSingle && !inDouble && ch === '`') {
inBacktick = !inBacktick;
continue;
}
if (!inSingle && !inDouble && !inBacktick && !dollarTag && isSqlIdentifierStart(ch)) {
let tokenEnd = index + 1;
while (tokenEnd < text.length && isSqlIdentifierPart(text[tokenEnd])) {
tokenEnd++;
}
const token = text.slice(index, tokenEnd).toLowerCase();
if (token === 'begin' && plsqlDeclareBeginSkips > 0) {
plsqlDeclareBeginSkips--;
justClosedPLSQLBlock = false;
} else if (token === 'begin' && shouldEnterPlsqlBeginBlock(text, tokenEnd)) {
plsqlDepth++;
justClosedPLSQLBlock = false;
} else if (token === 'declare' && shouldEnterPlsqlDeclareBlock(text, tokenEnd)) {
plsqlDepth++;
plsqlDeclareBeginSkips++;
justClosedPLSQLBlock = false;
} else if (token === 'end' && plsqlDepth > 0 && !isPlsqlControlEnd(text, tokenEnd)) {
plsqlDepth--;
if (plsqlDeclareBeginSkips > plsqlDepth) {
plsqlDeclareBeginSkips = plsqlDepth;
}
justClosedPLSQLBlock = plsqlDepth === 0;
}
index = tokenEnd - 1;
continue;
}
if (!inSingle && !inDouble && !inBacktick && (ch === ';' || ch === '')) {
if (plsqlDepth > 0) {
continue;
}
push(justClosedPLSQLBlock ? index + 1 : index);
statementStart = index + 1;
justClosedPLSQLBlock = false;
continue;
}
}
push(text.length);
return ranges;
};
export const resolveCurrentSqlStatementRange = (sql: string, cursorOffset: number): SqlStatementRange | null => {
const text = String(sql || '').replace(/\r\n/g, '\n');
const offset = Math.max(0, Math.min(text.length, Number.isFinite(cursorOffset) ? cursorOffset : 0));
const ranges = findSqlStatementRanges(text);
if (ranges.length === 0) {
return null;
}
const containingRange = ranges.find((range) => offset >= range.start && offset <= range.end);
if (containingRange) {
return containingRange;
}
const nextRange = ranges.find((range) => offset < range.start);
if (nextRange) {
return nextRange;
}
return ranges[ranges.length - 1];
};
export const resolveExecutableSql = (
sql: string,
cursorOffset: number,
selectedSql = '',
): SqlExecutionSelection | null => {
const selected = String(selectedSql || '').trim();
if (selected) {
return { sql: selectedSql, source: 'selection' };
}
const text = String(sql || '').replace(/\r\n/g, '\n');
const offset = Math.max(0, Math.min(text.length, Number.isFinite(cursorOffset) ? cursorOffset : 0));
const ranges = findSqlStatementRanges(text);
const statement = ranges.find((range) => offset >= range.start && offset <= range.end);
if (statement?.text.trim()) {
return { sql: statement.text, source: 'statement' };
}
const lineStart = text.lastIndexOf('\n', Math.max(0, offset - 1)) + 1;
const nextLineBreak = text.indexOf('\n', offset);
const lineEnd = nextLineBreak === -1 ? text.length : nextLineBreak;
const line = text.slice(lineStart, lineEnd).trim();
if (line) {
const lineStatement = [...ranges].reverse().find((range) => range.start < lineEnd && range.end >= lineStart);
if (lineStatement?.text.trim()) {
return { sql: lineStatement.text, source: 'statement' };
}
}
if (line) {
return { sql: line, source: 'line' };
}
return null;
};