fix(editor): validate MyBatis statement placeholders
This commit is contained in:
7 files changed
+141
-74
No files matched your search
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user