fix(sql): skip metadata refresh for temporary tables
This commit is contained in:
parent
e00a6a8b52
commit
9057be0e78
|
|
@ -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" });
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue