fix(sqlite): honor selected-table reply budgets (#4216)

This commit is contained in:
dogan
2026-10-22 13:30:36 -05:00
committed by GitHub
parent b4bc6cf3ee
commit 02a0e72f70
2 changed files with 65 additions and 9 deletions
+18 -9
View File
@@ -255,14 +255,29 @@ const readSchema = (
);
let selectedTable: SelectedTableFacts | undefined;
const metadata = new Map<string, z.output<typeof tableRow>>();
// Names bound the minimum eventual schema output while metadata is collected.
// The full schema objects below account for them in the actual reply budget.
const selectedKey =
selectedName === undefined ? undefined : sqliteIdentifierKey(selectedName);
const unresolvedVirtualTables: { name: string; prefix: string }[] = [];
// Names bound eventual output only when the whole catalog is requested.
// Selected scope retains one table and at most one possible shadow parent;
// unrelated names are examined without retaining them or charging the reply.
const metadataBudget = new OutputBudget();
for (const raw of db.prepare("PRAGMA main.table_list").iterate()) {
const value = tableRow.parse(raw);
if (value.schema === "main") {
if (value.schema !== "main") continue;
if (selectedName === undefined) {
metadataBudget.retain(value.name);
metadata.set(value.name, value);
} else if (value.name === selectedName) {
metadata.set(value.name, value);
}
if (value.type === "virtual" && value.ncol === 0) {
const prefix = sqliteIdentifierKey(`${value.name}_`);
if (
selectedKey === undefined ||
(unresolvedVirtualTables.length === 0 && selectedKey.startsWith(prefix))
)
unresolvedVirtualTables.push({ name: value.name, prefix });
}
}
// A trigger stores its target as written in CREATE TRIGGER, which SQLite
@@ -278,12 +293,6 @@ const readSchema = (
);
return foldedNames.get(sqliteIdentifierKey(written)) ?? written;
};
const unresolvedVirtualTables = [...metadata.values()]
.filter((value) => value.type === "virtual" && value.ncol === 0)
.map((value) => ({
name: value.name,
prefix: sqliteIdentifierKey(`${value.name}_`),
}));
const shadowParentOf = (
name: string,
listed: z.output<typeof tableRow>,
@@ -235,6 +235,53 @@ it("counts schema names once when enforcing the complete reply budget", async ()
);
});
it("keeps unrelated catalog names outside the selected-table reply budget", async () => {
const path = await createDatabase(
"CREATE TABLE selected(value TEXT); INSERT INTO selected VALUES ('retained');",
);
const producer = new DatabaseSync(path);
try {
producer.exec("BEGIN");
for (let index = 0; index < 60; index++) {
const name = `unrelated_${index}_${"x".repeat(300 * 1024)}`;
producer.exec(`CREATE TABLE "${name}" (value TEXT)`);
}
producer.exec("COMMIT");
} finally {
producer.close();
}
const selected = inspectSqliteDatabaseSnapshot(path, {
path,
table: "selected",
});
expect(selected.schema).toMatchObject({
scope: "selected_table",
completeness: "complete",
tables: [{ name: "selected" }],
indexes: [],
views: [],
triggers: [],
});
expect(selected.schema.tables).toHaveLength(1);
expect(selected.rows).toMatchObject({
returned_rows: 1,
values: [[{ type: "text", value: "retained" }]],
});
expect(() =>
inspectSqliteDatabaseSnapshot(path, { path, table: "absent" }),
).toThrowError(
expect.objectContaining({
reason: "selection",
message:
"Selected ordinary table does not exist with that exact name: absent",
}),
);
for (const schema_scope of ["inventory", "database"] as const)
expect(() =>
inspectSqliteDatabaseSnapshot(path, { path, schema_scope }),
).toThrowError(expect.objectContaining({ reason: "output-limit" }));
});
it("rejects malformed schema bytes instead of returning replacement text", async () => {
const path = await createDatabase(
"CREATE TABLE records(value TEXT /*schema-marker*/)",