fix(mysql): strip CLI \G suffix before executing queries
This commit is contained in:
5 files changed
+82
-8
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { executionCandidateForMode } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { buildExecutionCandidates, currentExecutableStatementRange, executableStatementRanges, fullSqlRange, hasMultipleExecutionTargets, splitSqlStatementRanges, statementRangeAtCursor, supportsExecutionTargetPicker } from "@/lib/sql/sqlStatementRanges";
|
||||
import { buildExecutionCandidates, currentExecutableStatementRange, executableStatementRanges, fullSqlRange, hasMultipleExecutionTargets, splitSqlStatementRanges, statementRangeAtCursor, stripMysqlClientDisplayCommand, supportsExecutionTargetPicker } from "@/lib/sql/sqlStatementRanges";
|
||||
|
||||
function indexOf(sql: string, needle: string, occurrence = 1): number {
|
||||
let from = 0;
|
||||
@@ -377,6 +377,25 @@ BEGIN
|
||||
END;
|
||||
SELECT 3 FROM DUMMY;`;
|
||||
|
||||
describe("stripMysqlClientDisplayCommand", () => {
|
||||
it("removes a trailing MySQL CLI vertical-output command", () => {
|
||||
expect(stripMysqlClientDisplayCommand("SHOW CREATE FUNCTION fun_grade \\G")).toBe("SHOW CREATE FUNCTION fun_grade");
|
||||
expect(stripMysqlClientDisplayCommand("SHOW CREATE FUNCTION fun_grade \\G;")).toBe("SHOW CREATE FUNCTION fun_grade;");
|
||||
expect(stripMysqlClientDisplayCommand("SELECT 1\\G\n")).toBe("SELECT 1\n");
|
||||
});
|
||||
|
||||
it("removes the lowercase terminator and a command following the semicolon", () => {
|
||||
expect(stripMysqlClientDisplayCommand("SHOW STATUS \\g")).toBe("SHOW STATUS");
|
||||
expect(stripMysqlClientDisplayCommand("SELECT 1; \\G")).toBe("SELECT 1;");
|
||||
});
|
||||
|
||||
it("does not rewrite ordinary SQL or a command inside a line comment", () => {
|
||||
expect(stripMysqlClientDisplayCommand("SELECT '\\G'")).toBe("SELECT '\\G'");
|
||||
expect(stripMysqlClientDisplayCommand("SELECT 1 -- keep \\G")).toBe("SELECT 1 -- keep \\G");
|
||||
expect(stripMysqlClientDisplayCommand("SELECT 1 # keep \\G")).toBe("SELECT 1 # keep \\G");
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitSqlStatementRanges", () => {
|
||||
it("splits multiple top-level statements", () => {
|
||||
const sql = "SELECT 1;\nSELECT 2;\nSELECT 3;";
|
||||
|
||||
@@ -32,6 +32,22 @@ export function supportsExecutionTargetPicker(databaseType?: DatabaseType): bool
|
||||
return !!databaseType && (databaseType === "redis" || isHttpJsonRestDatabaseType(databaseType) || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType));
|
||||
}
|
||||
|
||||
/** Remove the MySQL CLI's trailing vertical-output command before execution. */
|
||||
export function stripMysqlClientDisplayCommand(sql: string): string {
|
||||
const trimmed = sql.trimEnd();
|
||||
const hasTrailingSemicolon = trimmed.endsWith(";");
|
||||
const withoutTrailingSemicolon = hasTrailingSemicolon ? trimmed.slice(0, -1).trimEnd() : trimmed;
|
||||
if (!withoutTrailingSemicolon.endsWith("\\G") && !withoutTrailingSemicolon.endsWith("\\g")) return sql;
|
||||
|
||||
const markerStart = withoutTrailingSemicolon.length - 2;
|
||||
const lineStart = withoutTrailingSemicolon.lastIndexOf("\n", markerStart - 1) + 1;
|
||||
const linePrefix = withoutTrailingSemicolon.slice(lineStart, markerStart);
|
||||
if (linePrefix.includes("--") || linePrefix.includes("#")) return sql;
|
||||
|
||||
const executableSql = withoutTrailingSemicolon.slice(0, markerStart).trimEnd();
|
||||
return `${executableSql}${hasTrailingSemicolon ? ";" : ""}${sql.slice(trimmed.length)}`;
|
||||
}
|
||||
|
||||
export function hasMultipleExecutionTargets(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): boolean {
|
||||
if (databaseType === "redis") {
|
||||
return redisExecutableCommandCount(sql) > 1;
|
||||
|
||||
@@ -106,6 +106,14 @@ describe("queryStore MySQL grouped-result editing", () => {
|
||||
expect(listIndexes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("strips the MySQL CLI vertical-output suffix only from the submitted SQL", async () => {
|
||||
const sourceSql = "SHOW CREATE FUNCTION fun_grade \\G;";
|
||||
const tab = await executeGrouped(sourceSql, ["Create Function"]);
|
||||
|
||||
expect(executeMulti.mock.calls[0]?.[2]).toBe("SHOW CREATE FUNCTION fun_grade;");
|
||||
expect(tab.lastExecutedSql).toBe(sourceSql);
|
||||
});
|
||||
|
||||
it("identifies the FROM root independently of metadata completion order", async () => {
|
||||
getColumns.mockImplementation(async (_connectionId: string, _database: string, _schema: string, table: string) => {
|
||||
if (table === "users") await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
@@ -132,4 +132,29 @@ describe("queryStore query result export", () => {
|
||||
|
||||
expect(request?.useAgentCursor).toBe(true);
|
||||
});
|
||||
|
||||
it("strips the MySQL CLI vertical-output suffix from export re-execution SQL", async () => {
|
||||
mocks.getConfig.mockReturnValue({ id: "mysql-1", name: "MySQL", db_type: "mysql", database: "app", query_timeout_secs: 30 });
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "app", "Query");
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
tab.sql = "SHOW CREATE FUNCTION fun_grade \\G";
|
||||
tab.lastExecutedSql = tab.sql;
|
||||
tab.result = {
|
||||
columns: ["Create Function"],
|
||||
rows: [["definition"]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
};
|
||||
|
||||
const request = await store.buildQueryResultExportRequest(tabId, {
|
||||
exportId: "export-g",
|
||||
filePath: "fun.csv",
|
||||
format: "csv",
|
||||
});
|
||||
|
||||
expect(request?.sql).toBe("SHOW CREATE FUNCTION fun_grade");
|
||||
expect(request?.queryBaseSql).toBe("SHOW CREATE FUNCTION fun_grade");
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ import { appendLargeValueCells, canUseTableDataLargeValuePreview, remapLargeValu
|
||||
import { simpleDataGridOrderByReferencesMissingColumn, sortDataGridRowIndexes, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort";
|
||||
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { agentProtocolQueryResultMaxRows, capQueryResultTotal, effectiveQueryResultMaxRows, limitQueryPagination, queryResultLimitReached } from "@/lib/dataGrid/queryResultRowLimit";
|
||||
import { elasticsearchRestRequestRanges, executableStatementRanges, splitSqlStatementRanges, sqlStatementParameterOptionsForCompatibility } from "@/lib/sql/sqlStatementRanges";
|
||||
import { elasticsearchRestRequestRanges, executableStatementRanges, splitSqlStatementRanges, sqlStatementParameterOptionsForCompatibility, stripMysqlClientDisplayCommand } from "@/lib/sql/sqlStatementRanges";
|
||||
import type { SqlParameterOptions } from "@/lib/sql/sqlParameters";
|
||||
import { replaceSqlServerLeadingUseQuery, sqlServerLeadingUseScript, sqlServerUseDatabaseFromStatement } from "@/lib/sql/sqlCompletionLookupTarget";
|
||||
import { classifySqlRisk } from "@/lib/sql/sqlRisk";
|
||||
@@ -6241,6 +6241,9 @@ export const useQueryStore = defineStore("query", () => {
|
||||
mongoCommands = splitMongoCommandRanges(sql);
|
||||
}
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
|
||||
if (effectiveDbType === "mysql") {
|
||||
sqlToExecute = stripMysqlClientDisplayCommand(sqlToExecute);
|
||||
}
|
||||
if (tab.autoCommit === false && !supportsTransaction(conn?.db_type)) {
|
||||
tab.autoCommit = true;
|
||||
}
|
||||
@@ -8291,12 +8294,13 @@ export const useQueryStore = defineStore("query", () => {
|
||||
await connStore.ensureConnected(location.connectionId);
|
||||
const conn = connStore.getConfig(location.connectionId);
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
|
||||
const executableSql = effectiveDbType === "mysql" ? stripMysqlClientDisplayCommand(sql) : sql;
|
||||
const executionDatabase = dataTabExecutionDatabase(conn, location.database, location.catalog);
|
||||
// main 引入全局查询超时:queryTimeoutSecsForConnection 现需传入全局默认值;
|
||||
// settingsStore 取 defineStore 顶层声明的实例(本函数无局部覆盖)。
|
||||
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs);
|
||||
const useAgentCursor = usesAgentCursorForQuery(conn?.db_type, conn?.driver_profile);
|
||||
const queryBaseSql = queryResultBaseSql(tab);
|
||||
const queryBaseSql = effectiveDbType === "mysql" ? stripMysqlClientDisplayCommand(queryResultBaseSql(tab)) : queryResultBaseSql(tab);
|
||||
const exportSettings = useSettingsStore().editorSettings;
|
||||
const exportRowLimit = exportSettings.exportRowLimitEnabled ? exportSettings.exportRowLimit : Number.POSITIVE_INFINITY;
|
||||
|
||||
@@ -8375,7 +8379,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const remaining = exportRowLimit - rows.length;
|
||||
const effectivePageLimit = Math.min(pageLimit, remaining);
|
||||
const plan = await api.prepareQueryPaginationExecutionPlan({
|
||||
sql,
|
||||
sql: executableSql,
|
||||
queryBaseSql,
|
||||
databaseType: effectiveDbType,
|
||||
pagination: { limit: effectivePageLimit, offset, sessionId },
|
||||
@@ -8437,7 +8441,8 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (!effectiveDbType) return undefined;
|
||||
if (effectiveDbType === "mongodb") return undefined;
|
||||
const useAgentCursor = usesAgentCursorForQuery(conn?.db_type, conn?.driver_profile);
|
||||
const queryBaseSql = queryResultBaseSql(tab);
|
||||
const executableSql = effectiveDbType === "mysql" ? stripMysqlClientDisplayCommand(sql) : sql;
|
||||
const queryBaseSql = effectiveDbType === "mysql" ? stripMysqlClientDisplayCommand(queryResultBaseSql(tab)) : queryResultBaseSql(tab);
|
||||
const resultStatementIndex = tab.result.statement_index;
|
||||
const batchSql = tab.resultBaseSql ?? tab.lastExecutedSql ?? tab.sql;
|
||||
const batchStatements = effectiveDbType === "postgres" && tab.result.truncated === true && Number.isInteger(resultStatementIndex) && resultStatementIndex! > 0 ? splitSqlStatementRanges(batchSql, effectiveDbType) : [];
|
||||
@@ -8452,7 +8457,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
database: dataTabExecutionDatabase(conn, location.database, location.catalog),
|
||||
schema: location.schema,
|
||||
catalog: location.catalog,
|
||||
sql,
|
||||
sql: executableSql,
|
||||
queryBaseSql,
|
||||
setupSql,
|
||||
databaseType: effectiveDbType,
|
||||
@@ -8488,13 +8493,14 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (!effectiveDbType) return;
|
||||
|
||||
const exportId = uuid();
|
||||
const executableSql = effectiveDbType === "mysql" ? stripMysqlClientDisplayCommand(sql) : sql;
|
||||
const request: api.QueryResultExportRequest = {
|
||||
exportId,
|
||||
connectionId: tab.connectionId,
|
||||
database: tab.database,
|
||||
schema: tab.schema,
|
||||
sql,
|
||||
queryBaseSql: sql,
|
||||
sql: executableSql,
|
||||
queryBaseSql: executableSql,
|
||||
databaseType: effectiveDbType,
|
||||
useAgentCursor: usesAgentCursorForQuery(conn?.db_type, conn?.driver_profile),
|
||||
filePath,
|
||||
|
||||
Reference in new issue
Block a user