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 {
|
||||
if (!result) return undefined;
|
||||
const columnValues = result.columns.map((_, colIndex) => result.rows.map((row) => row[colIndex] ?? null));
|
||||
return removeUndefinedFields({
|
||||
const rowCount = result.rows.length;
|
||||
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],
|
||||
execution_error: result.execution_error,
|
||||
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_values: result.spatial_values?.map((row) => [...row]),
|
||||
large_value_cells: result.large_value_cells?.map((cell) => ({ ...cell })),
|
||||
columnValues,
|
||||
rowCount: result.rows.length,
|
||||
rowCount,
|
||||
mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined,
|
||||
mongo_copy_documents: result.mongo_copy_documents ? clonePlain(result.mongo_copy_documents) : undefined,
|
||||
affected_rows: result.affected_rows,
|
||||
@@ -413,6 +423,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult
|
||||
sourceFrom: result.sourceFrom,
|
||||
sourceTo: result.sourceTo,
|
||||
});
|
||||
return { ...metadata, columnValues };
|
||||
}
|
||||
|
||||
function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResult | undefined {
|
||||
@@ -445,18 +456,17 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul
|
||||
}
|
||||
|
||||
function snapshotToPayload(snapshot: TabResultSnapshot): TabResultSnapshotPayload {
|
||||
return removeUndefinedFields({
|
||||
...snapshot,
|
||||
result: toColumnarResult(snapshot.result),
|
||||
results: snapshot.results?.map((result) => toColumnarResult(result)!),
|
||||
resultRuns: snapshot.resultRuns?.map((run) =>
|
||||
removeUndefinedFields({
|
||||
...run,
|
||||
result: toColumnarResult(run.result),
|
||||
results: run.results?.map((result) => toColumnarResult(result)!),
|
||||
}),
|
||||
),
|
||||
});
|
||||
const { result, results, resultRuns, ...metadata } = snapshot;
|
||||
return {
|
||||
...removeUndefinedFields(metadata),
|
||||
result: toColumnarResult(result),
|
||||
results: results?.map((result) => toColumnarResult(result)!),
|
||||
resultRuns: resultRuns?.map(({ result, results, ...run }) => ({
|
||||
...removeUndefinedFields(run),
|
||||
result: toColumnarResult(result),
|
||||
results: results?.map((result) => toColumnarResult(result)!),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function payloadToSnapshot(payload: TabResultSnapshotPayload): TabResultSnapshot {
|
||||
@@ -725,7 +735,7 @@ export function encodeTabResultSnapshot(snapshot: TabResultSnapshot): Uint8Array
|
||||
columnCount: stats.columnCount,
|
||||
payload: snapshotToPayload(snapshot),
|
||||
};
|
||||
return encode(removeUndefinedFields(envelope));
|
||||
return encode(envelope, { ignoreUndefined: true });
|
||||
}
|
||||
|
||||
export function decodeTabResultSnapshot(bytes: Uint8Array | ArrayBuffer): TabResultSnapshot | undefined {
|
||||
|
||||
Reference in new issue
Block a user