add: 增加对于查询得到的结果集直接进行数据编辑的功能。当查询是单表且包含主键(有联合主键时包含全部主键)时,可以在结果集内双击单元格修改数据,修改结果以事务形式提交或回滚。

This commit is contained in:
rarnu
2026-05-07 15:09:59 +08:00
parent ecbd94c7be
commit 814fda7433
15 changed files with 627 additions and 2 deletions
+161
View File
@@ -6,7 +6,9 @@ import { orderPinnedFirst } from "@/lib/pinnedItems";
import { canCancelQueryExecution } from "@/lib/queryExecutionState";
import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions";
import { buildExplainSql, parseExplainResult } from "@/lib/explainPlan";
import { analyzeEditableQuery, allPrimaryKeysPresent } from "@/lib/sqlAnalysis";
import * as api from "@/lib/api";
import { useConnectionStore } from "@/stores/connectionStore";
import { isTauriRuntime } from "@/lib/tauriRuntime";
interface SavedTab {
@@ -227,7 +229,163 @@ export const useQueryStore = defineStore("query", () => {
await executeTabSql(activeTabId.value, sql);
}
/**
* Analyze if the query result is editable (single-table SELECT with primary keys).
* If editable, fetches table metadata and sets queryAnalysis + tableMeta on the tab.
*/
async function analyzeQueryEditability(tab: QueryTab, sql: string) {
console.log("[DBX] === analyzeQueryEditability CALLED ===", { tabId: tab.id });
// Only analyze for query mode tabs with a valid result set
if (tab.mode !== "query") {
console.log("[DBX] SKIP: mode is not query, mode =", tab.mode);
return;
}
if (!tab.result || !tab.result.columns.length) {
console.log("[DBX] SKIP: no result or no columns", {
hasResult: !!tab.result,
colCount: tab.result?.columns.length,
});
tab.queryAnalysis = undefined;
tab.tableMeta = undefined;
return;
}
console.log("[DBX] analyzeQueryEditability:", {
sql: sql.substring(0, 100),
mode: tab.mode,
columns: tab.result.columns,
connectionId: tab.connectionId,
database: tab.database,
schema: tab.schema,
});
const analysis = analyzeEditableQuery(sql);
console.log("[DBX] analyzeEditableQuery result:", analysis);
if (!analysis) {
console.log("[DBX] SKIP: analyzeEditableQuery returned null");
tab.queryAnalysis = undefined;
tab.tableMeta = undefined;
return;
}
// Resolve schema: use explicit schema from analysis/tab, else fall back to db-type defaults
const connStore = useConnectionStore();
const conn = connStore.getConfig(tab.connectionId);
const dbType = conn?.db_type || "";
let schema = analysis.schema || tab.schema;
if (!schema) {
if (dbType === "postgres") schema = "public";
else schema = "";
}
console.log("[DBX] Resolved schema:", schema, "dbType:", dbType);
if (analysis.selectStar && tab.connectionId && tab.database) {
try {
console.log("[DBX] Fetching columns:", {
connectionId: tab.connectionId,
database: tab.database,
schema,
tableName: analysis.tableName,
});
const columns = await api.getColumns(tab.connectionId, tab.database, schema, analysis.tableName);
console.log(
"[DBX] Columns fetched:",
columns.length,
"columns, PKs:",
columns.filter((c) => c.is_primary_key).map((c) => c.name),
);
const primaryKeys = columns.filter((c) => c.is_primary_key).map((c) => c.name);
if (primaryKeys.length === 0) {
console.log("[DBX] SKIP: no primary keys found for table", analysis.tableName);
tab.queryAnalysis = undefined;
tab.tableMeta = undefined;
return;
}
tab.tableMeta = {
schema: schema || undefined,
tableName: analysis.tableName,
columns,
primaryKeys,
};
// Verify all PK columns are present in result
if (!allPrimaryKeysPresent(primaryKeys, tab.result.columns)) {
console.log(
"[DBX] SKIP: not all PKs in result columns. PKs:",
primaryKeys,
"Result cols:",
tab.result.columns,
);
tab.queryAnalysis = undefined;
tab.tableMeta = undefined;
return;
}
console.log("[DBX] SET queryAnalysis + tableMeta successfully");
tab.queryAnalysis = analysis;
} catch (err) {
console.error("[DBX] ERROR fetching columns:", err);
tab.queryAnalysis = undefined;
tab.tableMeta = undefined;
}
} else {
// Non-SELECT *: need to fetch table metadata to get PK info
console.log("[DBX] Non-SELECT* path, fetching table metadata");
if (!tab.connectionId || !tab.database) {
console.log("[DBX] SKIP: no connectionId or database");
tab.queryAnalysis = undefined;
tab.tableMeta = undefined;
return;
}
try {
const columns = await api.getColumns(tab.connectionId, tab.database, schema, analysis.tableName);
console.log(
"[DBX] Columns fetched:",
columns.length,
"columns, PKs:",
columns.filter((c) => c.is_primary_key).map((c) => c.name),
);
const primaryKeys = columns.filter((c) => c.is_primary_key).map((c) => c.name);
if (primaryKeys.length === 0) {
console.log("[DBX] SKIP: no primary keys found for table", analysis.tableName);
tab.queryAnalysis = undefined;
tab.tableMeta = undefined;
return;
}
// Check all PKs are in the result columns
if (!allPrimaryKeysPresent(primaryKeys, tab.result.columns)) {
console.log(
"[DBX] SKIP: not all PKs in result columns. PKs:",
primaryKeys,
"Result cols:",
tab.result.columns,
);
tab.queryAnalysis = undefined;
tab.tableMeta = undefined;
return;
}
console.log("[DBX] SET queryAnalysis + tableMeta successfully (non-SELECT* path)");
tab.tableMeta = {
schema: schema || undefined,
tableName: analysis.tableName,
columns,
primaryKeys,
};
tab.queryAnalysis = analysis;
} catch (err) {
console.error("[DBX] ERROR fetching columns (non-SELECT* path):", err);
tab.queryAnalysis = undefined;
tab.tableMeta = undefined;
}
}
}
async function executeTabSql(id: string, sql: string) {
console.log("[DBX] === executeTabSql CALLED ===", { tabId: id, sql: sql.substring(0, 80) });
const tab = tabs.value.find((t) => t.id === id);
if (!tab || !sql.trim()) return;
@@ -249,6 +407,8 @@ export const useQueryStore = defineStore("query", () => {
current.activeResultIndex = undefined;
current.result = results[0];
}
// Analyze editability after successful execution
await analyzeQueryEditability(current, sql);
}
} catch (e: any) {
const current = tabs.value.find((t) => t.id === id);
@@ -256,6 +416,7 @@ export const useQueryStore = defineStore("query", () => {
current.result = toErrorResult(e);
current.results = undefined;
current.activeResultIndex = undefined;
current.queryAnalysis = undefined;
}
} finally {
const current = tabs.value.find((t) => t.id === id);