perf(dameng): defer cold query metadata until after execution

This commit is contained in:
zipg
2026-10-14 16:25:44 +08:00
committed by GitHub
parent 3e9974b226
commit cfdcd9041b
5 changed files with 532 additions and 2 deletions
@@ -188,7 +188,112 @@ describe("queryStore hidden primary key editing", () => {
expect(executeMulti).toHaveBeenCalledWith("mysql-1", "app", "SELECT name FROM users", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
}, 10_000);
it("appends the hidden primary key for Dameng tables whose columns keep the created case", async () => {
it.each((["dameng", "jdbc"] as const).flatMap((dbType) => [true, false].flatMap((autoCommit) => [true, false].map((returnsPrimaryKey) => ({ dbType, autoCommit, returnsPrimaryKey })))))(
"dispatches cold Dameng SQL before metadata with $dbType, autoCommit=$autoCommit, returnsPrimaryKey=$returnsPrimaryKey",
async ({ dbType, autoCommit, returnsPrimaryKey }) => {
const columnsGate = deferred<Awaited<ReturnType<typeof getColumns>>>();
const resultGate = deferred<Awaited<ReturnType<typeof executeMulti>>>();
getConnectionConfig.mockReturnValue({ id: "dameng-1", name: "Dameng", db_type: dbType, driver_profile: "dameng", database: "SYSDBA", query_timeout_secs: 30 });
const columns = [
{ name: "ID", data_type: "INT", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "NAME", data_type: "VARCHAR(50)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
];
const resultColumns = returnsPrimaryKey ? ["ID", "NAME"] : ["NAME"];
const sql = `SELECT ${resultColumns.join(", ")} FROM APP.USERS`;
analyzeEditableQueryEditability.mockResolvedValue({
editable: true,
analysis: { schema: "APP", tableName: "USERS", selectStar: false, columns: resultColumns.map((name) => ({ sourceName: name, resultName: name, expression: name })) },
});
getColumns.mockReturnValue(columnsGate.promise);
executeMulti.mockReturnValue(resultGate.promise);
executeInManualTransaction.mockReturnValue(resultGate.promise);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("dameng-1", "SYSDBA", "Query");
store.setAutoCommit(tabId, autoCommit);
const tab = store.tabs.find((item) => item.id === tabId)!;
const execution = store.executeTabSql(tabId, sql);
const dispatch = autoCommit ? executeMulti : executeInManualTransaction;
await vi.waitFor(() => expect(dispatch).toHaveBeenCalledOnce());
expect(dispatch.mock.calls[0]?.[autoCommit ? 2 : 1]).toBe(sql);
expect(getColumns).not.toHaveBeenCalled();
expect(listIndexes).not.toHaveBeenCalled();
resultGate.resolve([{ columns: resultColumns, rows: [returnsPrimaryKey ? [1, "Alice"] : ["Alice"]], affected_rows: 0, execution_time_ms: 1 }]);
await execution;
expect(tab.isExecuting).toBe(false);
expect(tab.result?.columns).toEqual(resultColumns);
expect(tab.queryAnalysis).toBeUndefined();
await vi.waitFor(() => expect(getColumns).toHaveBeenCalledOnce());
columnsGate.resolve(columns);
await vi.waitFor(() => expect(tab.tableMeta?.primaryKeys).toEqual(["ID"]));
expect(tab.result?.columns).toEqual(resultColumns);
if (returnsPrimaryKey) {
expect(tab.queryAnalysis).toBeDefined();
expect(tab.queryEditabilityReason).toBeUndefined();
} else {
expect(tab.queryAnalysis).toBeUndefined();
expect(tab.queryEditabilityReason).toBe("primary-key-not-returned");
}
},
);
it("does not apply delayed Dameng metadata to a newer result", async () => {
const firstColumns = deferred<Awaited<ReturnType<typeof getColumns>>>();
const columns = [
{ name: "ID", data_type: "INT", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "NAME", data_type: "VARCHAR(50)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
];
getConnectionConfig.mockReturnValue({ id: "dameng-1", name: "Dameng", db_type: "dameng", database: "SYSDBA", query_timeout_secs: 30 });
analyzeEditableQueryEditability.mockImplementation(async (sql: string) => ({
editable: true,
analysis: { schema: "APP", tableName: sql.includes("ARCHIVE") ? "ARCHIVE" : "USERS", selectStar: false, columns: ["ID", "NAME"].map((name) => ({ sourceName: name, resultName: name, expression: name })) },
}));
getColumns.mockImplementation((_connectionId: string, _database: string, _schema: string, table: string) => (table === "USERS" ? firstColumns.promise : Promise.resolve(columns)));
executeMulti.mockResolvedValueOnce([{ columns: ["ID", "NAME"], rows: [[1, "Alice"]], affected_rows: 0, execution_time_ms: 1 }]);
executeMulti.mockResolvedValueOnce([{ columns: ["ID", "NAME"], rows: [[2, "Bob"]], affected_rows: 0, execution_time_ms: 1 }]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("dameng-1", "SYSDBA", "Query");
store.setAutoCommit(tabId, true);
const tab = store.tabs.find((item) => item.id === tabId)!;
await store.executeTabSql(tabId, "SELECT ID, NAME FROM APP.USERS");
await vi.waitFor(() => expect(getColumns).toHaveBeenCalledOnce());
await store.executeTabSql(tabId, "SELECT ID, NAME FROM APP.ARCHIVE");
await vi.waitFor(() => expect(tab.tableMeta?.tableName).toBe("ARCHIVE"));
const newerMetadata = tab.tableMeta;
firstColumns.resolve(columns);
const { loadTableMetadata } = await import("@/lib/metadata/tableMetadataCache");
await loadTableMetadata({ connectionId: "dameng-1", database: "SYSDBA", schema: "APP", tableName: "USERS", databaseType: "dameng" });
expect(tab.tableMeta).toBe(newerMetadata);
expect(tab.tableMeta?.tableName).toBe("ARCHIVE");
expect(tab.result?.rows).toEqual([[2, "Bob"]]);
expect(tab.queryEditabilityReason).toBeUndefined();
});
it("keeps a successful Dameng result available when background metadata fails", async () => {
getConnectionConfig.mockReturnValue({ id: "dameng-1", name: "Dameng", db_type: "dameng", database: "SYSDBA", query_timeout_secs: 30 });
getColumns.mockRejectedValue(new Error("dictionary unavailable"));
executeMulti.mockResolvedValue([{ columns: ["name"], rows: [["Alice"]], affected_rows: 0, execution_time_ms: 1 }]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("dameng-1", "SYSDBA", "Query");
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT name FROM users");
const tab = store.tabs.find((item) => item.id === tabId)!;
await vi.waitFor(() => expect(tab.queryEditabilityReason).toBe("metadata-unavailable"));
expect(tab.isExecuting).toBe(false);
expect(tab.error).toBeUndefined();
expect(tab.result?.rows).toEqual([["Alice"]]);
expect(tab.queryAnalysis).toBeUndefined();
});
it("appends the cached hidden primary key for Dameng tables whose columns keep the created case", async () => {
// #10233: Dameng keeps the case an unquoted identifier was created with, so the
// server reports `name` while the Oracle-family dialect handling folds the SQL
// token to `NAME`. The binding has to survive that mismatch, otherwise the result
@@ -214,10 +319,15 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("dameng-1", "SYSDBA", "Query");
executeMulti.mockResolvedValueOnce([{ columns: ["name"], rows: [["Alice"]], affected_rows: 0, execution_time_ms: 1 }]);
await store.executeTabSql(tabId, "SELECT name FROM users");
expect(executeMulti).toHaveBeenCalledWith("dameng-1", "SYSDBA", "SELECT name FROM users", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
await vi.waitFor(() => expect(store.tabs.find((item) => item.id === tabId)?.queryEditabilityReason).toBe("primary-key-not-returned"));
await store.executeTabSql(tabId, "SELECT name FROM users");
expect(executeMulti).toHaveBeenCalledWith("dameng-1", "SYSDBA", 'SELECT name, "id" AS "__DBX_PK_0" FROM users', undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
expect(executeMulti).toHaveBeenLastCalledWith("dameng-1", "SYSDBA", 'SELECT name, "id" AS "__DBX_PK_0" FROM users', undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
const tab = store.tabs.find((item) => item.id === tabId)!;
expect(tab.result?.hidden_column_indexes).toEqual([1]);
await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["name", "id"]));
+8
View File
@@ -6697,6 +6697,14 @@ export const useQueryStore = defineStore("query", () => {
const target = resolveEditableSourceMetadataTarget(tab, analysis, source, conn, databaseType, executionDatabase);
const cached = getCachedTableMetadata(target.request);
// Dameng dictionary reads can be much slower than the user's SELECT.
// Cold metadata only enriches editing: dispatch the original SQL first,
// then let analyzeQueryMetadataInBackground populate the shared cache.
// Do not start a competing dictionary request before the result arrives.
if (databaseType === "dameng" && !cached) {
queryExecutionLog("info", "metadata:preflight:deferred", { traceId, table: target.request.tableName, databaseType, elapsed: elapsed() });
return unchanged;
}
let loaded = cached ? loadedEditableSourceFromMetadata(target, cached.metadata) : undefined;
if (!cached && (databaseType === "oracle" || databaseType === "xugu")) {
// Oracle column discovery can be slow. A star projection over a table
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,369 @@
{
"date": "2026-10-14",
"environment": {
"database": "DM8",
"driver": "DmJdbcDriver8 8.1.5.45",
"runtime": "JDK 21",
"tableColumns": 301,
"tableRows": 10000,
"returnedRows": 100,
"scope": "actual queryStore with a thin JSON-RPC adapter to the current-source Dameng agent; no Web/Tauri UI or Rust HTTP transport"
},
"before": [
{
"name": "normal-cold",
"metadataDelayMs": 0,
"autoCommit": true,
"resultElapsedMs": 87.56,
"rows": 100,
"primaryKeys": [
"ID"
],
"events": [
{
"operation": "columns:start",
"elapsed": 4.7
},
{
"operation": "indexes:start",
"elapsed": 4.87
},
{
"operation": "indexes:done",
"elapsed": 63.35
},
{
"operation": "columns:done",
"elapsed": 71.24
},
{
"operation": "execute:start",
"elapsed": 72.96,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 83.05,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
}
]
},
{
"name": "normal-cold-warm",
"resultElapsedMs": 6.61,
"rows": 100,
"hiddenColumnIndexes": [
4
],
"events": [
{
"operation": "execute:start",
"elapsed": 0.63,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 5.31,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
}
]
},
{
"name": "delayed-metadata-cold",
"metadataDelayMs": 2000,
"autoCommit": true,
"resultElapsedMs": 1022.49,
"rows": 100,
"primaryKeys": [
"ID"
],
"editability": "primary-key-not-returned",
"events": [
{
"operation": "columns:start",
"elapsed": 0.58
},
{
"operation": "indexes:start",
"elapsed": 0.65
},
{
"operation": "indexes:done",
"elapsed": 19.07
},
{
"operation": "execute:start",
"elapsed": 1003.38,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 1019.46,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "columns:done",
"elapsed": 2021.75
}
]
},
{
"name": "delayed-metadata-cold-warm",
"resultElapsedMs": 18.82,
"rows": 100,
"hiddenColumnIndexes": [
4
],
"events": [
{
"operation": "execute:start",
"elapsed": 0.89,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 17.18,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
}
]
},
{
"name": "delayed-metadata-manual",
"metadataDelayMs": 2000,
"autoCommit": false,
"resultElapsedMs": 1028.66,
"rows": 100,
"primaryKeys": [
"ID"
],
"editability": "primary-key-not-returned",
"events": [
{
"operation": "columns:start",
"elapsed": 0.93
},
{
"operation": "indexes:start",
"elapsed": 1.05
},
{
"operation": "indexes:done",
"elapsed": 17.26
},
{
"operation": "execute:start",
"elapsed": 1019.33,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 1026.43,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "columns:done",
"elapsed": 2018.76
}
]
},
{
"name": "delayed-metadata-manual-warm",
"resultElapsedMs": 14.67,
"rows": 100,
"hiddenColumnIndexes": [
4
],
"events": [
{
"operation": "execute:start",
"elapsed": 1.77,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 12,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
}
]
}
],
"after": [
{
"name": "normal-cold",
"metadataDelayMs": 0,
"autoCommit": true,
"resultElapsedMs": 49.94,
"rows": 100,
"primaryKeys": [
"ID"
],
"editability": "primary-key-not-returned",
"events": [
{
"operation": "execute:start",
"elapsed": 3.91,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 44.78,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "columns:start",
"elapsed": 49.72
},
{
"operation": "indexes:start",
"elapsed": 49.92
},
{
"operation": "indexes:done",
"elapsed": 78.93
},
{
"operation": "columns:done",
"elapsed": 86.36
}
]
},
{
"name": "normal-cold-warm",
"resultElapsedMs": 6.32,
"rows": 100,
"hiddenColumnIndexes": [
4
],
"events": [
{
"operation": "execute:start",
"elapsed": 0.93,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 5,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
}
]
},
{
"name": "delayed-metadata-cold",
"metadataDelayMs": 2000,
"autoCommit": true,
"resultElapsedMs": 5.58,
"rows": 100,
"primaryKeys": [
"ID"
],
"editability": "primary-key-not-returned",
"events": [
{
"operation": "execute:start",
"elapsed": 0.45,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 4.46,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "columns:start",
"elapsed": 5.47
},
{
"operation": "indexes:start",
"elapsed": 5.56
},
{
"operation": "indexes:done",
"elapsed": 25.31
},
{
"operation": "columns:done",
"elapsed": 2027.28
}
]
},
{
"name": "delayed-metadata-cold-warm",
"resultElapsedMs": 19.43,
"rows": 100,
"hiddenColumnIndexes": [
4
],
"events": [
{
"operation": "execute:start",
"elapsed": 1.27,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 17.63,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
}
]
},
{
"name": "delayed-metadata-manual",
"metadataDelayMs": 2000,
"autoCommit": false,
"resultElapsedMs": 21.89,
"rows": 100,
"primaryKeys": [
"ID"
],
"editability": "primary-key-not-returned",
"events": [
{
"operation": "execute:start",
"elapsed": 12.76,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 19.18,
"sql": "SELECT C1, C2, C3, C4 FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "columns:start",
"elapsed": 21.63
},
{
"operation": "indexes:start",
"elapsed": 21.85
},
{
"operation": "indexes:done",
"elapsed": 62.53
},
{
"operation": "columns:done",
"elapsed": 2064.44
}
]
},
{
"name": "delayed-metadata-manual-warm",
"resultElapsedMs": 13.84,
"rows": 100,
"hiddenColumnIndexes": [
4
],
"events": [
{
"operation": "execute:start",
"elapsed": 0.8,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
},
{
"operation": "execute:done",
"elapsed": 12.25,
"sql": "SELECT C1, C2, C3, C4, \"ID\" AS \"__DBX_PK_0\" FROM SYSDBA.T10308W FETCH FIRST 100 ROWS ONLY"
}
]
}
]
}
@@ -0,0 +1,43 @@
# 达梦查询元数据异步化实测(2026-10-14)
## 环境和边界
- 本地 `dm8_demo` 容器,DM Database Server 64 V8,构建标识 `03134284094-20230927-203585-20067`。
- 本次工作区源码构建的 Dameng JDBC agent,JDK 21,DmJdbcDriver8 8.1.5.45。
- 复用只读测试表 `SYSDBA.T10308W`:301 列、10,000 行,返回前 100 行。
- 运行实际 `useQueryStore().executeTabSql()`;临时 Vitest 驱动只把后端 API 映射到真实 agent JSON-RPC,SQL、列和索引均由真实达梦返回。SQL 分析使用现有前端分析器。
- 未启动 Web/Tauri 客户端或 Rust HTTP 网关,数字不包含真实 HTTP 传输和界面渲染。连接已打开;“冷”指表元数据缓存为空。
- 对照使用主线 `184b55c89`;优化使用同一主线上的改动。每个场景为一次时序校验,不能作为普遍性能增幅的统计结论。
## SQL
```sql
SELECT C1, C2, C3, C4
FROM SYSDBA.T10308W
FETCH FIRST 100 ROWS ONLY
```
## 结果
| 场景 | 改动前:结果可用 | 改动后:结果可用 |
| --- | ---: | ---: |
| Normal cold metadata | 87.56 ms | 49.94 ms |
| Metadata response +2s | 1022.49 ms | 5.58 ms |
| Metadata +2s, manual TX | 1028.66 ms | 21.89 ms |
额外 2 秒是测试驱动在真实 `get_columns` 响应之后注入的延迟,不是达梦实测的自然元数据耗时。对照路径会先发列/索引请求,等待约 1 秒的预检预算;优化后先发 SQL,结果返回后才发元数据请求。
![首次结果等待时间](../assets/pr-dameng-query-metadata/first-result-latency.png)
## 校验内容
- 正常、慢元数据、手动事务三个场景均返回真实 100 行,列数据一致。
- 优化后每个冷缓存场景均满足 `execute:done` 早于 `columns:start` 和 `indexes:start`。
- 元数据未准备好时结果已可读,不对缺少主键的结果开启编辑。
- 元数据准备好后再次查询命中缓存,SQL 补入 `"ID" AS "__DBX_PK_0"`,隐藏列索引为 4,恢复编辑,未重复读取列元数据。
- 手动事务通过真实 agent 的 `begin_manual_transaction` 开始,读完后回滚。
- 修改前后各 3 个实测场景通过。另有元数据失败、过期响应、JDBC profile 和手动事务的常规回归测试。
完整时间戳及 SQL 记录见 [live-results.json](../assets/pr-dameng-query-metadata/live-results.json)。
此结果证明移除了达梦冷元数据对 SQL 派发的依赖,**未复现也未宣称解决 #10308 中 280–300 秒的完整生产延迟**。旧游标关闭、驱动及服务端查询耗时仍是独立排查方向。