fix(sqlite): budget schema names by returned text

This commit is contained in:
gensokyo-dev committed 2026-10-23 18:30:52 +00:00
1 parent 5d2c8c2b78
commit 7a46b394e7
2 files changed
+44 -24

No files matched your search

+6 -4
View File
@@ -124,8 +124,10 @@ const missingSelectedTable = (name: string): SqliteInspectionFailure =>
);
// SQLite folds only ASCII letters when matching identifiers. Fold stored
// bytes so replacement decoding cannot make distinct names share an identity.
// Latin-1 maps every byte to one code unit without hex expansion. These opaque
// keys are never returned as text or charged against the JSON reply budget.
const sqliteNameBytesKey = (name: Uint8Array): string =>
Buffer.from(name).toString("hex");
Buffer.from(name.buffer, name.byteOffset, name.byteLength).toString("latin1");
const sqliteIdentifierBytesKey = (
name: Uint8Array,
encoding: DatabaseEncoding,
@@ -147,7 +149,7 @@ const sqliteIdentifierBytesKey = (
bytes[index + asciiOffset] = letter + 0x20;
}
}
return bytes.toString("hex");
return bytes.toString("latin1");
};
const sqliteNameBytes = (
db: DatabaseSync,
@@ -333,14 +335,14 @@ const readSchema = (
const value = tableRow.parse(raw);
const nameKey = sqliteNameBytesKey(value.name);
if (selectedName === undefined) {
metadataBudget.retain(nameKey);
metadataBudget.retain(value.display_name);
metadata.set(nameKey, value);
} else if (nameKey === selectedKey) {
metadata.set(nameKey, value);
}
if (value.type === "virtual" && value.ncol === 0) {
const virtualName = value.display_name;
const prefix = `${sqliteIdentifierBytesKey(value.name, encoding)}${underscore.toString("hex")}`;
const prefix = `${sqliteIdentifierBytesKey(value.name, encoding)}${underscore.toString("latin1")}`;
if (
selectedKey === undefined ||
(unresolvedVirtualTables.length === 0 &&
@@ -217,26 +217,44 @@ it.each(["UTF-8", "UTF-16le", "UTF-16be"])(
},
);
it("counts schema names once when enforcing the complete reply budget", async () => {
const names = Array.from(
{ length: 48 },
(_, index) => `${index}_${"x".repeat(128 * 1024)}`,
);
const path = await createDatabase(
names.map((name) => `CREATE TABLE "${name}" (value TEXT);`).join("\n"),
);
const result = inspectSqliteDatabaseSnapshot(path, {
path,
schema_scope: "database",
});
expect(result.schema.tables).toHaveLength(names.length);
expect(result.schema.tables.map((table) => table.name)).toEqual(
expect.arrayContaining(names),
);
expect(Buffer.byteLength(JSON.stringify(result))).toBeLessThan(
16 * 1024 * 1024,
);
});
it.each(["UTF-8", "UTF-16le", "UTF-16be"])(
"counts returned %s schema names rather than internal lookup keys in the reply budget",
async (encoding) => {
const names = Array.from(
{ length: 48 },
(_, index) => `${index}_${"x".repeat(128 * 1024)}`,
);
const path = await createDatabase(
`PRAGMA encoding='${encoding}'; BEGIN; ${names
.map((name) => `CREATE TABLE "${name}" (value TEXT);`)
.join("\n")} COMMIT;`,
);
for (const schema_scope of ["inventory", "database"] as const) {
const result = inspectSqliteDatabaseSnapshot(path, {
path,
schema_scope,
});
expect(result.database_encoding).toBe(encoding);
expect(result.schema.completeness).toBe("complete");
expect(result.schema.tables.map((table) => table.name).sort()).toEqual(
[...names].sort(),
);
for (const table of result.schema.tables)
expect(table).toMatchObject(
schema_scope === "inventory"
? { columns_completeness: "not_requested", columns: [] }
: {
sql: `CREATE TABLE "${table.name}" (value TEXT)`,
columns_completeness: "complete",
columns: [{ name: "value", declared_type: "TEXT" }],
},
);
expect(
Buffer.byteLength(JSON.stringify({ ok: true, inspection: result })),
).toBeLessThan(16 * 1024 * 1024);
}
},
);
it("keeps unrelated catalog names outside the selected-table reply budget", async () => {
const path = await createDatabase(