perf(mysql): batch adjacent DML into multi-statement COM_QUERY for large scripts

This commit is contained in:
zipg authored and GitHub committed 2026-08-11 13:06:21 +08:00
1 parent adb2b4c84b
commit 7c1d094946
9 files changed
+683 -82

No files matched your search

+7 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
import { frontendQueryTimeoutDelayMs, frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
describe("queryTimeout", () => {
it("lets PostgreSQL row queries use the backend inactivity timeout", () => {
@@ -19,6 +19,12 @@ describe("queryTimeout", () => {
expect(queryTimeoutSecsForConnection({ query_timeout_secs: undefined })).toBe(30);
});
it("does not schedule frontend timeouts beyond the browser timer limit", () => {
expect(frontendQueryTimeoutDelayMs(60)).toBe(60_000);
expect(frontendQueryTimeoutDelayMs(11_401_200)).toBeUndefined();
expect(frontendQueryTimeoutDelayMs(0)).toBeUndefined();
});
it("uses the global timeout only for inheriting connections", () => {
expect(queryTimeoutSecsForConnection({ query_timeout_secs: 30, query_timeout_inherit: true }, 12)).toBe(12);
expect(queryTimeoutSecsForConnection({ query_timeout_secs: 30, query_timeout_inherit: false }, 12)).toBe(30);
@@ -234,6 +234,35 @@ describe("execution summary", () => {
{ status: "error", isError: true },
]);
});
it("maps out-of-order results to their explicit statement indexes", () => {
const items = executionSummaryItems({
results: [
{ columns: ["value"], rows: [["third"]], affected_rows: 0, execution_time_ms: 3, statement_index: 2 },
{ columns: ["value"], rows: [["first"]], affected_rows: 0, execution_time_ms: 1, statement_index: 0 },
],
batchSqlExecution: {
executionId: "run-out-of-order",
submittedSql: "SELECT 'first'; SELECT 'second'; SELECT 'third'",
editorFingerprint: "fingerprint",
sourceOffset: 0,
completed: 2,
total: 3,
startedAt: 1,
items: [
{ statementIndex: 0, sql: "SELECT 'first'", from: 0, to: 14, status: "success" },
{ statementIndex: 1, sql: "SELECT 'second'", from: 16, to: 31, status: "skipped" },
{ statementIndex: 2, sql: "SELECT 'third'", from: 33, to: 47, status: "success" },
],
},
});
expect(items.map((item) => [item.statementIndex, item.result?.rows[0]?.[0]])).toEqual([
[0, "first"],
[1, undefined],
[2, "third"],
]);
});
});
describe("statement execution markers", () => {
+7
View File
@@ -3,6 +3,7 @@ import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
import type { ConnectionConfig, DatabaseType } from "@/types/database";
export const DEFAULT_QUERY_TIMEOUT_SECS = 30;
const MAX_BROWSER_TIMER_DELAY_MS = 2_147_483_647;
const POSTGRES_ROW_STATEMENT_KEYWORDS = new Set(["select", "show", "explain", "table", "with"]);
const POSTGRES_RETURNING_STATEMENT_KEYWORDS = new Set(["insert", "update", "delete", "merge"]);
@@ -30,6 +31,12 @@ export function frontendQueryTimeoutSecsForSql(sql: string, databaseType: Databa
return baseTimeoutSecs * statementCount;
}
export function frontendQueryTimeoutDelayMs(timeoutSecs: number): number | undefined {
const timeoutMs = timeoutSecs * 1000;
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_BROWSER_TIMER_DELAY_MS) return undefined;
return timeoutMs;
}
function postgresQueryMayReturnRows(sql: string, databaseType: DatabaseType): boolean {
return splitSqlStatementRanges(sql, databaseType).some(({ sql: statement }) => {
const tokens = tokenizeSqlSemantic(statement, "postgres");
+5 -1
View File
@@ -368,8 +368,12 @@ export interface ExecutionSummaryItem {
export function executionSummaryItems(tab: Pick<QueryTab, "result" | "results" | "batchSqlExecution">): ExecutionSummaryItem[] {
const results = tab.results?.length ? tab.results : tab.result ? [tab.result] : [];
if (tab.batchSqlExecution?.items.length) {
const resultsByStatementIndex = new Map<number, QueryResult>();
results.forEach((result, resultIndex) => {
resultsByStatementIndex.set(result.statement_index ?? resultIndex, result);
});
return tab.batchSqlExecution.items.map((item, index) => {
const result = results.find((candidate, resultIndex) => (candidate.statement_index ?? resultIndex) === item.statementIndex);
const result = resultsByStatementIndex.get(item.statementIndex);
const returnedRows = result?.rows.length ?? 0;
const affectedRows = item.affectedRows ?? result?.affected_rows ?? 0;
const hasTabularResult = (result?.columns.length ?? 0) > 0;