feat(postgres): add table vacuum action

This commit is contained in:
amwps290 authored and GitHub committed 2026-08-20 16:04:22 +08:00
1 parent 0e0edcd206
commit 11407b537f
23 files changed
+554 -36

No files matched your search

@@ -91,6 +91,18 @@ describe("production SQL safety", () => {
}
});
it("detects VACUUM in a production PostgreSQL database", () => {
const pg = connection({ db_type: "postgres", production_databases: ["prod_app"] });
for (const sql of ['VACUUM "public"."orders"', 'VACUUM ANALYZE "public"."orders"', 'VACUUM FULL ANALYZE "public"."orders"']) {
expect(assessProductionSql(sql, pg, "prod_app")).toMatchObject({ active: true, isMutation: true, databases: ["prod_app"] });
}
});
it("detects qualified VACUUM targets for database-qualified dialects", () => {
const mysql = connection({ db_type: "mysql", production_databases: ["prod_app"] });
expect(assessProductionSql("VACUUM FULL prod_app.orders", mysql, "staging")).toMatchObject({ active: true, isMutation: true, databases: ["prod_app"] });
});
it("detects qualified procedure calls and privilege targets", () => {
for (const sql of ["CALL prod_app.purge_users()", "CALL `prod_app`.`purge_users`()", "GRANT ALL ON prod_app.* TO 'u'@'%'", "GRANT EXECUTE ON PROCEDURE prod_app.purge_users TO 'u'@'%'"]) {
expect(assessProductionSql(sql, connection(), "staging")).toMatchObject({ active: true, isMutation: true, databases: ["prod_app"] });
@@ -88,6 +88,8 @@ export const TREE_SCHEMA_TYPES = new Set<DatabaseType>([
export const DATABASE_OBJECT_TREE_TYPES = new Set<DatabaseType>(["jdbc"]);
export const PG_VACUUM_TYPES = new Set<DatabaseType>(["postgres", "gaussdb", "kwdb", "kingbase", "highgo", "uxdb", "vastbase", "opengauss"]);
export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb"]);
export const DIAGRAM_SQL_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlite", "rqlite", "turso", "cloudflare-d1", "sqlserver", "oracle", "redshift", "dameng", "gaussdb", "kwdb", "opengauss", "questdb", "oceanbase-oracle"]);
@@ -1,7 +1,7 @@
import type { ConnectionConfig, DatabaseType, TreeNodeType } from "@/types/database";
import { supportsDatabaseFeature } from "@/lib/database/databaseDriverManifest";
import { canEditTableStructure } from "@/lib/table/tableStructureCapabilities";
import { CLEARABLE_QUERY_SCHEMA_TYPES, DATABASE_OBJECT_TREE_TYPES, DATABASE_SCHEMA_QUALIFIED_TYPES, FETCH_FIRST_TYPES, PG_LIKE_STRUCTURE_TYPES, SCHEMA_AWARE_TYPES, SINGLE_DATABASE_TYPES, TREE_SCHEMA_TYPES } from "@/lib/database/databaseCapabilitySets";
import { CLEARABLE_QUERY_SCHEMA_TYPES, DATABASE_OBJECT_TREE_TYPES, DATABASE_SCHEMA_QUALIFIED_TYPES, FETCH_FIRST_TYPES, PG_LIKE_STRUCTURE_TYPES, PG_VACUUM_TYPES, SCHEMA_AWARE_TYPES, SINGLE_DATABASE_TYPES, TREE_SCHEMA_TYPES } from "@/lib/database/databaseCapabilitySets";
import { supportsRegisteredConnectionScopedQueryExecution, supportsRegisteredQueryTargetDatabaseListing, usesRegisteredConnectionOnlyQueryTarget } from "@/lib/database/sqlExecutionTargetRegistry";
export function isSchemaAware(dbType?: DatabaseType): boolean {
@@ -196,6 +196,10 @@ export function supportsTableTruncate(dbType?: DatabaseType): boolean {
return !!dbType && dbType !== "impala" && dbType !== "sqlite" && dbType !== "rqlite" && dbType !== "turso" && dbType !== "cloudflare-d1" && dbType !== "duckdb" && dbType !== "influxdb" && dbType !== "victoriametrics" && dbType !== "manticoresearch";
}
export function supportsTableVacuum(dbType?: DatabaseType): boolean {
return !!dbType && PG_VACUUM_TYPES.has(dbType);
}
export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
return !!dbType && PG_LIKE_STRUCTURE_TYPES.has(dbType);
}
@@ -18,6 +18,14 @@ export interface TableAdminSqlOptions {
cascade?: boolean;
}
export interface VacuumTableSqlOptions {
databaseType?: DatabaseType;
schema?: string | null;
tableName: string;
full?: boolean;
analyze?: boolean;
}
export interface MysqlAutoIncrementSqlOptions {
databaseType: DatabaseType;
driverProfile?: string | null;
@@ -286,6 +294,10 @@ export function buildTruncateTableSql(options: TableAdminSqlOptions): Promise<st
return api.buildTruncateTableSql(options);
}
export function buildVacuumTableSql(options: VacuumTableSqlOptions): Promise<string> {
return api.buildVacuumTableSql(options);
}
export function buildMysqlAutoIncrementSql(options: MysqlAutoIncrementSqlOptions): Promise<string> {
return api.buildMysqlAutoIncrementSql(options);
}
@@ -26,6 +26,7 @@ const COPY_TARGET_RE = new RegExp(String.raw`^\s*COPY\s+(${TARGET_NAME_PATTERN})
const TRUNCATE_TARGET_RE = new RegExp(String.raw`\bTRUNCATE\s+(?:TABLE\s+)?(${TARGET_NAME_PATTERN})`, "gi");
const RENAME_TABLE_TARGET_RE = new RegExp(String.raw`\bRENAME\s+TABLE\s+(${TARGET_NAME_PATTERN})\s+TO\s+(${TARGET_NAME_PATTERN})`, "gi");
const MAINTENANCE_TABLE_TARGET_RE = new RegExp(String.raw`\b(?:ANALYZE|OPTIMIZE|REPAIR|CHECK)\s+(?:NO_WRITE_TO_BINLOG\s+|LOCAL\s+)?TABLE\s+(${TARGET_NAME_PATTERN})`, "gi");
const VACUUM_TABLE_TARGET_RE = new RegExp(String.raw`\bVACUUM(?:\s+FULL)?(?:\s+ANALYZE)?\s+(${TARGET_NAME_PATTERN})`, "gi");
const COMMENT_TARGET_RE = new RegExp(String.raw`\bCOMMENT\s+ON\s+(?:TABLE|VIEW|COLUMN|INDEX|SEQUENCE|FUNCTION|PROCEDURE|TYPE)\s+(${TARGET_NAME_PATTERN})`, "gi");
const ROUTINE_CALL_TARGET_RE = new RegExp(String.raw`\b(?:CALL|EXEC|EXECUTE)\s+(${QUALIFIED_NAME_PATTERN})`, "gi");
const PRIVILEGE_TARGET_RE = new RegExp(String.raw`\b(?:GRANT|REVOKE|DENY)\b[\s\S]*?\bON\s+(?:(?:TABLE|SEQUENCE|FUNCTION|PROCEDURE|ROUTINE|OBJECT)\s+|OBJECT\s*::\s*)?(${QUALIFIED_NAME_PATTERN})`, "gi");
@@ -159,7 +160,22 @@ function referencedDatabases(statements: string[], dbType: DatabaseType, activeD
if (!statementIsMutation) continue;
const currentDatabase = useDatabase || normalizedActiveDatabase;
collectQualifiedTargetDatabases(statement, dbType, quotedIdentifiers, currentDatabase, statementDatabases, DML_TARGET_RE, DDL_OBJECT_TARGET_RE, INDEX_ON_TARGET_RE, TRUNCATE_TARGET_RE, MAINTENANCE_TABLE_TARGET_RE, COMMENT_TARGET_RE, ROUTINE_CALL_TARGET_RE, PRIVILEGE_TARGET_RE);
collectQualifiedTargetDatabases(
statement,
dbType,
quotedIdentifiers,
currentDatabase,
statementDatabases,
DML_TARGET_RE,
DDL_OBJECT_TARGET_RE,
INDEX_ON_TARGET_RE,
TRUNCATE_TARGET_RE,
MAINTENANCE_TABLE_TARGET_RE,
VACUUM_TABLE_TARGET_RE,
COMMENT_TARGET_RE,
ROUTINE_CALL_TARGET_RE,
PRIVILEGE_TARGET_RE,
);
collectQualifiedTargetDatabaseGroups(statement, dbType, quotedIdentifiers, currentDatabase, statementDatabases, RENAME_TABLE_TARGET_RE, [1, 2]);
for (const match of statement.matchAll(DATABASE_TARGET_RE)) {
const database = databaseTargetKindMeansDatabase(match[1], dbType) ? normalizeTargetDatabase(match[2], quotedIdentifiers) : "";