fix(editor): validate MyBatis statement placeholders
This commit is contained in:
parent
9e5dfa3396
commit
5309170bf7
|
|
@ -18,6 +18,7 @@ import { currentStatementFrameRangeTo, visualSqlColumnsWithInlineHints } from "@
|
|||
import { expandToSqlStatementWindow, parseInsertValueHints } from "@/lib/sql/insertValueHints";
|
||||
import { insertValueHintColumnNames } from "@/lib/sql/insertValueHintColumns";
|
||||
import { formatSqlText, compressSqlText, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
import { enabledSqlParameterSyntaxes, resolveSqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax";
|
||||
import { blankLineDeletionChanges, replaceSelectedEditorText } from "@/lib/editor/queryEditorTextEdits";
|
||||
import { buildSqlInConditionFromPasteSource, insertTextForSqlInCondition } from "@/lib/sql/sqlInListPaste";
|
||||
import { resolveSqlSingleQuoteKeyAction } from "@/lib/sql/sqlQuoteCaret";
|
||||
|
|
@ -167,6 +168,11 @@ let lastEmittedViewport: { scrollTop: number; scrollLeft: number } | undefined =
|
|||
let latestSelection: { anchor: number; head: number } | undefined = props.initialSelection;
|
||||
const connectionStore = useConnectionStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
function sqlStatementParameterOptions() {
|
||||
const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, props.databaseType);
|
||||
return { databaseType: props.databaseType, enabledSyntaxes: enabledSqlParameterSyntaxes(toggles) };
|
||||
}
|
||||
const { isDark, themePalette } = useTheme();
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
|
|
@ -620,11 +626,12 @@ function requestExecuteFromView(currentView: EditorViewType, cursorPos: number,
|
|||
}
|
||||
// No selection → resolve the execution target, optionally via the picker.
|
||||
const doc = currentView.state.doc.toString();
|
||||
const candidates = buildExecutionCandidates(doc, cursorPos, props.databaseType);
|
||||
const parameterOptions = sqlStatementParameterOptions();
|
||||
const candidates = buildExecutionCandidates(doc, cursorPos, props.databaseType, parameterOptions);
|
||||
if (candidates.length === 0) return false;
|
||||
// The execution shortcut keeps executing the configured target (cursor/all) directly:
|
||||
// it stays keyboard-driven and never pops the picker, which is reserved for click entry points.
|
||||
if (options.bypassPicker || !settingsStore.editorSettings.showExecutionTargetPicker || !hasMultipleExecutionTargets(doc, props.databaseType)) {
|
||||
if (options.bypassPicker || !settingsStore.editorSettings.showExecutionTargetPicker || !hasMultipleExecutionTargets(doc, props.databaseType, parameterOptions)) {
|
||||
const preferredKind = settingsStore.editorSettings.executeMode === "current" ? "cursor" : "all";
|
||||
const candidate = candidates.find((item) => item.kind === preferredKind) ?? candidates[0];
|
||||
emitExecutionRequest(candidate.sql, options.openInNewResultTab);
|
||||
|
|
@ -1231,13 +1238,13 @@ function contextObjectMenuItem(action: QueryContextObjectAction): ContextMenuIte
|
|||
}
|
||||
|
||||
function executableStatementRangeStartingAt(currentView: EditorViewType, lineFrom: number) {
|
||||
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType);
|
||||
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType, sqlStatementParameterOptions());
|
||||
return executableStatementRangeStartingAtLine(executableStatementRangeCache, lineFrom);
|
||||
}
|
||||
|
||||
function currentExecutableStatementRange(currentView: EditorViewType): SqlTextRange | null {
|
||||
if (!supportsExecutionTargetPicker(props.databaseType)) return null;
|
||||
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType);
|
||||
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType, sqlStatementParameterOptions());
|
||||
return executableStatementRangeAtCursor(executableStatementRangeCache, currentView.state.selection.main.head);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,25 @@ describe("executableStatementRangeCacheForDoc", () => {
|
|||
expect(executableStatementRangeStartingAt(cache, secondStatementLine.from)?.sql).toBe("SELECT *\nFROM menus AS mn\nLIMIT 100");
|
||||
});
|
||||
|
||||
it("keeps MyBatis parameters in a Kingbase gutter execution range", () => {
|
||||
const sql = ["SELECT sum(nvl(a.medfee_sumamt, 0)) AS medfee_sumamt, a.insutype", "FROM yd_org_decla_detail a", "WHERE a.busin_type = '1' AND a.clr_ym = #{ym}", "GROUP BY a.clr_ym, a.insutype;"].join("\n");
|
||||
const doc = Text.of(sql.split("\n"));
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "kingbase");
|
||||
|
||||
expect(executableStatementRangeStartingAt(cache, doc.line(1).from)?.sql).toBe(sql.slice(0, -1));
|
||||
});
|
||||
|
||||
it("keeps a valid placeholder-only line executable and respects disabled MyBatis syntax", () => {
|
||||
const sql = ["SELECT *", "FROM t", "#{where_clause};"].join("\n");
|
||||
const doc = Text.of(sql.split("\n"));
|
||||
const enabled = executableStatementRangeCacheForDoc(null, doc, "kingbase", { enabledSyntaxes: ["mybatis"] });
|
||||
const disabled = executableStatementRangeCacheForDoc(enabled, doc, "kingbase", { enabledSyntaxes: ["shell"] });
|
||||
|
||||
expect(executableStatementRangeAtCursor(enabled, doc.line(3).from + 2)?.sql).toBe(sql.slice(0, -1));
|
||||
expect(executableStatementRangeAtCursor(disabled, doc.line(3).from + 2)).toBeNull();
|
||||
expect(disabled).not.toBe(enabled);
|
||||
});
|
||||
|
||||
it("resolves statements with leading whitespace for gutter run buttons", () => {
|
||||
const doc = Text.of([" SELECT 1;", " SELECT 2;", "\t SELECT 3;", "", " "]);
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "mysql");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { extractSqlParameterDescriptors, extractSqlParameters, sqlParameterLiteral, substituteSqlParameters } from "@/lib/sql/sqlParameters";
|
||||
import { extractSqlParameterDescriptors, extractSqlParameters, readSqlBracedParameterAt, sqlParameterLiteral, substituteSqlParameters } from "@/lib/sql/sqlParameters";
|
||||
|
||||
describe("extractSqlParameters", () => {
|
||||
it("shares strict braced-placeholder validation", () => {
|
||||
expect(readSqlBracedParameterAt("#{month}", 0)?.name).toBe("month");
|
||||
expect(readSqlBracedParameterAt("#{1month}", 0)).toBeNull();
|
||||
expect(readSqlBracedParameterAt("#{month", 0)).toBeNull();
|
||||
expect(readSqlBracedParameterAt("#{month}", 0, { enabledSyntaxes: ["shell"] })).toBeNull();
|
||||
});
|
||||
|
||||
it("extracts unique template parameters in order", () => {
|
||||
const sql = "select * from t where pt_dt between ${start_date} and ${end_date} or pt_dt = ${start_date}";
|
||||
expect(extractSqlParameters(sql)).toEqual(["start_date", "end_date"]);
|
||||
|
|
|
|||
|
|
@ -281,6 +281,16 @@ describe("splitSqlStatementRanges", () => {
|
|||
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT 1", "SELECT 2"]);
|
||||
});
|
||||
|
||||
it("keeps MyBatis placeholders instead of treating them as hash comments", () => {
|
||||
const sql = "SELECT * FROM yd_org_decla_detail WHERE clr_ym = #{ym};\nSELECT 2";
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "kingbase"))).toEqual(["SELECT * FROM yd_org_decla_detail WHERE clr_ym = #{ym}", "SELECT 2"]);
|
||||
});
|
||||
|
||||
it("treats malformed or disabled MyBatis prefixes as hash comments", () => {
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges("SELECT 1; #{1ym};\nSELECT 2", "kingbase"))).toEqual(["SELECT 1", "SELECT 2"]);
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges("SELECT 1; #{ym};\nSELECT 2", "kingbase", { enabledSyntaxes: ["shell"] }))).toEqual(["SELECT 1", "SELECT 2"]);
|
||||
});
|
||||
|
||||
it("ignores semicolons in block comments", () => {
|
||||
const sql = "SELECT /* a; b */ 1;\nSELECT 2";
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT /* a; b */ 1", "SELECT 2"]);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,35 @@
|
|||
import type { Text } from "@codemirror/state";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { readSqlBracedParameterAt, type SqlParameterOptions } from "@/lib/sql/sqlParameters";
|
||||
import { executableStatementRanges, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
|
||||
export interface ExecutableStatementRangeCache {
|
||||
doc: Text;
|
||||
databaseType?: DatabaseType;
|
||||
parameterOptions?: SqlParameterOptions;
|
||||
parameterSyntaxKey: string;
|
||||
byStart: Map<number, SqlTextRange>;
|
||||
byExecutableLineStart: Map<number, SqlTextRange>;
|
||||
ranges: SqlTextRange[];
|
||||
}
|
||||
|
||||
export type ExecutableStatementRangeParser = (sql: string, databaseType?: DatabaseType) => SqlTextRange[];
|
||||
export type ExecutableStatementRangeParser = (sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions) => SqlTextRange[];
|
||||
|
||||
export function executableStatementRangeCacheForDoc(cache: ExecutableStatementRangeCache | null, doc: Text, databaseType?: DatabaseType, parse: ExecutableStatementRangeParser = executableStatementRanges): ExecutableStatementRangeCache {
|
||||
if (cache?.doc === doc && cache.databaseType === databaseType) return cache;
|
||||
export function executableStatementRangeCacheForDoc(
|
||||
cache: ExecutableStatementRangeCache | null,
|
||||
doc: Text,
|
||||
databaseType?: DatabaseType,
|
||||
parameterOptionsOrParse?: SqlParameterOptions | ExecutableStatementRangeParser,
|
||||
customParse: ExecutableStatementRangeParser = executableStatementRanges,
|
||||
): ExecutableStatementRangeCache {
|
||||
const parameterOptions = typeof parameterOptionsOrParse === "function" ? undefined : parameterOptionsOrParse;
|
||||
const parse = typeof parameterOptionsOrParse === "function" ? parameterOptionsOrParse : customParse;
|
||||
const parameterSyntaxKey = parameterOptions?.enabledSyntaxes ? parameterOptions.enabledSyntaxes.join(",") : "*";
|
||||
if (cache?.doc === doc && cache.databaseType === databaseType && cache.parameterSyntaxKey === parameterSyntaxKey) return cache;
|
||||
|
||||
const byStart = new Map<number, SqlTextRange>();
|
||||
const byExecutableLineStart = new Map<number, SqlTextRange>();
|
||||
const ranges = parse(doc.toString(), databaseType);
|
||||
const ranges = parse(doc.toString(), databaseType, parameterOptions);
|
||||
for (const range of ranges) {
|
||||
byStart.set(range.from, range);
|
||||
const line = doc.lineAt(range.from);
|
||||
|
|
@ -25,7 +37,7 @@ export function executableStatementRangeCacheForDoc(cache: ExecutableStatementRa
|
|||
byExecutableLineStart.set(line.from, range);
|
||||
}
|
||||
}
|
||||
return { doc, databaseType, byStart, byExecutableLineStart, ranges };
|
||||
return { doc, databaseType, parameterOptions, parameterSyntaxKey, byStart, byExecutableLineStart, ranges };
|
||||
}
|
||||
|
||||
export function executableStatementRangeStartingAt(cache: ExecutableStatementRangeCache, lineFrom: number): SqlTextRange | null {
|
||||
|
|
@ -36,7 +48,9 @@ export function executableStatementRangeAtCursor(cache: ExecutableStatementRange
|
|||
const pos = Math.max(0, Math.min(cursorPos, cache.doc.length));
|
||||
const line = cache.doc.lineAt(pos);
|
||||
const lineText = line.text.trim();
|
||||
if (!lineText || lineText.startsWith("--") || lineText.startsWith("#") || isCursorOnLeadingBlockComment(line.text, pos - line.from)) return null;
|
||||
const lineContentStart = line.from + line.text.search(/\S|$/);
|
||||
const startsHashComment = lineText.startsWith("#") && readSqlBracedParameterAt(cache.doc.toString(), lineContentStart, cache.parameterOptions)?.syntax !== "mybatis";
|
||||
if (!lineText || lineText.startsWith("--") || startsHashComment || isCursorOnLeadingBlockComment(line.text, pos - line.from)) return null;
|
||||
|
||||
for (let index = 0; index < cache.ranges.length; index += 1) {
|
||||
const range = cache.ranges[index];
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ export interface SqlParameterDescriptor {
|
|||
token: string;
|
||||
}
|
||||
|
||||
export interface SqlBracedParameter extends SqlParameterDescriptor {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
interface ParameterOccurrence extends SqlParameterDescriptor {
|
||||
start: number;
|
||||
end: number;
|
||||
|
|
@ -36,6 +41,19 @@ const PARAMETER_NAME_START_RE = /[\p{L}_]/u;
|
|||
const PARAMETER_NAME_CHAR_RE = /[\p{L}\p{N}_]/u;
|
||||
const SQL_SERVER_TEMP_TABLE_CONTEXT_KEYWORDS = new Set(["table", "from", "join", "into", "update", "truncate"]);
|
||||
|
||||
export function readSqlBracedParameterAt(sql: string, start: number, options?: SqlParameterOptions): SqlBracedParameter | null {
|
||||
const open = sql.slice(start, start + 2);
|
||||
const syntax: SqlParameterSyntax | null = open === "${" ? "shell" : open === "#{" ? "mybatis" : null;
|
||||
if (!syntax || (options?.enabledSyntaxes && !options.enabledSyntaxes.includes(syntax))) return null;
|
||||
|
||||
const closeBrace = sql.indexOf("}", start + 2);
|
||||
if (closeBrace === -1) return null;
|
||||
const name = sql.slice(start + 2, closeBrace).trim();
|
||||
if (!PARAMETER_NAME_RE.test(name)) return null;
|
||||
|
||||
return { key: name, name, syntax, token: sql.slice(start, closeBrace + 1), start, end: closeBrace + 1 };
|
||||
}
|
||||
|
||||
export function extractSqlParameters(sql: string, options?: SqlParameterOptions): string[] {
|
||||
return extractSqlParameterDescriptors(sql, options).map((descriptor) => descriptor.key);
|
||||
}
|
||||
|
|
@ -163,26 +181,12 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
|
|||
continue;
|
||||
}
|
||||
}
|
||||
if (ch === "$" && next === "{" && isSyntaxEnabled("shell")) {
|
||||
const end = sql.indexOf("}", i + 2);
|
||||
if (end !== -1) {
|
||||
const name = sql.slice(i + 2, end).trim();
|
||||
if (PARAMETER_NAME_RE.test(name)) {
|
||||
occurrences.push({ key: name, name, syntax: "shell", token: sql.slice(i, end + 1), start: i, end: end + 1 });
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ch === "#" && next === "{" && isSyntaxEnabled("mybatis")) {
|
||||
const end = sql.indexOf("}", i + 2);
|
||||
if (end !== -1) {
|
||||
const name = sql.slice(i + 2, end).trim();
|
||||
if (PARAMETER_NAME_RE.test(name)) {
|
||||
occurrences.push({ key: name, name, syntax: "mybatis", token: sql.slice(i, end + 1), start: i, end: end + 1 });
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
if ((ch === "$" || ch === "#") && next === "{") {
|
||||
const parameter = readSqlBracedParameterAt(sql, i, options);
|
||||
if (parameter) {
|
||||
occurrences.push(parameter);
|
||||
i = parameter.end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (isHashLineComment(sql, i)) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { SqlExecutionCandidate } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
|
||||
import { readSqlBracedParameterAt, type SqlParameterOptions } from "@/lib/sql/sqlParameters";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
/**
|
||||
|
|
@ -18,11 +19,11 @@ export function supportsExecutionTargetPicker(databaseType?: DatabaseType): bool
|
|||
return !!databaseType && (databaseType === "redis" || databaseType === "elasticsearch" || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType));
|
||||
}
|
||||
|
||||
export function hasMultipleExecutionTargets(sql: string, databaseType?: DatabaseType): boolean {
|
||||
export function hasMultipleExecutionTargets(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): boolean {
|
||||
if (databaseType === "redis") {
|
||||
return redisExecutableCommandCount(sql) > 1;
|
||||
}
|
||||
return splitSqlStatementRanges(sql, databaseType).length > 1;
|
||||
return splitSqlStatementRanges(sql, databaseType, parameterOptions).length > 1;
|
||||
}
|
||||
|
||||
interface RawStatement {
|
||||
|
|
@ -272,7 +273,7 @@ const SAP_HANA_SCRIPT_BLOCK_TERMINATORS = new Set(["IF", "FOR", "WHILE"]);
|
|||
* only the statement text (the trailing semicolon and inter-statement
|
||||
* whitespace are excluded so editor highlights stay tight).
|
||||
*/
|
||||
export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType): RawStatement[] {
|
||||
export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): RawStatement[] {
|
||||
if (databaseType === "elasticsearch") {
|
||||
const requests = splitElasticsearchRestRequestRanges(sql);
|
||||
if (requests) return requests;
|
||||
|
|
@ -423,7 +424,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
|||
i = newline === -1 ? len : newline + 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "#") {
|
||||
if (startsHashLineComment(sql, i, parameterOptions)) {
|
||||
const newline = sql.indexOf("\n", i);
|
||||
i = newline === -1 ? len : newline + 1;
|
||||
continue;
|
||||
|
|
@ -491,9 +492,9 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
|||
continue;
|
||||
}
|
||||
} else if (ch === ";") {
|
||||
const isMysqlRoutineBlock = isMysqlRoutineBlockDatabase(databaseType) && statementStart !== -1 && startsWithMysqlRoutineBlock(sql.slice(statementStart, i));
|
||||
const isMysqlRoutineBlock = isMysqlRoutineBlockDatabase(databaseType) && statementStart !== -1 && startsWithMysqlRoutineBlock(sql.slice(statementStart, i), parameterOptions);
|
||||
if (isMysqlRoutineBlock) {
|
||||
if (!mysqlRoutineBlockIsComplete(sql.slice(statementStart, i + 1))) {
|
||||
if (!mysqlRoutineBlockIsComplete(sql.slice(statementStart, i + 1), parameterOptions)) {
|
||||
markContent(i);
|
||||
i += 1;
|
||||
continue;
|
||||
|
|
@ -544,14 +545,14 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
|||
* The returned range covers only the statement's own text (no trailing `;`),
|
||||
* which lets the editor highlight a tight preview range.
|
||||
*/
|
||||
export function statementRangeAtCursor(sql: string, cursorPos: number, databaseType?: DatabaseType): SqlTextRange | null {
|
||||
export function statementRangeAtCursor(sql: string, cursorPos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): SqlTextRange | null {
|
||||
const pos = clampCursor(sql, cursorPos);
|
||||
if (isCursorOnBlankLine(sql, pos)) return null;
|
||||
|
||||
const statements = splitSqlStatementRanges(sql, databaseType);
|
||||
const statements = splitSqlStatementRanges(sql, databaseType, parameterOptions);
|
||||
for (let index = 0; index < statements.length; index += 1) {
|
||||
const statement = statements[index];
|
||||
const softRanges = splitStatementRangeAtSoftStarts(sql, statement, databaseType);
|
||||
const softRanges = splitStatementRangeAtSoftStarts(sql, statement, databaseType, parameterOptions);
|
||||
// Cursor inside the statement body, including the exact start/end.
|
||||
if (pos >= statement.from && pos <= statement.to) {
|
||||
return rangeForCursorInSoftRanges(sql, softRanges, pos) ?? rangeFor(statement, sql);
|
||||
|
|
@ -569,7 +570,7 @@ export function statementRangeAtCursor(sql: string, cursorPos: number, databaseT
|
|||
if (pos >= statement.hitFrom && pos < statement.from && (sql.slice(pos, statement.from).trim() === "" || (databaseType === "elasticsearch" && isElasticsearchRequestPreamble(sql.slice(statement.hitFrom, statement.from))))) {
|
||||
const previous = statements[index - 1];
|
||||
if (previous && isCursorInSameLineDelimiterGap(sql, previous.to, pos)) {
|
||||
const previousSoftRanges = splitStatementRangeAtSoftStarts(sql, previous, databaseType);
|
||||
const previousSoftRanges = splitStatementRangeAtSoftStarts(sql, previous, databaseType, parameterOptions);
|
||||
return rangeForCursorInSoftRanges(sql, previousSoftRanges, pos) ?? rangeFor(previous, sql);
|
||||
}
|
||||
return rangeForCursorInSoftRanges(sql, softRanges, pos) ?? rangeFor(statement, sql);
|
||||
|
|
@ -631,13 +632,13 @@ function rangeForCursorInSoftRanges(sql: string, ranges: RawStatement[], pos: nu
|
|||
return null;
|
||||
}
|
||||
|
||||
function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, databaseType?: DatabaseType): RawStatement[] {
|
||||
function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): RawStatement[] {
|
||||
if (isOraclePlSqlStatement(statement.sql, databaseType)) return [statement];
|
||||
if (isSapHanaScriptBlockStatement(statement.sql, databaseType)) return [statement];
|
||||
// Routine bodies contain top-level-looking SET/INSERT/SELECT lines that are not independent statements.
|
||||
if (isMysqlRoutineBlockDatabase(databaseType) && startsWithMysqlRoutineBlock(statement.sql)) return [statement];
|
||||
if (isMysqlRoutineBlockDatabase(databaseType) && startsWithMysqlRoutineBlock(statement.sql, parameterOptions)) return [statement];
|
||||
|
||||
const lineStarts = topLevelSoftStatementLineStarts(sql, statement, databaseType);
|
||||
const lineStarts = topLevelSoftStatementLineStarts(sql, statement, databaseType, parameterOptions);
|
||||
if (lineStarts.length <= 1) return [statement];
|
||||
|
||||
const boundaries: Array<{ hitFrom: number; from: number; keyword: string }> = [];
|
||||
|
|
@ -661,7 +662,7 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d
|
|||
continue;
|
||||
}
|
||||
|
||||
if (isSetOperationQueryContinuation(sql, statement.from, lineStart.from, lineStart.keyword)) {
|
||||
if (isSetOperationQueryContinuation(sql, statement.from, lineStart.from, lineStart.keyword, parameterOptions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -719,7 +720,7 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d
|
|||
for (let index = 0; index < boundaries.length; index += 1) {
|
||||
const boundary = boundaries[index];
|
||||
const next = boundaries[index + 1];
|
||||
const to = next ? trimRangeEndBeforeNextBoundary(sql, boundary.from, next.from) : trimRangeEnd(sql, boundary.from, statement.to);
|
||||
const to = next ? trimRangeEndBeforeNextBoundary(sql, boundary.from, next.from, parameterOptions) : trimRangeEnd(sql, boundary.from, statement.to);
|
||||
if (to > boundary.from) {
|
||||
ranges.push({
|
||||
hitFrom: boundary.hitFrom,
|
||||
|
|
@ -733,7 +734,7 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d
|
|||
return ranges.length > 0 ? ranges : [statement];
|
||||
}
|
||||
|
||||
function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, databaseType?: DatabaseType): Array<{ hitFrom: number; from: number; keyword: string }> {
|
||||
function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): Array<{ hitFrom: number; from: number; keyword: string }> {
|
||||
const starts: Array<{ hitFrom: number; from: number; keyword: string }> = [];
|
||||
const len = statement.to;
|
||||
const explainOptionsStart = explainOptionsParenAt(sql, statement.from);
|
||||
|
|
@ -751,7 +752,7 @@ function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, d
|
|||
const ch = sql[i];
|
||||
const next = sql[i + 1] ?? "";
|
||||
|
||||
if (state === "none" && firstNonWhitespaceOnLine === -1 && ch !== "\n" && ch !== "\r" && !isSqlWhitespace(ch) && !startsLineComment(sql, i) && !startsBlockComment(sql, i)) {
|
||||
if (state === "none" && firstNonWhitespaceOnLine === -1 && ch !== "\n" && ch !== "\r" && !isSqlWhitespace(ch) && !startsLineComment(sql, i, parameterOptions) && !startsBlockComment(sql, i)) {
|
||||
firstNonWhitespaceOnLine = i;
|
||||
if (parenDepth === 0) {
|
||||
const keyword = softStatementKeywordAt(sql, i, databaseType);
|
||||
|
|
@ -850,7 +851,7 @@ function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, d
|
|||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === "#") {
|
||||
if (startsHashLineComment(sql, i, parameterOptions)) {
|
||||
state = "lineComment";
|
||||
i += 1;
|
||||
continue;
|
||||
|
|
@ -915,9 +916,9 @@ function softStatementStartKeywords(databaseType?: DatabaseType): Set<string> {
|
|||
return new Set([...COMMON_SOFT_STATEMENT_START_KEYWORDS, ...(databaseType ? (DATABASE_SOFT_STATEMENT_KEYWORDS[databaseType] ?? []) : [])]);
|
||||
}
|
||||
|
||||
function isSetOperationQueryContinuation(sql: string, from: number, to: number, keyword: string): boolean {
|
||||
function isSetOperationQueryContinuation(sql: string, from: number, to: number, keyword: string, parameterOptions?: SqlParameterOptions): boolean {
|
||||
if (keyword !== "SELECT" && keyword !== "WITH") return false;
|
||||
const words = topLevelWordsBefore(sql, from, to, 3);
|
||||
const words = topLevelWordsBefore(sql, from, to, 3, parameterOptions);
|
||||
const last = words[words.length - 1];
|
||||
if (last && SET_OPERATION_KEYWORDS.has(last)) return true;
|
||||
if (last && SET_OPERATION_MODIFIER_KEYWORDS.has(last)) {
|
||||
|
|
@ -952,7 +953,7 @@ function startsWithMysqlCreateTable(sql: string, statementFrom: number): boolean
|
|||
return /^CREATE\s+(?:TEMPORARY\s+)?TABLE\b/i.test(text);
|
||||
}
|
||||
|
||||
function topLevelWordsBefore(sql: string, from: number, to: number, limit: number): string[] {
|
||||
function topLevelWordsBefore(sql: string, from: number, to: number, limit: number, parameterOptions?: SqlParameterOptions): string[] {
|
||||
const words: string[] = [];
|
||||
let state: QuoteState | "lineComment" | "blockComment" = "none";
|
||||
let dollarTag = "";
|
||||
|
|
@ -1044,7 +1045,7 @@ function topLevelWordsBefore(sql: string, from: number, to: number, limit: numbe
|
|||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === "#") {
|
||||
if (startsHashLineComment(sql, i, parameterOptions)) {
|
||||
state = "lineComment";
|
||||
i += 1;
|
||||
continue;
|
||||
|
|
@ -1267,8 +1268,13 @@ function skipBalancedParens(sql: string, pos: number): number | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function startsLineComment(sql: string, pos: number): boolean {
|
||||
return (sql[pos] === "-" && sql[pos + 1] === "-") || sql[pos] === "#";
|
||||
function startsLineComment(sql: string, pos: number, parameterOptions?: SqlParameterOptions): boolean {
|
||||
return (sql[pos] === "-" && sql[pos + 1] === "-") || startsHashLineComment(sql, pos, parameterOptions);
|
||||
}
|
||||
|
||||
function startsHashLineComment(sql: string, pos: number, parameterOptions?: SqlParameterOptions): boolean {
|
||||
if (sql[pos] !== "#") return false;
|
||||
return readSqlBracedParameterAt(sql, pos, parameterOptions)?.syntax !== "mybatis";
|
||||
}
|
||||
|
||||
function startsBlockComment(sql: string, pos: number): boolean {
|
||||
|
|
@ -1283,7 +1289,7 @@ function trimRangeEnd(sql: string, from: number, to: number): number {
|
|||
return end;
|
||||
}
|
||||
|
||||
function trimRangeEndBeforeNextBoundary(sql: string, from: number, nextBoundaryFrom: number): number {
|
||||
function trimRangeEndBeforeNextBoundary(sql: string, from: number, nextBoundaryFrom: number, parameterOptions?: SqlParameterOptions): number {
|
||||
let state: QuoteState | "lineComment" | "blockComment" = "none";
|
||||
let dollarTag = "";
|
||||
let lastContentEnd = from;
|
||||
|
|
@ -1384,7 +1390,7 @@ function trimRangeEndBeforeNextBoundary(sql: string, from: number, nextBoundaryF
|
|||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === "#") {
|
||||
if (startsHashLineComment(sql, i, parameterOptions)) {
|
||||
state = "lineComment";
|
||||
i += 1;
|
||||
continue;
|
||||
|
|
@ -1462,12 +1468,12 @@ function isMysqlRoutineBlockDatabase(databaseType?: DatabaseType): boolean {
|
|||
return !!databaseType && MYSQL_ROUTINE_BLOCK_DATABASES.has(databaseType);
|
||||
}
|
||||
|
||||
function startsWithMysqlRoutineBlock(sql: string): boolean {
|
||||
return isMysqlRoutineDdlStart(sql) && mysqlRoutineTokens(sql).some((token) => token.kind === "word" && token.value === "BEGIN");
|
||||
function startsWithMysqlRoutineBlock(sql: string, parameterOptions?: SqlParameterOptions): boolean {
|
||||
return isMysqlRoutineDdlStart(sql, parameterOptions) && mysqlRoutineTokens(sql, parameterOptions).some((token) => token.kind === "word" && token.value === "BEGIN");
|
||||
}
|
||||
|
||||
function isMysqlRoutineDdlStart(sql: string): boolean {
|
||||
const words = mysqlRoutineWords(sql).slice(0, 16);
|
||||
function isMysqlRoutineDdlStart(sql: string, parameterOptions?: SqlParameterOptions): boolean {
|
||||
const words = mysqlRoutineWords(sql, parameterOptions).slice(0, 16);
|
||||
if (words[0] !== "CREATE") return false;
|
||||
|
||||
for (const word of words.slice(1)) {
|
||||
|
|
@ -1477,10 +1483,10 @@ function isMysqlRoutineDdlStart(sql: string): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
function mysqlRoutineBlockIsComplete(sql: string): boolean {
|
||||
if (!startsWithMysqlRoutineBlock(sql)) return false;
|
||||
function mysqlRoutineBlockIsComplete(sql: string, parameterOptions?: SqlParameterOptions): boolean {
|
||||
if (!startsWithMysqlRoutineBlock(sql, parameterOptions)) return false;
|
||||
|
||||
const tokens = mysqlRoutineTokens(sql);
|
||||
const tokens = mysqlRoutineTokens(sql, parameterOptions);
|
||||
let beginDepth = 0;
|
||||
let sawBegin = false;
|
||||
|
||||
|
|
@ -1502,13 +1508,13 @@ function mysqlRoutineBlockIsComplete(sql: string): boolean {
|
|||
return sawBegin && beginDepth === 0 && tokens[tokens.length - 1]?.kind === "semicolon";
|
||||
}
|
||||
|
||||
function mysqlRoutineWords(sql: string): string[] {
|
||||
return mysqlRoutineTokens(sql)
|
||||
function mysqlRoutineWords(sql: string, parameterOptions?: SqlParameterOptions): string[] {
|
||||
return mysqlRoutineTokens(sql, parameterOptions)
|
||||
.filter((token): token is { kind: "word"; value: string } => token.kind === "word")
|
||||
.map((token) => token.value);
|
||||
}
|
||||
|
||||
function mysqlRoutineTokens(sql: string): Array<{ kind: "word" | "semicolon"; value: string }> {
|
||||
function mysqlRoutineTokens(sql: string, parameterOptions?: SqlParameterOptions): Array<{ kind: "word" | "semicolon"; value: string }> {
|
||||
const tokens: Array<{ kind: "word" | "semicolon"; value: string }> = [];
|
||||
let state: QuoteState | "lineComment" | "blockComment" = "none";
|
||||
let i = 0;
|
||||
|
|
@ -1572,7 +1578,7 @@ function mysqlRoutineTokens(sql: string): Array<{ kind: "word" | "semicolon"; va
|
|||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === "#") {
|
||||
if (startsHashLineComment(sql, i, parameterOptions)) {
|
||||
state = "lineComment";
|
||||
i += 1;
|
||||
continue;
|
||||
|
|
@ -1975,21 +1981,21 @@ function normalizeSql(sql: string): string {
|
|||
* cursor statement and the full document are effectively the same SQL — in
|
||||
* that case only a single candidate is returned to avoid duplicates.
|
||||
*/
|
||||
export function executableStatementRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] {
|
||||
export function executableStatementRanges(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): SqlTextRange[] {
|
||||
if (databaseType === "redis") return redisExecutableCommandRanges(sql);
|
||||
if (databaseType === "mongodb") return splitMongoCommandRanges(sql).map(({ from, to, text }) => ({ from, to, sql: text }));
|
||||
return splitSqlStatementRanges(sql, databaseType).flatMap((statement) => splitStatementRangeAtSoftStarts(sql, statement, databaseType).map((range) => rangeFor(range, sql)));
|
||||
return splitSqlStatementRanges(sql, databaseType, parameterOptions).flatMap((statement) => splitStatementRangeAtSoftStarts(sql, statement, databaseType, parameterOptions).map((range) => rangeFor(range, sql)));
|
||||
}
|
||||
|
||||
export function currentExecutableStatementRange(sql: string, cursorPos: number, databaseType?: DatabaseType): SqlTextRange | null {
|
||||
export function currentExecutableStatementRange(sql: string, cursorPos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): SqlTextRange | null {
|
||||
if (databaseType === "redis") return redisCommandRangeAtCursor(sql, cursorPos);
|
||||
if (databaseType === "mongodb") return null;
|
||||
return statementRangeAtCursor(sql, cursorPos, databaseType);
|
||||
return statementRangeAtCursor(sql, cursorPos, databaseType, parameterOptions);
|
||||
}
|
||||
|
||||
export function buildExecutionCandidates(sql: string, cursorPos: number, databaseType?: DatabaseType): SqlExecutionCandidate[] {
|
||||
export function buildExecutionCandidates(sql: string, cursorPos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): SqlExecutionCandidate[] {
|
||||
const full = fullSqlRange(sql);
|
||||
const cursorStatement = currentExecutableStatementRange(sql, cursorPos, databaseType);
|
||||
const cursorStatement = currentExecutableStatementRange(sql, cursorPos, databaseType, parameterOptions);
|
||||
|
||||
if (!full && !cursorStatement) return [];
|
||||
if (!full) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue