fix: decode .sqlplan files the way SSMS writes them; name the parse failure
- lib/plan-file.ts: BOM-aware decode for UTF-8, UTF-16LE/BE (with and without BOM) and UTF-32, with a NUL-density fallback for BOM-less UTF-16. The browser had a two-byte FF FE sniff only, so a UTF-16BE or BOM-less plan arrived as NUL-filled junk and the digest blamed the export. - digestPlan now quotes fast-xml-parser's first line instead of a fixed sentence, so a truncated file, a stray entity and a wrong file stay distinguishable. - plan-file.ts holds the decode so the XML parser stays out of the client bundle. - tests: 6 encodings round-trip through decodePlan into a digest; a truncated plan's message is asserted to carry a reason; e2e drops a real UTF-16LE file. 36 unit, 23 e2e. - history.spec picked the rail's first row, which is now one of the new failing-input rows; it selects the newest finished run instead. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
3d1866367b
commit
4b5c19b5d2
@@ -76,3 +76,35 @@ test("a real .sqlplan digests", () => {
|
||||
assert.ok(d.topOperators.length > 0, "no operators found — check attribute spellings");
|
||||
assert.ok(d.sql.length > 0);
|
||||
});
|
||||
|
||||
// ---- plan file decoding (lib/plan-file.ts: the browser reads a .sqlplan into bytes, then hands text to the digest) ----
|
||||
const enc = (s: string) => new TextEncoder().encode(s);
|
||||
const utf16le = (s: string) => { const b = new Uint8Array(s.length * 2); for (let i = 0; i < s.length; i++) { b[i * 2] = s.charCodeAt(i); b[i * 2 + 1] = 0; } return b; };
|
||||
const utf16be = (s: string) => { const b = new Uint8Array(s.length * 2); for (let i = 0; i < s.length; i++) { b[i * 2] = 0; b[i * 2 + 1] = s.charCodeAt(i); } return b; };
|
||||
const sample = '<?xml version="1.0"?><ShowPlanXML xmlns="http://schemas.microsoft.com/sqlserver/2004/07/showplan" Version="1.0"><BatchSequence><Batch><Statements><StmtSimple StatementText="SELECT 1"/></Statements></Batch></BatchSequence></ShowPlanXML>';
|
||||
|
||||
test("every .sqlplan encoding SSMS and friends produce arrives as clean text", async () => {
|
||||
const { decodePlan } = await import("../lib/plan-file.ts");
|
||||
const cases: [string, Uint8Array][] = [
|
||||
["utf-8", enc(sample)],
|
||||
["utf-8 + bom", enc("\uFEFF" + sample)],
|
||||
["utf-16le + bom (what SSMS writes)", Uint8Array.from([0xff, 0xfe, ...utf16le(sample)])],
|
||||
["utf-16be + bom", Uint8Array.from([0xfe, 0xff, ...utf16be(sample)])],
|
||||
["utf-16le, no bom", utf16le(sample)],
|
||||
["utf-16be, no bom", utf16be(sample)],
|
||||
];
|
||||
for (const [name, bytes] of cases) {
|
||||
const out = decodePlan(bytes);
|
||||
assert.ok(out.startsWith("<?xml"), `${name}: starts with ${JSON.stringify(out.slice(0, 12))}`);
|
||||
assert.ok(!out.includes("\0"), `${name}: still contains NUL bytes`);
|
||||
assert.equal(digestPlan(out).sql, "SELECT 1", `${name}: digest after decode`);
|
||||
}
|
||||
});
|
||||
|
||||
test("a parse failure says why instead of blaming the export", async () => {
|
||||
const { PlanError } = await import("../lib/digest.ts");
|
||||
const e: unknown = (() => { try { digestPlan("<ShowPlanXML><Unclosed></ShowPlanXML>"); return null; } catch (e) { return e; } })();
|
||||
assert.ok(e instanceof PlanError);
|
||||
assert.ok(e.message.length > 40, `message too thin: ${e.message}`);
|
||||
assert.notEqual(e.message, "That XML will not parse — re-export the actual execution plan.");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user