fix(mysql): stop multi-statement execution after first error
This commit is contained in:
1 parent
6368c49c16
commit
7a42b1e96b
12 files changed
+523
-47
No files matched your search
@@ -2,6 +2,7 @@ import { computed, ref } from "vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requiresDatabaseSelection, useSqlExecution } from "../useSqlExecution";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
@@ -140,6 +141,88 @@ describe("useSqlExecution", () => {
|
||||
expect(executedSql).toContain("where fp.create_at < @date_start");
|
||||
});
|
||||
|
||||
it("records a later MySQL batch error and skips metadata refresh", async () => {
|
||||
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
|
||||
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
|
||||
const queryStore = useQueryStore();
|
||||
const historyStore = useHistoryStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
|
||||
const tab = activeTab.value;
|
||||
if (!tab) return;
|
||||
const successfulResult = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 };
|
||||
tab.result = successfulResult;
|
||||
tab.results = [successfulResult, { columns: ["Error"], execution_error: true, rows: [["Duplicate entry '1'"]], affected_rows: 0, execution_time_ms: 1 }];
|
||||
tab.activeResultIndex = 0;
|
||||
});
|
||||
const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined);
|
||||
const refreshObjects = vi.spyOn(connectionStore, "refreshObjectListTreeNode").mockResolvedValue(undefined);
|
||||
|
||||
const execution = useSqlExecution({
|
||||
activeTab: computed(() => activeTab.value),
|
||||
activeConnection: computed(() => activeConnection.value),
|
||||
executableSql: computed(() => "SELECT 1 AS value; CREATE TABLE duplicate_target (id INT)"),
|
||||
activeOutputView,
|
||||
});
|
||||
|
||||
await execution.tryExecute();
|
||||
|
||||
expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: false, error: "Duplicate entry '1'", affected_rows: undefined }));
|
||||
expect(refreshObjects).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not treat an unmarked MySQL Error alias as a batch failure", async () => {
|
||||
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
|
||||
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
|
||||
const queryStore = useQueryStore();
|
||||
const historyStore = useHistoryStore();
|
||||
vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
|
||||
const tab = activeTab.value;
|
||||
if (!tab) return;
|
||||
const successfulResult = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 };
|
||||
tab.result = successfulResult;
|
||||
tab.results = [successfulResult, { columns: ["Error"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 }];
|
||||
tab.activeResultIndex = 0;
|
||||
});
|
||||
const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined);
|
||||
|
||||
const execution = useSqlExecution({
|
||||
activeTab: computed(() => activeTab.value),
|
||||
activeConnection: computed(() => activeConnection.value),
|
||||
executableSql: computed(() => "SELECT 1 AS value; SELECT 2 AS Error"),
|
||||
activeOutputView,
|
||||
});
|
||||
|
||||
await execution.tryExecute();
|
||||
|
||||
expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: true, error: undefined }));
|
||||
});
|
||||
|
||||
it("continues to record active non-MySQL errors as failures", async () => {
|
||||
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("postgres"));
|
||||
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
|
||||
const queryStore = useQueryStore();
|
||||
const historyStore = useHistoryStore();
|
||||
vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
|
||||
if (activeTab.value) activeTab.value.result = { columns: ["Error"], rows: [["relation does not exist"]], affected_rows: 0, execution_time_ms: 1 };
|
||||
});
|
||||
const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined);
|
||||
|
||||
const execution = useSqlExecution({
|
||||
activeTab: computed(() => activeTab.value),
|
||||
activeConnection: computed(() => activeConnection.value),
|
||||
executableSql: computed(() => "SELECT * FROM missing_table"),
|
||||
activeOutputView,
|
||||
});
|
||||
|
||||
await execution.tryExecute();
|
||||
|
||||
expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: false, error: "relation does not exist" }));
|
||||
});
|
||||
|
||||
it("requires production confirmation even when ordinary danger prompts are disabled", async () => {
|
||||
const activeTab = ref<QueryTab | undefined>(queryTab("prod_app"));
|
||||
const activeConnection = ref<ConnectionConfig | undefined>({ ...connection("mysql"), production_databases: ["prod_app"] });
|
||||
|
||||
@@ -9,6 +9,7 @@ import { isSingleDatabase, usesTreeSchemaMode } from "@/lib/database/databaseCap
|
||||
import { canExecuteWithoutSelectedDatabase } from "@/lib/connection/connectionLevelDatabaseBootstrap";
|
||||
import { classifySqlActivityKind } from "@/lib/history/historyActivityKind";
|
||||
import { sqlMetadataRefreshTarget } from "@/lib/sql/sqlMetadataRefresh";
|
||||
import { isMysqlExecutionErrorResult, usesMysqlProtocolDatabaseType } from "@/lib/query/queryResultError";
|
||||
import { classifyRedisCommandSafety, firstRedisCommandToken } from "@/lib/redis/redisCommandSafety";
|
||||
import { isSqlExecutionSnapshot, resolveExecutableSql, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { extractSqlParameterDescriptors, type SqlParameterDescriptor, type SqlParameterSyntax } from "@/lib/sql/sqlParameters";
|
||||
@@ -41,6 +42,16 @@ function primarySqlOperation(sql: string): string {
|
||||
return statement?.match(/^([a-z]+)/i)?.[1]?.toUpperCase() || "SQL";
|
||||
}
|
||||
|
||||
function firstQueryExecutionError(tab: Pick<QueryTab, "result" | "results">, databaseType: DatabaseType | undefined) {
|
||||
const activeResult = tab.result;
|
||||
if (activeResult && isMysqlExecutionErrorResult(activeResult, databaseType)) return activeResult;
|
||||
if (!usesMysqlProtocolDatabaseType(databaseType) && activeResult?.columns.includes("Error")) return activeResult;
|
||||
if (!usesMysqlProtocolDatabaseType(databaseType)) return undefined;
|
||||
|
||||
const results = tab.results?.length ? tab.results : tab.result ? [tab.result] : [];
|
||||
return results.find((result) => isMysqlExecutionErrorResult(result, databaseType));
|
||||
}
|
||||
|
||||
export function useSqlExecution(deps: {
|
||||
activeTab: ComputedRef<QueryTab | undefined>;
|
||||
activeConnection: ComputedRef<ConnectionConfig | undefined>;
|
||||
@@ -146,20 +157,23 @@ export function useSqlExecution(deps: {
|
||||
sql ??= await resolvedExecutableSql();
|
||||
const tab = deps.activeTab.value;
|
||||
if (!tab || !sql.trim()) return;
|
||||
if (requiresDatabaseSelection(tab, deps.activeConnection.value, sql)) {
|
||||
const executionConnection = connectionStore.getConfig(tab.connectionId) ?? deps.activeConnection.value;
|
||||
const executionDatabaseType = executionConnection?.db_type;
|
||||
if (requiresDatabaseSelection(tab, executionConnection, sql)) {
|
||||
deps.onMissingDatabase?.();
|
||||
return;
|
||||
}
|
||||
deps.activeOutputView.value = "result";
|
||||
const connName = connectionStore.getConfig(tab.connectionId)?.name || "";
|
||||
const connName = executionConnection?.name || "";
|
||||
const start = Date.now();
|
||||
const isRedis = deps.activeConnection.value?.db_type === "redis";
|
||||
const isRedis = executionDatabaseType === "redis";
|
||||
await queryStore.executeCurrentSql(sql, isRedis ? { skipRedisSafetyCheck: deps.blockDangerousRedisCommands?.value === false } : undefined);
|
||||
if (tab.result && !tab.result.columns.length && !tab.results?.some((result) => result.columns.length > 0)) {
|
||||
deps.activeOutputView.value = "summary";
|
||||
}
|
||||
const elapsed = Date.now() - start;
|
||||
const success = !tab.result?.columns.includes("Error");
|
||||
const failure = firstQueryExecutionError(tab, executionDatabaseType);
|
||||
const success = !failure;
|
||||
historyStore.add({
|
||||
connection_id: tab.connectionId,
|
||||
connection_name: connName,
|
||||
@@ -167,7 +181,7 @@ export function useSqlExecution(deps: {
|
||||
sql,
|
||||
execution_time_ms: elapsed,
|
||||
success,
|
||||
error: success ? undefined : String(tab.result?.rows?.[0]?.[0] ?? ""),
|
||||
error: failure ? String(failure.rows?.[0]?.[0] ?? "") : undefined,
|
||||
activity_kind: classifySqlActivityKind(sql),
|
||||
operation: primarySqlOperation(sql),
|
||||
affected_rows: success ? tab.result?.affected_rows : undefined,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { QueryResult } from "@/types/database";
|
||||
|
||||
import { isNoSnapshotErrorResult } from "@/lib/query/queryResultError";
|
||||
import { isMysqlExecutionErrorResult, isNoSnapshotErrorResult } from "@/lib/query/queryResultError";
|
||||
|
||||
function errorResult(message: string): QueryResult {
|
||||
return { columns: ["Error"], rows: [[message]], affected_rows: 0, execution_time_ms: 0 };
|
||||
@@ -39,3 +39,23 @@ describe("isNoSnapshotErrorResult", () => {
|
||||
expect(isNoSnapshotErrorResult({ ...errorResult("There is currently no snapshot."), rows: [] })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isMysqlExecutionErrorResult", () => {
|
||||
it("recognizes an explicitly marked MySQL batch execution error", () => {
|
||||
expect(isMysqlExecutionErrorResult({ ...errorResult("Duplicate entry '1'"), execution_error: true }, "mysql")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not mistake an unmarked Error alias without type metadata for an execution error", () => {
|
||||
const result: QueryResult = {
|
||||
columns: ["Error"],
|
||||
rows: [["2"]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
};
|
||||
expect(isMysqlExecutionErrorResult(result, "mysql")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not apply the native MySQL heuristic to JDBC connections", () => {
|
||||
expect(isMysqlExecutionErrorResult({ ...errorResult("Duplicate entry '1'"), execution_error: true }, "jdbc")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueryResult } from "@/types/database";
|
||||
import type { DatabaseType, QueryResult } from "@/types/database";
|
||||
|
||||
// Lake/external tables (e.g. Paimon in StarRocks) return this error on a data
|
||||
// read when no snapshot exists yet, while metadata reads (DESC/SHOW CREATE)
|
||||
@@ -6,6 +6,17 @@ import type { QueryResult } from "@/types/database";
|
||||
// callers can detect this case and fall back to a structure-only (LIMIT 0)
|
||||
// preview instead of showing a cryptic server error.
|
||||
const NO_SNAPSHOT_ERROR_PATTERN = /there is currently no snapshot/i;
|
||||
const MYSQL_PROTOCOL_DATABASE_TYPES = new Set<DatabaseType>(["mysql", "doris", "starrocks", "manticoresearch"]);
|
||||
|
||||
export function usesMysqlProtocolDatabaseType(databaseType: DatabaseType | undefined): boolean {
|
||||
return databaseType !== undefined && MYSQL_PROTOCOL_DATABASE_TYPES.has(databaseType);
|
||||
}
|
||||
|
||||
// The batch executor marks synthesized MySQL-protocol errors explicitly so a
|
||||
// successful result column named Error is never mistaken for a failure.
|
||||
export function isMysqlExecutionErrorResult(result: QueryResult, databaseType: DatabaseType | undefined): boolean {
|
||||
return usesMysqlProtocolDatabaseType(databaseType) && result.execution_error === true;
|
||||
}
|
||||
|
||||
export function isNoSnapshotErrorResult(result: QueryResult | undefined | null): boolean {
|
||||
if (!result || !result.columns.includes("Error") || result.rows.length === 0) return false;
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface TabResultSnapshot {
|
||||
|
||||
interface ColumnarQueryResult {
|
||||
columns: string[];
|
||||
execution_error?: true;
|
||||
column_types?: string[];
|
||||
columnValues: CellValue[][];
|
||||
rowCount: number;
|
||||
@@ -134,6 +135,7 @@ function stripSessionIds(result: QueryResult | undefined): QueryResult | undefin
|
||||
if (!result) return undefined;
|
||||
return {
|
||||
columns: [...result.columns],
|
||||
execution_error: result.execution_error,
|
||||
column_types: result.column_types ? [...result.column_types] : undefined,
|
||||
rows: result.rows.map((row) => [...row]),
|
||||
mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined,
|
||||
@@ -167,6 +169,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult
|
||||
const columnValues = result.columns.map((_, colIndex) => result.rows.map((row) => row[colIndex] ?? null));
|
||||
return removeUndefinedFields({
|
||||
columns: [...result.columns],
|
||||
execution_error: result.execution_error,
|
||||
column_types: result.column_types ? [...result.column_types] : undefined,
|
||||
columnValues,
|
||||
rowCount: result.rows.length,
|
||||
@@ -185,6 +188,7 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul
|
||||
const rows = Array.from({ length: result.rowCount }, (_, rowIndex) => result.columnValues.map((values) => values[rowIndex] ?? null));
|
||||
return {
|
||||
columns: [...result.columns],
|
||||
execution_error: result.execution_error,
|
||||
column_types: result.column_types ? [...result.column_types] : undefined,
|
||||
rows,
|
||||
mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
analyzeEditableQueryEditability: vi.fn(),
|
||||
closeClientConnectionSession: vi.fn(),
|
||||
closeQuerySession: vi.fn(),
|
||||
executeMulti: vi.fn(),
|
||||
getConnectionConfig: vi.fn(),
|
||||
prepareQueryPaginationExecutionPlan: vi.fn(),
|
||||
saveOpenTabsState: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
analyzeEditableQueryEditability: mocks.analyzeEditableQueryEditability,
|
||||
closeClientConnectionSession: mocks.closeClientConnectionSession,
|
||||
closeQuerySession: mocks.closeQuerySession,
|
||||
executeMulti: mocks.executeMulti,
|
||||
prepareQueryPaginationExecutionPlan: mocks.prepareQueryPaginationExecutionPlan,
|
||||
saveOpenTabsState: mocks.saveOpenTabsState,
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
useConnectionStore: () => ({
|
||||
ensureConnected: vi.fn().mockResolvedValue(undefined),
|
||||
getConfig: mocks.getConnectionConfig,
|
||||
recordConnectionLostError: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({
|
||||
editorSettings: { autoCalculateTotalRows: false, pageSize: 100 },
|
||||
}),
|
||||
}));
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||
});
|
||||
}
|
||||
|
||||
describe("queryStore multi-statement errors", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "mysql-1",
|
||||
name: "MySQL",
|
||||
db_type: "mysql",
|
||||
database: "app",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.prepareQueryPaginationExecutionPlan.mockImplementation(async (options) => ({
|
||||
sqlToExecute: options.sql,
|
||||
pageSql: undefined,
|
||||
pageLimit: undefined,
|
||||
pageOffset: undefined,
|
||||
countSql: undefined,
|
||||
useAgentResultSession: false,
|
||||
}));
|
||||
mocks.analyzeEditableQueryEditability.mockResolvedValue({ editable: false, reason: "multiple-statements" });
|
||||
});
|
||||
|
||||
it("opens the first error result from a mixed result batch", async () => {
|
||||
mocks.executeMulti.mockResolvedValue([
|
||||
{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
|
||||
{ columns: ["Error"], execution_error: true, rows: [["no such table: missing"]], affected_rows: 0, execution_time_ms: 1 },
|
||||
]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "app", "Query");
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT 1 AS value; SELECT * FROM missing");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.activeResultIndex).toBe(1);
|
||||
expect(tab.result?.columns).toEqual(["Error"]);
|
||||
});
|
||||
|
||||
it("does not promote an unmarked Error alias without type metadata as a batch failure", async () => {
|
||||
mocks.executeMulti.mockResolvedValue([
|
||||
{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
|
||||
{ columns: ["Error"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 },
|
||||
]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "app", "Query");
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT 1 AS value; SELECT 2 AS Error");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.activeResultIndex).toBe(0);
|
||||
expect(tab.result?.columns).toEqual(["value"]);
|
||||
});
|
||||
|
||||
it("does not apply the MySQL result heuristic to a JDBC MySQL dialect", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "mysql-1",
|
||||
name: "JDBC MySQL",
|
||||
db_type: "jdbc",
|
||||
connection_string: "jdbc:mysql://localhost:3306/app",
|
||||
database: "app",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.executeMulti.mockResolvedValue([
|
||||
{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
|
||||
{ columns: ["Error"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 },
|
||||
]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "app", "Query");
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT 1 AS value; SELECT 2 AS Error");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.activeResultIndex).toBe(0);
|
||||
expect(tab.result?.columns).toEqual(["value"]);
|
||||
});
|
||||
});
|
||||
@@ -45,6 +45,7 @@ import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sq
|
||||
import { clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor";
|
||||
import { buildTabResultSnapshot, deleteTabResultSnapshot, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
|
||||
import { queryResultBaseSql, queryResultExecutionSql } from "@/lib/tabs/tabPresentation";
|
||||
import { isMysqlExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import { decodeQueryResultArchive, encodeQueryResultArchive, type DecodedQueryResultArchive } from "@/lib/query/queryResultArchive";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
@@ -1982,6 +1983,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
return markQueryResultRowsRaw({
|
||||
columns: ["Error"],
|
||||
execution_error: true,
|
||||
rows: [[message]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
@@ -2920,8 +2922,9 @@ export const useQueryStore = defineStore("query", () => {
|
||||
current.results[activeGroupIndex] = results[0];
|
||||
current.result = results[0];
|
||||
} else if (results.length > 1) {
|
||||
const errorResultIndex = results.findIndex((result) => isMysqlExecutionErrorResult(result, conn?.db_type));
|
||||
const activeResultIndex = results.findIndex((result) => result.columns.length > 0);
|
||||
const resultIndex = preservedResultIndex(results, current.activeResultIndex, options?.preserveActiveResultIndex) ?? (activeResultIndex >= 0 ? activeResultIndex : 0);
|
||||
const resultIndex = errorResultIndex >= 0 ? errorResultIndex : (preservedResultIndex(results, current.activeResultIndex, options?.preserveActiveResultIndex) ?? (activeResultIndex >= 0 ? activeResultIndex : 0));
|
||||
current.results = results;
|
||||
current.activeResultIndex = resultIndex;
|
||||
current.result = results[resultIndex];
|
||||
|
||||
@@ -449,6 +449,8 @@ export interface OwnerInfo {
|
||||
|
||||
export interface QueryResult {
|
||||
columns: string[];
|
||||
/** Set for synthesized query execution failures. */
|
||||
execution_error?: true;
|
||||
/** Internal row identifiers appended to editable query results. */
|
||||
hidden_column_indexes?: number[];
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user