fix(sql): skip metadata refresh for temporary tables

This commit is contained in:
zipg 2026-08-08 10:44:21 +08:00 committed by GitHub
parent e00a6a8b52
commit 9057be0e78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 2 deletions

View File

@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { sqlMetadataRefreshTarget } from "@/lib/sql/sqlMetadataRefresh";
describe("sqlMetadataRefreshTarget", () => {
it.each(["CREATE TEMP TABLE session_rows (id int)", "CREATE GLOBAL TEMPORARY TABLE session_rows (id int)", "DROP TABLE #temp_list", "DROP TABLE IF EXISTS ##global_temp", "ALTER TABLE [#temp_list] ADD note nvarchar(50)"])(
"does not refresh persistent metadata for temporary table DDL: %s",
(sql) => {
expect(sqlMetadataRefreshTarget(sql, "dbo")).toEqual({ scope: "none" });
},
);
it("still refreshes the active schema for persistent table DDL", () => {
expect(sqlMetadataRefreshTarget("DROP TABLE orders", "dbo")).toEqual({ scope: "database", schema: "dbo" });
});
it("does not treat MySQL hash comments as metadata changes", () => {
expect(sqlMetadataRefreshTarget("SELECT 1; # DROP TABLE orders", "app")).toEqual({ scope: "none" });
});
it("refreshes persistent DDL while ignoring temporary statements in the same batch", () => {
expect(sqlMetadataRefreshTarget("CREATE TABLE #stage (id int); ALTER TABLE sales.orders ADD note varchar(50)", "dbo")).toEqual({ scope: "database", schema: "sales" });
});
});

View File

@ -4,15 +4,23 @@ export type SqlMetadataRefreshTarget = { scope: "none" } | { scope: "connection"
const DATABASE_DDL_RE = /\b(CREATE|DROP)\s+DATABASE\b/i;
const SCHEMA_DDL_RE = /\b(CREATE|DROP)\s+SCHEMA\b/i;
const OBJECT_DDL_RE = /\b(CREATE|ALTER|DROP|RENAME)\s+(OR\s+REPLACE\s+)?(((GLOBAL|LOCAL)\s+)?TEMP(ORARY)?\s+)?(MATERIALIZED\s+)?(TABLE|VIEW|INDEX|SEQUENCE|PROCEDURE|FUNCTION|TRIGGER|TYPE)\b/i;
const EXPLICIT_TEMP_OBJECT_DDL_RE = /\b(?:CREATE|ALTER|DROP|RENAME)\s+(?:OR\s+REPLACE\s+)?(?:(?:GLOBAL|LOCAL)\s+)?TEMP(?:ORARY)?\s+(?:TABLE|VIEW|INDEX)\b/i;
const SQLSERVER_TEMP_TABLE_DDL_RE = /\b(?:CREATE|ALTER|DROP)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:\[##?[^\]]+\]|##?[A-Za-z_][\w$#]*)/i;
const OBJECT_NAME_DDL_RE =
/\b(?:CREATE|ALTER|DROP|RENAME)\s+(?:OR\s+REPLACE\s+)?(?:(?:(?:GLOBAL|LOCAL)\s+)?TEMP(?:ORARY)?\s+)?(?:MATERIALIZED\s+)?(?:TABLE|VIEW|SEQUENCE|PROCEDURE|FUNCTION|TRIGGER|TYPE)\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?((?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][\w$]*)\s*\.\s*(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][\w$]*))/i;
const INDEX_TABLE_DDL_RE = /\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][\w$]*)\s+ON\s+((?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][\w$]*)\s*\.\s*(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][\w$]*))/i;
const SQLSERVER_TEMP_TABLE_TOKEN_RE = /(\b(?:CREATE|ALTER|DROP)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?)(\[?)(##?)([A-Za-z_][\w$#]*)(\]?)/gi;
const TEMP_HASH_PLACEHOLDER = "\u0000dbx-temp-hash\u0000";
function stripSqlMetadataComments(sql: string): string {
return sql
.replace(SQLSERVER_TEMP_TABLE_TOKEN_RE, (_match, prefix: string, openBracket: string, hashes: string, name: string, closeBracket: string) => {
return `${prefix}${openBracket}${hashes.replaceAll("#", TEMP_HASH_PLACEHOLDER)}${name}${closeBracket}`;
})
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/--.*$/gm, " ")
.replace(/#.*$/gm, " ");
.replace(/#.*$/gm, " ")
.replaceAll(TEMP_HASH_PLACEHOLDER, "#");
}
export function sqlMetadataRefreshScope(sql: string): SqlMetadataRefreshScope {
@ -44,6 +52,10 @@ function schemaFromObjectDdl(statement: string): string | undefined {
return match?.[1] ? schemaFromQualifiedName(match[1]) : undefined;
}
function isTemporaryObjectDdl(statement: string): boolean {
return EXPLICIT_TEMP_OBJECT_DDL_RE.test(statement) || SQLSERVER_TEMP_TABLE_DDL_RE.test(statement);
}
export function sqlMetadataRefreshTarget(sql: string, activeSchema?: string): SqlMetadataRefreshTarget {
const statements = splitSqlMetadataStatements(sql);
if (statements.some((stmt) => DATABASE_DDL_RE.test(stmt))) return { scope: "connection" };
@ -57,6 +69,7 @@ export function sqlMetadataRefreshTarget(sql: string, activeSchema?: string): Sq
continue;
}
if (!OBJECT_DDL_RE.test(statement)) continue;
if (isTemporaryObjectDdl(statement)) continue;
hasDatabaseRefresh = true;
const schema = schemaFromObjectDdl(statement) || activeSchema;
if (schema) schemaTargets.add(schema);

View File

@ -14,13 +14,17 @@ test("schema DDL refreshes the selected database tree", () => {
test("object DDL refreshes the selected database tree", () => {
assert.equal(sqlMetadataRefreshScope("CREATE TABLE users (id int);"), "database");
assert.equal(sqlMetadataRefreshScope("CREATE TEMP TABLE scratch (id int);"), "database");
assert.equal(sqlMetadataRefreshScope("CREATE MATERIALIZED VIEW daily_users AS SELECT 1;"), "database");
assert.equal(sqlMetadataRefreshScope("ALTER TABLE users ADD COLUMN name text;"), "database");
assert.equal(sqlMetadataRefreshScope("DROP VIEW active_users;"), "database");
assert.equal(sqlMetadataRefreshScope("CREATE OR REPLACE FUNCTION f() RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;"), "database");
});
test("temporary table DDL does not refresh persistent metadata trees", () => {
assert.equal(sqlMetadataRefreshScope("CREATE TEMP TABLE scratch (id int);"), "none");
assert.deepEqual(sqlMetadataRefreshTarget("CREATE TEMPORARY TABLE scratch (id int);"), { scope: "none" });
});
test("qualified object DDL refreshes the matching schema node", () => {
assert.deepEqual(sqlMetadataRefreshTarget("CREATE TABLE public.users (id int);"), {
scope: "database",