7 files changed
+117
-41
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { currentStatementFrameRangeTo, estimateInlineHintVisualColumns, isWideSqlChar, visualSqlColumns, visualSqlColumnsWithInlineHints } from "@/lib/sql/currentStatementFrame";
|
||||
import { currentStatementFrameRangeTo, estimateInlineHintVisualColumns, isWideSqlChar, shouldRebuildCurrentStatementFrame, visualSqlColumns, visualSqlColumnsWithInlineHints } from "@/lib/sql/currentStatementFrame";
|
||||
import type { SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
|
||||
describe("currentStatementFrameRangeTo", () => {
|
||||
@@ -14,6 +14,18 @@ describe("currentStatementFrameRangeTo", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRebuildCurrentStatementFrame", () => {
|
||||
it("reuses frame decorations for pure viewport updates", () => {
|
||||
expect(shouldRebuildCurrentStatementFrame({ docChanged: false, selectionSet: false, configurationChanged: false })).toBe(false);
|
||||
});
|
||||
|
||||
it("rebuilds after document, selection, or configuration updates", () => {
|
||||
expect(shouldRebuildCurrentStatementFrame({ docChanged: true, selectionSet: false, configurationChanged: false })).toBe(true);
|
||||
expect(shouldRebuildCurrentStatementFrame({ docChanged: false, selectionSet: true, configurationChanged: false })).toBe(true);
|
||||
expect(shouldRebuildCurrentStatementFrame({ docChanged: false, selectionSet: false, configurationChanged: true })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("visualSqlColumns", () => {
|
||||
it("counts ASCII as one column, tabs as four, and CJK/fullwidth characters as two", () => {
|
||||
expect(visualSqlColumns("A\t中B")).toBe(1 + 4 + 2 + 2);
|
||||
|
||||
@@ -346,6 +346,24 @@ describe("splitSqlStatementRanges", () => {
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(oracleIssue2405PlSql, "oracle"))).toEqual([oracleIssue2405PlSql]);
|
||||
});
|
||||
|
||||
it("splits large Dameng package bodies without repeated prefix parsing", () => {
|
||||
const body = Array.from({ length: 3000 }, (_, index) => ` v_value := v_value + ${index % 10};`).join("\n");
|
||||
const packageBody = `CREATE OR REPLACE PACKAGE BODY app.big_pkg AS
|
||||
PROCEDURE run IS
|
||||
v_value NUMBER := 0;
|
||||
BEGIN
|
||||
${body}
|
||||
END run;
|
||||
END big_pkg;`;
|
||||
const sql = `${packageBody}\n/\nSELECT 1;`;
|
||||
const startedAt = performance.now();
|
||||
const ranges = splitSqlStatementRanges(sql, "dameng");
|
||||
const elapsedMs = performance.now() - startedAt;
|
||||
|
||||
expect(rangeSqlTexts(ranges)).toEqual([packageBody, "SELECT 1"]);
|
||||
expect(elapsedMs).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
it("keeps nested GaussDB procedure blocks together", () => {
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(gaussDbNestedProcedure, "gaussdb"))).toEqual([gaussDbNestedProcedure]);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,10 @@ export function currentStatementFrameRangeTo(nextChar: string, range: SqlTextRan
|
||||
return nextChar === ";" ? range.to + 1 : range.to;
|
||||
}
|
||||
|
||||
export function shouldRebuildCurrentStatementFrame(update: { docChanged: boolean; selectionSet: boolean; configurationChanged: boolean }): boolean {
|
||||
return update.docChanged || update.selectionSet || update.configurationChanged;
|
||||
}
|
||||
|
||||
export function visualSqlColumns(text: string): number {
|
||||
let columns = 0;
|
||||
for (const ch of text) {
|
||||
|
||||
@@ -33,18 +33,21 @@ function significantTokens(tokens: readonly SqlSemanticToken[]): SqlSemanticToke
|
||||
return tokens.filter((item) => item.kind !== "comment");
|
||||
}
|
||||
|
||||
function statementSpans(sql: string, tokens: readonly SqlSemanticToken[]): SqlSemanticSpan[] {
|
||||
const spans: SqlSemanticSpan[] = [];
|
||||
function statementTokenGroups(sql: string, tokens: readonly SqlSemanticToken[]): Array<{ span: SqlSemanticSpan; tokens: SqlSemanticToken[] }> {
|
||||
const groups: Array<{ span: SqlSemanticSpan; tokens: SqlSemanticToken[] }> = [];
|
||||
let start = 0;
|
||||
for (const item of tokens) {
|
||||
let tokenStart = 0;
|
||||
for (let index = 0; index < tokens.length; index += 1) {
|
||||
const item = tokens[index]!;
|
||||
if (item.kind !== "punctuation" || item.text !== ";" || item.depth !== 0) continue;
|
||||
const span = trimSpan(sql, start, item.span.start);
|
||||
if (span.end > span.start) spans.push(span);
|
||||
if (span.end > span.start) groups.push({ span, tokens: significantTokens(tokens.slice(tokenStart, index)) });
|
||||
start = item.span.end;
|
||||
tokenStart = index + 1;
|
||||
}
|
||||
const last = trimSpan(sql, start, sql.length);
|
||||
if (last.end > last.start) spans.push(last);
|
||||
return spans;
|
||||
if (last.end > last.start) groups.push({ span: last, tokens: significantTokens(tokens.slice(tokenStart)) });
|
||||
return groups;
|
||||
}
|
||||
|
||||
function trimSpan(sql: string, start: number, end: number): SqlSemanticSpan {
|
||||
@@ -444,10 +447,8 @@ export function parseInsertValuesClausesInRanges(sql: string, ranges: readonly T
|
||||
export function parseInsertValuesClauses(sql: string): InsertValuesClause[] {
|
||||
if (!sql.trim()) return [];
|
||||
const allTokens = tokenizeSqlSemantic(sql);
|
||||
const spans = statementSpans(sql, allTokens);
|
||||
const clauses: InsertValuesClause[] = [];
|
||||
for (const span of spans) {
|
||||
const tokens = significantTokens(tokensInSpan(allTokens, span));
|
||||
for (const { span, tokens } of statementTokenGroups(sql, allTokens)) {
|
||||
const clause = parseInsertClause(tokens, span);
|
||||
if (clause) clauses.push(clause);
|
||||
}
|
||||
|
||||
@@ -300,6 +300,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
||||
let state: QuoteState = "none";
|
||||
let dollarTag = "";
|
||||
let postgresDollarQuotedRoutine = false;
|
||||
let oraclePlSqlStatementEnd: number | null | undefined;
|
||||
let i = 0;
|
||||
|
||||
const isWhitespace = (ch: string) => ch === " " || ch === "\t" || ch === "\r" || ch === "\n";
|
||||
@@ -317,6 +318,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
||||
statementEnd = -1;
|
||||
pendingHintStart = -1;
|
||||
postgresDollarQuotedRoutine = false;
|
||||
oraclePlSqlStatementEnd = undefined;
|
||||
return;
|
||||
}
|
||||
const trimmedTo = trimRangeEnd(sql, statementStart, to);
|
||||
@@ -327,6 +329,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
||||
statementEnd = -1;
|
||||
pendingHintStart = -1;
|
||||
postgresDollarQuotedRoutine = false;
|
||||
oraclePlSqlStatementEnd = undefined;
|
||||
};
|
||||
|
||||
while (i < len) {
|
||||
@@ -512,12 +515,16 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
||||
// Internal semicolons remain part of the routine body.
|
||||
flush();
|
||||
} else {
|
||||
const statementSoFar = statementStart === -1 ? "" : sql.slice(statementStart, i);
|
||||
const isOraclePlSql = isOracleLikeDatabase(databaseType) && !postgresDollarQuotedRoutine && statementStart !== -1 && startsWithOraclePlSqlBlock(statementSoFar);
|
||||
const isSapHanaScriptBlock = isSapHanaScriptBlockDatabase(databaseType) && statementStart !== -1 && startsWithSapHanaScriptBlock(statementSoFar);
|
||||
if (oraclePlSqlStatementEnd === undefined && isOracleLikeDatabase(databaseType) && !postgresDollarQuotedRoutine && statementStart !== -1) {
|
||||
const statementSoFar = sql.slice(statementStart, i);
|
||||
oraclePlSqlStatementEnd = startsWithOraclePlSqlBlock(statementSoFar) ? statementStart + (oraclePlSqlBlockEnd(sql.slice(statementStart)) ?? sql.length - statementStart) : null;
|
||||
}
|
||||
const resolvedOraclePlSqlStatementEnd = oraclePlSqlStatementEnd;
|
||||
const isOraclePlSql = typeof resolvedOraclePlSqlStatementEnd === "number";
|
||||
const isSapHanaScriptBlock = isSapHanaScriptBlockDatabase(databaseType) && statementStart !== -1 && startsWithSapHanaScriptBlock(sql.slice(statementStart, i));
|
||||
if (isOraclePlSql || isSapHanaScriptBlock) {
|
||||
markContent(i);
|
||||
if (isOraclePlSql && !oraclePlSqlBlockIsComplete(sql.slice(statementStart, i + 1))) {
|
||||
if (isOraclePlSql && i + 1 < resolvedOraclePlSqlStatementEnd) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -1638,7 +1645,10 @@ function mysqlRoutineTokens(sql: string, parameterOptions?: SqlParameterOptions)
|
||||
}
|
||||
|
||||
function startsWithOraclePlSqlBlock(sql: string): boolean {
|
||||
const words = oraclePlSqlWords(sql);
|
||||
return startsWithOraclePlSqlBlockWords(oraclePlSqlWords(sql));
|
||||
}
|
||||
|
||||
function startsWithOraclePlSqlBlockWords(words: readonly string[]): boolean {
|
||||
const first = words[0];
|
||||
if (!first) return false;
|
||||
if (ORACLE_PL_SQL_BLOCK_STARTERS.has(first)) return first !== "BEGIN" || words[1] !== "TRANSACTION";
|
||||
@@ -1713,17 +1723,23 @@ function sapHanaScriptBlockIsComplete(sql: string): boolean {
|
||||
return sawBegin && stack.length === 0 && tokens[tokens.length - 1]?.kind === "semicolon";
|
||||
}
|
||||
|
||||
function oraclePlSqlBlockIsComplete(sql: string): boolean {
|
||||
function oraclePlSqlBlockEnd(sql: string): number | null {
|
||||
const tokens = oraclePlSqlTokens(sql);
|
||||
if (!startsWithOraclePlSqlBlock(sql)) return false;
|
||||
const words = oraclePlSqlWordValues(tokens);
|
||||
if (!startsWithOraclePlSqlBlockWords(words)) return null;
|
||||
|
||||
// Package/type specifications have no BEGIN — only declarations closed by
|
||||
// END [name];. Bodies also own an outer END beyond nested routine END pairs.
|
||||
const objectKind = oraclePlSqlCreateObjectKind(sql);
|
||||
const objectKind = oraclePlSqlCreateObjectKind(words);
|
||||
const stack: string[] = objectKind === "body" ? ["OBJECT_BODY"] : objectKind === "spec" ? ["OBJECT_SPEC"] : [];
|
||||
let sawBegin = false;
|
||||
for (let index = 0; index < tokens.length; index += 1) {
|
||||
const token = tokens[index];
|
||||
if (token.kind === "semicolon") {
|
||||
const complete = objectKind === "spec" ? stack.length === 0 : sawBegin && stack.length === 0;
|
||||
if (complete) return token.to;
|
||||
continue;
|
||||
}
|
||||
if (token.kind !== "word") continue;
|
||||
|
||||
if (token.value === "DECLARE") {
|
||||
@@ -1769,12 +1785,7 @@ function oraclePlSqlBlockIsComplete(sql: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
const endsWithSemicolon = tokens[tokens.length - 1]?.kind === "semicolon";
|
||||
if (objectKind === "spec") {
|
||||
// Specs complete on outer END [name]; without requiring a BEGIN block.
|
||||
return stack.length === 0 && endsWithSemicolon;
|
||||
}
|
||||
return sawBegin && stack.length === 0 && endsWithSemicolon;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1783,8 +1794,7 @@ function oraclePlSqlBlockIsComplete(sql: string): boolean {
|
||||
* - spec: PACKAGE specification only (declarations + END, no BEGIN)
|
||||
* - null: ordinary SQL / other objects (including plain CREATE TYPE ... AS OBJECT)
|
||||
*/
|
||||
function oraclePlSqlCreateObjectKind(sql: string): "body" | "spec" | null {
|
||||
const words = oraclePlSqlWords(sql);
|
||||
function oraclePlSqlCreateObjectKind(words: readonly string[]): "body" | "spec" | null {
|
||||
if (words[0] !== "CREATE") return null;
|
||||
|
||||
const index = skipOraclePlSqlCreateModifiers(words, 1);
|
||||
@@ -1799,13 +1809,22 @@ function oraclePlSqlCreateObjectKind(sql: string): "body" | "spec" | null {
|
||||
}
|
||||
|
||||
function oraclePlSqlWords(sql: string): string[] {
|
||||
return oraclePlSqlTokens(sql)
|
||||
.filter((token): token is { kind: "word"; value: string } => token.kind === "word")
|
||||
.map((token) => token.value);
|
||||
return oraclePlSqlWordValues(oraclePlSqlTokens(sql));
|
||||
}
|
||||
|
||||
function oraclePlSqlTokens(sql: string): Array<{ kind: "word" | "semicolon"; value: string }> {
|
||||
const tokens: Array<{ kind: "word" | "semicolon"; value: string }> = [];
|
||||
interface OraclePlSqlToken {
|
||||
kind: "word" | "semicolon";
|
||||
value: string;
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
function oraclePlSqlWordValues(tokens: readonly OraclePlSqlToken[]): string[] {
|
||||
return tokens.filter((token) => token.kind === "word").map((token) => token.value);
|
||||
}
|
||||
|
||||
function oraclePlSqlTokens(sql: string): OraclePlSqlToken[] {
|
||||
const tokens: OraclePlSqlToken[] = [];
|
||||
let state: QuoteState | "lineComment" | "blockComment" = "none";
|
||||
let i = 0;
|
||||
|
||||
@@ -1867,15 +1886,16 @@ function oraclePlSqlTokens(sql: string): Array<{ kind: "word" | "semicolon"; val
|
||||
continue;
|
||||
}
|
||||
if (ch === ";") {
|
||||
tokens.push({ kind: "semicolon", value: ";" });
|
||||
tokens.push({ kind: "semicolon", value: ";", from: i, to: i + 1 });
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const word = /^[A-Za-z_][\w$]*/.exec(sql.slice(i))?.[0];
|
||||
if (word) {
|
||||
tokens.push({ kind: "word", value: word.toUpperCase() });
|
||||
i += word.length;
|
||||
if (/[A-Za-z_]/.test(ch)) {
|
||||
const from = i;
|
||||
i += 1;
|
||||
while (i < sql.length && /[A-Za-z0-9_$]/.test(sql[i] ?? "")) i += 1;
|
||||
tokens.push({ kind: "word", value: sql.slice(from, i).toUpperCase(), from, to: i });
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
|
||||
Reference in new issue
Block a user