fix(cache): avoid repeated copies when encoding query results
Refs: #9755
This commit is contained in:
2 files changed
+137
-17
No files matched your search
@@ -0,0 +1,110 @@
|
|||||||
|
import { decode } from "@msgpack/msgpack";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { decodeTabResultSnapshot, encodeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
|
||||||
|
import type { QueryResult } from "@/types/database";
|
||||||
|
|
||||||
|
function createResult(rowCount: number): QueryResult {
|
||||||
|
return {
|
||||||
|
columns: ["id", "name", "enabled"],
|
||||||
|
rows: Array.from({ length: rowCount }, (_, index) => [index, `row-${index}`, index % 2 === 0]),
|
||||||
|
affected_rows: 0,
|
||||||
|
execution_time_ms: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("result cache serialization work", () => {
|
||||||
|
it.each([
|
||||||
|
{ columns: [], rows: [] },
|
||||||
|
{ columns: [], rows: [[]] },
|
||||||
|
{ columns: ["first", "second"], rows: [[1], []] },
|
||||||
|
])("preserves empty and ragged result shapes: %j", ({ columns, rows }) => {
|
||||||
|
const encoded = encodeTabResultSnapshot({ result: { columns, rows, affected_rows: 0, execution_time_ms: 0 }, cachedAt: 1 });
|
||||||
|
expect(decodeTabResultSnapshot(encoded)?.result?.rows).toEqual(rows.map((row) => columns.map((_, columnIndex) => row[columnIndex] ?? null)));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not repeatedly copy column vectors while cleaning optional metadata", () => {
|
||||||
|
const rowCount = 1000;
|
||||||
|
const result = createResult(rowCount);
|
||||||
|
let fullLengthMaps = 0;
|
||||||
|
const originalMap = Array.prototype.map;
|
||||||
|
const mapSpy = vi.spyOn(Array.prototype, "map").mockImplementation(function (this: unknown[], callback, thisArg) {
|
||||||
|
if (this.length === rowCount) fullLengthMaps += 1;
|
||||||
|
return originalMap.call(this, callback, thisArg);
|
||||||
|
});
|
||||||
|
let encoded: Uint8Array;
|
||||||
|
try {
|
||||||
|
encoded = encodeTabResultSnapshot({ result, cachedAt: 1 });
|
||||||
|
} finally {
|
||||||
|
mapSpy.mockRestore();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(fullLengthMaps).toBeLessThanOrEqual(result.columns.length);
|
||||||
|
expect(decodeTabResultSnapshot(encoded)?.result?.rows).toEqual(result.rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads each source row once when converting a wide result to columns", () => {
|
||||||
|
const result = createResult(100);
|
||||||
|
let rowReads = 0;
|
||||||
|
result.rows = new Proxy(result.rows, {
|
||||||
|
get(target, property, receiver) {
|
||||||
|
if (typeof property === "string" && /^\d+$/.test(property)) rowReads += 1;
|
||||||
|
return Reflect.get(target, property, receiver);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const encoded = encodeTabResultSnapshot({ result, cachedAt: 1 });
|
||||||
|
|
||||||
|
expect(rowReads).toBe(result.rows.length);
|
||||||
|
expect(decodeTabResultSnapshot(encoded)?.result?.rows).toEqual(createResult(100).rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the version-1 payload and metadata cleanup for results and saved runs", () => {
|
||||||
|
const result = createResult(2);
|
||||||
|
result.rows = [
|
||||||
|
[1, "", false],
|
||||||
|
[2, null, true],
|
||||||
|
];
|
||||||
|
result.session_id = "live-session";
|
||||||
|
result.mongo_documents = [{ retained: null, omitted: undefined, nested: { empty: undefined, values: [null, undefined, 0] }, date: new Date(0) }];
|
||||||
|
const encoded = encodeTabResultSnapshot({
|
||||||
|
result,
|
||||||
|
results: [result],
|
||||||
|
resultRuns: [{ id: "run-1", title: "Run 1", sequence: 1, sql: "select 1", createdAt: 1, result, results: [result] }],
|
||||||
|
cachedAt: 1,
|
||||||
|
});
|
||||||
|
const envelope = decode(encoded) as any;
|
||||||
|
const expectedResult = {
|
||||||
|
columns: ["id", "name", "enabled"],
|
||||||
|
columnValues: [
|
||||||
|
[1, 2],
|
||||||
|
["", null],
|
||||||
|
[false, true],
|
||||||
|
],
|
||||||
|
rowCount: 2,
|
||||||
|
mongo_documents: [{ retained: null, nested: { values: [null, null, 0] }, date: {} }],
|
||||||
|
affected_rows: 0,
|
||||||
|
execution_time_ms: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(envelope).toEqual({
|
||||||
|
magic: "DBX_TAB_RESULT_CACHE",
|
||||||
|
version: 1,
|
||||||
|
codec: "msgpack-columnar",
|
||||||
|
cachedAt: 1,
|
||||||
|
rowCount: 2,
|
||||||
|
columnCount: 3,
|
||||||
|
payload: {
|
||||||
|
result: expectedResult,
|
||||||
|
results: [expectedResult],
|
||||||
|
resultRuns: [{ id: "run-1", title: "Run 1", sequence: 1, sql: "select 1", createdAt: 1, result: expectedResult, results: [expectedResult] }],
|
||||||
|
cachedAt: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.rows).toEqual([
|
||||||
|
[1, "", false],
|
||||||
|
[2, null, true],
|
||||||
|
]);
|
||||||
|
expect(result.mongo_documents[0]).toHaveProperty("omitted", undefined);
|
||||||
|
expect(result.session_id).toBe("live-session");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -388,8 +388,19 @@ function stripResultRunSessionIds(resultRuns: QueryTab["resultRuns"]): QueryTab[
|
|||||||
|
|
||||||
function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult | undefined {
|
function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult | undefined {
|
||||||
if (!result) return undefined;
|
if (!result) return undefined;
|
||||||
const columnValues = result.columns.map((_, colIndex) => result.rows.map((row) => row[colIndex] ?? null));
|
const rowCount = result.rows.length;
|
||||||
return removeUndefinedFields({
|
const columnValues = result.columns.map(() => {
|
||||||
|
const values: CellValue[] = [];
|
||||||
|
values.length = rowCount;
|
||||||
|
return values;
|
||||||
|
});
|
||||||
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
||||||
|
const row = result.rows[rowIndex];
|
||||||
|
for (let columnIndex = 0; columnIndex < columnValues.length; columnIndex++) {
|
||||||
|
columnValues[columnIndex][rowIndex] = row?.[columnIndex] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const metadata = removeUndefinedFields({
|
||||||
columns: [...result.columns],
|
columns: [...result.columns],
|
||||||
execution_error: result.execution_error,
|
execution_error: result.execution_error,
|
||||||
statement_index: result.statement_index,
|
statement_index: result.statement_index,
|
||||||
@@ -398,8 +409,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult
|
|||||||
spatial_columns: result.spatial_columns?.map((entry) => ({ column_index: entry.column_index, srid: entry.srid })),
|
spatial_columns: result.spatial_columns?.map((entry) => ({ column_index: entry.column_index, srid: entry.srid })),
|
||||||
spatial_values: result.spatial_values?.map((row) => [...row]),
|
spatial_values: result.spatial_values?.map((row) => [...row]),
|
||||||
large_value_cells: result.large_value_cells?.map((cell) => ({ ...cell })),
|
large_value_cells: result.large_value_cells?.map((cell) => ({ ...cell })),
|
||||||
columnValues,
|
rowCount,
|
||||||
rowCount: result.rows.length,
|
|
||||||
mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined,
|
mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined,
|
||||||
mongo_copy_documents: result.mongo_copy_documents ? clonePlain(result.mongo_copy_documents) : undefined,
|
mongo_copy_documents: result.mongo_copy_documents ? clonePlain(result.mongo_copy_documents) : undefined,
|
||||||
affected_rows: result.affected_rows,
|
affected_rows: result.affected_rows,
|
||||||
@@ -413,6 +423,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult
|
|||||||
sourceFrom: result.sourceFrom,
|
sourceFrom: result.sourceFrom,
|
||||||
sourceTo: result.sourceTo,
|
sourceTo: result.sourceTo,
|
||||||
});
|
});
|
||||||
|
return { ...metadata, columnValues };
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResult | undefined {
|
function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResult | undefined {
|
||||||
@@ -445,18 +456,17 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul
|
|||||||
}
|
}
|
||||||
|
|
||||||
function snapshotToPayload(snapshot: TabResultSnapshot): TabResultSnapshotPayload {
|
function snapshotToPayload(snapshot: TabResultSnapshot): TabResultSnapshotPayload {
|
||||||
return removeUndefinedFields({
|
const { result, results, resultRuns, ...metadata } = snapshot;
|
||||||
...snapshot,
|
return {
|
||||||
result: toColumnarResult(snapshot.result),
|
...removeUndefinedFields(metadata),
|
||||||
results: snapshot.results?.map((result) => toColumnarResult(result)!),
|
result: toColumnarResult(result),
|
||||||
resultRuns: snapshot.resultRuns?.map((run) =>
|
results: results?.map((result) => toColumnarResult(result)!),
|
||||||
removeUndefinedFields({
|
resultRuns: resultRuns?.map(({ result, results, ...run }) => ({
|
||||||
...run,
|
...removeUndefinedFields(run),
|
||||||
result: toColumnarResult(run.result),
|
result: toColumnarResult(result),
|
||||||
results: run.results?.map((result) => toColumnarResult(result)!),
|
results: results?.map((result) => toColumnarResult(result)!),
|
||||||
}),
|
})),
|
||||||
),
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function payloadToSnapshot(payload: TabResultSnapshotPayload): TabResultSnapshot {
|
function payloadToSnapshot(payload: TabResultSnapshotPayload): TabResultSnapshot {
|
||||||
@@ -725,7 +735,7 @@ export function encodeTabResultSnapshot(snapshot: TabResultSnapshot): Uint8Array
|
|||||||
columnCount: stats.columnCount,
|
columnCount: stats.columnCount,
|
||||||
payload: snapshotToPayload(snapshot),
|
payload: snapshotToPayload(snapshot),
|
||||||
};
|
};
|
||||||
return encode(removeUndefinedFields(envelope));
|
return encode(envelope, { ignoreUndefined: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeTabResultSnapshot(bytes: Uint8Array | ArrayBuffer): TabResultSnapshot | undefined {
|
export function decodeTabResultSnapshot(bytes: Uint8Array | ArrayBuffer): TabResultSnapshot | undefined {
|
||||||
|
|||||||
Reference in new issue
Block a user